C traps and pitfalls Andrew Koenig 2003-12-30 ABSTRACT The C language is like a carving knife: simple, sharp, and extremely useful in skilled hands. Like any sharp tool, C can injure people who don't know how to handle it. 0,Introduction 5,Library Functions 1,Lexical Pitfalls 6,The Preprocessor 2,Syntactic Pitfalls 7,Portability Pitfalls 3,Linkage 8,This Space Available 4,Semantic Pitfalls References
(beginning-----4)
traps and pitfalls Andrew Koenig 2003-12-30 ABSTRACT
The C language is like a carving knife: simple, sharp, and extremely useful in skilled hands. Like any sharp tool, C can injure people who don't know how to handle it.
0,Introduction 5,Library Functions
1,Lexical Pitfalls 6,The Preprocessor
2,Syntactic Pitfalls 7,Portability Pitfalls
3,Linkage 8,This Space Available
4,Semantic Pitfalls References
Introduction
The C language and its typical implementations are designed to be used easily by experts. The language is terse and expressive. There are few restrictions to keep the user from blundering. A user who has blundered is often rewarded by an effect that is not obviously related to the cause.
In this paper, we will look at some of these unexpected rewards. Because they are unexpected, it may well be impossible to classify them completely. Nevertheless, we have made a rough effort to do so by looking at what has to happen in order to run a C program. We assume the reader has at least a passing acquaintance with the C language.
Section 1 looks at problems that occur while the program is being broken into tokens. Section 2 follows the program as the compiler groups its tokens into declarations, expressions, and statements. Section 3 recognizes that a C program is often made out of several parts that are compiled separately and bound together. Section 4 deals with misconceptions of meaning: things that happen while the program is actually running. Section 5 examines the relationship between our programs and the library routines they use. In section 6 we note that the program we write is not really the program we run; the preprocessor has gotten at it first. Finally, section 7 discusses portability problems: reasons a program might run on one implementation and not another.
--------------------------------------------------------------------------------
Lexical Pitfalls BACK-TO-TOP
The first part of a compiler is usually called a lexical analyzer. This looks at the sequence of characters that make up the program and breaks them up into tokens. A token is a sequence of one or more characters that have a (relatively) uniform meaning in the language being compiled. In C, for instance, the token -> has a meaning that is quite distinct from that of either of the characters that make it up, and that is independent of the context in which the -> appears.
For another example, consider the statement:
if (x > big) big = x;
Each non-blank character in this statement is a separate token, except for the keyword if and the two instances of the identifier big.
In fact, C programs are broken into tokens twice. First the preprocessor reads the program. It must tokenize the program so that it can find the identifiers, some of which may represent macros. It must then replace each macro invocation by the result of evaluating that macro. Finally, the result of the macro replacement is reassembled into a character stream which is given to the compiler proper. The compiler then breaks the stream into tokens a second time.
In this section, we will explore some common misunderstandings about the meanings of tokens and the relationship between tokens and the characters that make them up. We will talk about the preprocessor later.
1.1. = is not ==
Programming languages derived from Algol, such as Pascal and Ada, use := for assignment and = for comparison. C, on the other hand, uses = for assignment and == for comparison. This is because assignment is more frequent than comparison, so the more common meaning is given to the shorter symbol.
Moreover, C treats assignment as an operator, so that multiple assignments (such as a=b=c) can be written easily and assignments can be embedded in larger expressions.
This convenience causes a potential problem: one can inadvertently write an assignment where one intended a comparison. Thus, this statement, which looks like it is checking whether x is equal to y:
if (x = y)
foo();
actually sets x to the value of y and then checks whether that value is nonzero. Or consider the following loop that is intended to skip blanks, tabs, and newlines in a file:
while (c == ' ' || c = 't' || c == 'n')
c = getc (f);
The programmer mistakenly used = instead of == in the comparison with 't'. This "comparison'' actually assigns 't' to c and compares the (new) value of c to zero. Since 't' is not zero, the "comparison'' will always be true, so the loop will eat the entire file. What it does after that depends on whether the particular implementation allows a program to keep reading after it has reached end of file. If it does, the loop will run forever.
Some C compilers try to help the user by giving a warning message for conditions of the form e1 = e2. To avoid warning messages from such compilers, when you want to assign a value to a variable and then check whether the variable is zero, consider making the comparison explicit. In other words, instead of:
if (x = y)
foo();
write:
if ((x = y) != 0)
foo();
This will also help make your intentions plain.
1.2. & and | are not && or ||
It is easy to miss an inadvertent substitution of = for == because so many other languages use = for comparison. It is also easy to interchange & and &&, or | and ||, especially because the & and | operators in C are different from their counterparts in some other languages. We will look at these operators more closely in section 4.
1.3. Multi-character Tokens
Some C tokens, such as /, *, and =, are only one character long. Other C tokens, such as /* and ==, and identifiers, are several characters long. When the C compiler encounters a / followed by an *, it must be able to decide whether to treat these two characters as two separate tokens or as one single token. The C reference manual tells how to decide: "If the input stream has been parsed into tokens up to a given character, the next token is taken to include the longest string of characters which could possibly constitute a token.'' Thus, if a / is the first character of a token, and the / is immediately followed by a *, the two characters begin a comment, regardless of any other context.
The following statement looks like it sets y to the value of x divided by the value pointed to by p:
y = x/*p /* p points at the divisor */;
In fact, /* begins a comment, so the compiler will simply gobble up the program text until the */ appears. In other words, the statement just sets y to the value of x and doesn't even look at p. Rewriting this statement as
y = x / *p /* p points at the divisor */;
or even
y = x/(*p) /* p points at the divisor */;
would cause it to do the division the comment suggests.
This sort of near-ambiguity can cause trouble in other contexts. For example, older versions of C use =+ to mean what present versions mean by +=. Such a compiler will treat
a=-1;
as meaning the same thing as
a =- 1;
or
a = a - 1;
This will surprise a programmer who intended
a = -1;
On the other hand, compilers for these older versions of C would interpret
a=/*b;
as
a =/ * b ;
even though the /* looks like a comment.
1.4. Exceptions
Compound assignment operators such as += are really multiple tokens. Thus,
a + /* strange */ = 1
means the same as
a += 1
These operators are the only cases in which things that look like single tokens are really multiple tokens. In particular,
p - > a
is illegal. It is not a synonym for
p -> a
As another example, the >> operator is a single token, so >>= is made up of two tokens, not three. On the other hand, those older compilers that still accept =+ as a synonym for += treat =+ as a single token.
1.5. Strings and Characters
Single and double quotes mean very different things in C, and there are some contexts in which confusing them will result in surprises rather than error messages.
A character enclosed in single quotes is just another way of writing an integer. The integer is the one that corresponds to the given character in the implementation's collating sequence. Thus, in an ASCII implementation, 'a' means exactly the same thing as 0141 or 97. A string enclosed in double quotes, on the other hand, is a short-hand way of writing a pointer to a nameless array that has been initialized with the characters between the quotes and an extra character whose binary value is zero.
The following two program fragments are equivalent:
printf ("Hello worldn");
char hello[] = {'H', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd', 'n', 0}; printf (hello);
Using a pointer instead of an integer (or vice versa) will often cause a warning message, so using double quotes instead of single quotes (or vice versa) is usually caught. The major exception is in function calls, where most compilers do not check argument types. Thus, saying
printf('n');
instead of
printf ("n");
will usually result in a surprise at run time.
Because an integer is usually large enough to hold several characters, some C compilers permit multiple characters in a character constant. This means that writing 'yes' instead of "yes" may well go undetected. The latter means "the address of the first of four consecutive memory locations containing y, e, s, and a null character, respectively.'' The former means "an integer that is composed of the values of the characters y, e, and s in some implementation-defined manner.'' Any similarity between these two quantities is purely coincidental.
--------------------------------------------------------------------------------
Syntactic Pitfalls BACK-TO-TOP
To understand a C program, it is not enough to understand the tokens that make it up. One must also understand how the tokens combine to form declarations, expressions, statements, and programs. While these combinations are usually well-defined, the definitions are sometimes counter-intuitive or confusing.
In this section, we look at some syntactic constructions that are less than obvious.
2.1. Understanding Declarations
I once talked to someone who was writing a C program that was going to run stand-alone in a small microprocessor. When this machine was switched on, the hardware would call the subroutine whose address was stored in location 0.
In order to simulate turning power on, we had to devise a C statement that would call this subroutine explicitly. After some thought, we came up with the following:
(*(void(*)())0)();
Expressions like these strike terror into the hearts of C programmers. They needn't, though, because they can usually be constructed quite easily with the help of a single, simple rule: declare it the way you use it.
Every C variable declaration has two parts: a type and a list of stylized expressions that are expected to evaluate to that type. The simplest such expression is a variable:
float f, g;
indicates that the expressions f and g, when evaluated, will be of type float. Because the thing declared is an expression, parentheses may be used freely:
float ((f));
means that ((f)) evaluates to a float and therefore, by inference, that f is also a float.
Similar logic applies to function and pointer types. For example,
float ff();
means that the expression ff() is a float, and therefore that ff is a function that returns a float. Analogously,
float *pf;
means that *pf is a float and therefore that pf is a pointer to a float.
These forms combine in declarations the same way they do in expressions. Thus
float *g(), (*h)();
says that *g() and (*h)() are float expressions. Since () binds more tightly than *, *g() means the same thing as *(g()): g is a function that returns a pointer to a float, and h is a pointer to a function that returns a float.
Once we know how to declare a variable of a given type, it is easy to write a cast for that type: just remove the variable name and the semicolon from the declaration and enclose the whole thing in parentheses. Thus, since
float *g();
declares g to be a function returning a pointer to a float, (float *()) is a cast to that type.
Armed with this knowledge, we are now prepared to tackle (*(void(*)())0)(). We can analyze this statement in two parts. First, suppose that we have a variable fp that contains a function pointer and we want to call the function to which fp points. That is done this way:
(*fp)();
If fp is a pointer to a function, *fp is the function itself, so (*fp)() is the way to invoke it. The parentheses in (*fp) are essential because the expression would otherwise be interpreted as *(fp()). We have now reduced the problem to that of finding an appropriate expression to replace fp.
This problem is the second part of our analysis. If C could read our mind about types, we could write:
(*0)();
This doesn't work because the * operator insists on having a pointer as its operand. Furthermore, the operand must be a pointer to a function so that the result of * can be called. Thus, we need to cast 0 into a type loosely described as "pointer to function returning void.''
If fp is a pointer to a function returning void, then (*fp)() is a void value, and its declaration would look like this:
void (*fp)();
Thus, we could write:
void (*fp)();
(*fp)();
at the cost of declaring a dummy variable. But once we know how to declare the variable, we know how to cast a constant to that type: just drop the name from the variable declaration. Thus, we cast 0 to a "pointer to function returning void'' by saying:
(void(*)())0
and we can now replace fp by (void(*)())0:
(*(void(*)())0)();
The semicolon on the end turns the expression into a statement.
At the time we tackled this problem, there was no such thing as a typedef declaration. Using it, we could have solved the problem more clearly:
typedef void (*funcptr)();
(* (funcptr) 0)();
2.2. Operators Don't Always Have the Precedence You Want
Suppose that the manifest constant FLAG is an integer with exactly one bit turned on in its binary representation (in other words, a power of two), and you want to test whether the integer variable flags has that bit turned on. The usual way to write this is:
if (flags & FLAG) ...
The meaning of this is plain to most C programmers: an if statement tests whether the expression in the parentheses evaluates to 0 or not. It might be nice to make this test more explicit for documentation purposes:
if (flags & FLAG != 0) ...
The statement is now easier to understand. It is also wrong, because != binds more tightly than &, so the interpretation is now:
if (flags & (FLAG != 0)) ...
This will work (by coincidence) if FLAG is 1 or 0 (!), but not for any other power of two.
Suppose you have two integer variables, h and l, whose values are between 0 and 15 inclusive, and you want to set r to an 8-bit value whose low-order bits are those of l and whose high-order bits are those of h. The natural way to do this is to write:
r = h<<4 + l;
Unfortunately, this is wrong. Addition binds more tightly than shifting, so this example is equivalent to
r = h << (4 + l);
Here are two ways to get it right:
r = (h << 4) + l;
r = h << 4 | l;
One way to avoid these problems is to parenthesize everything, but expressions with too many parentheses are hard to understand, so it is probably useful to try to remember the precedence levels in C. Unfortunately, there are fifteen of them, so this is not always easy to do. It can be made easier, though, by classifying them into groups.
The operators that bind the most tightly are the ones that aren't really operators: subscripting, function calls, and structure selection. These all associate to the left.
Next come the unary operators. These have the highest precedence of any of the true operators. Because function calls bind more tightly than unary operators, you must write (*p)() to call a function pointed to by p; *p() implies that p is a function that returns a pointer. Casts are unary operators and have the same precedence as any other unary operator. Unary operators are right-associative, so *p++ is interpreted as *(p++) and not as (*p)++.
Next come the true binary operators. The arithmetic operators have the highest precedence, then the shift operators, the relational operators, the logical operators, the assignment operators, and finally the conditional operator. The two most important things to keep in mind are:
1. Every logical operator has lower precedence than every relational operator.
2. The shift operators bind more tightly than the relational operators but less tightly than the arithmetic operators.
Within the various operator classes, there are few surprises. Multiplication, division, and remainder have the same precedence, addition and subtraction have the same precedence, and the two shift operators have the same precedence.
One small surprise is that the six relational operators do not all have the same precedence: == and != bind less tightly than the other relational operators. This allows us, for instance, to see if a and b are in the same relative order as c and d by the expression
a < b == c < d
Within the logical operators, no two have the same precedence. The bitwise operators all bind more tightly than the sequential operators, each and operator binds more tightly than the corresponding or operator, and the bitwise exclusive or operator (^) falls between bitwise and and bitwise or.
The ternary conditional operator has lower precedence than any we have mentioned so far. This permits the selection expression to contain logical combinations of relational operators, as in
z = a < b && b < c ? d : e
This example also shows that it makes sense for assignment to have a lower precedence than the conditional operator. Moreover, all the compound assignment operators have the same precedence and they all group right to left, so that
a = b = c
means the same as
b = c; a = b;
Lowest of all is the comma operator. This is easy to remember because the comma is often used as a substitute for the semicolon when an expression is required instead of a statement.
Assignment is another operator often involved in precedence mixups. Consider, for example, the following loop intended to copy one file to another:
while (c=getc(in) != EOF)
putc(c,out);
The way the expression in the while statement is written makes it look like c should be assigned the value of getc(in) and then compared with EOF to terminate the loop. Unhappily, assignment has lower precedence than any comparison operator, so the value of c will be the result of comparing getc(in), the value of which is then discarded, and EOF. Thus, the ``copy'' of the file will consist of a stream of bytes whose value is 1.
It is not too hard to see that the example above should be written:
while ((c=getc(in)) != EOF)
putc(c,out);
However, errors of this sort can be hard to spot in more complicated expressions. For example, several versions of the lint program distributed with the UNIX?system have the following erroneous line:
if( (t=BTYPE(pt1?aty)==STRTY) || t==UNIONTY ){
This was intended to assign a value to t and then see if t is equal to STRTY or UNIONTY. The actual effect is quite different.
The precedence of the C logical operators comes about for historical reasons. B, the predecessor of C, had logical operators that corresponded rougly to C's & and | operators. Although they were defined to act on bits, the compiler would treat them as && and || if they were in a conditional context. When the two usages were split apart in C, it was deemed too dangerous to change the precedence much.
2.3. Watch Those Semicolons!
An extra semicolon in a C program usually makes little difference: either it is a null statement, which has no effect, or it elicits a diagnostic message from the compiler, which makes it easy to remove. One important exception is after an if or while clause, which must be followed by exactly one statement. Consider this example:
if (x[i] > big);
big = x[i];
The semicolon on the first line will not upset the compiler, but this program fragment means something quite different from:
if (x[i] > big)
big = x[i];
The first one is equivalent to:
if (x[i] > big) { }
big = x[i];
which is, of course, equivalent to:
big = x[i];
(unless x, i, or big is a macro with side effects).
Another place that a semicolon can make a big difference is at the end of a declaration just before a
function definition. Consider the following fragment:
struct foo {
int x;
}
f()
{
. . .
}
There is a semicolon missing between the first } and the f that immediately follows it. The effect of this is to declare that the function f returns a struct foo, which is defined as part of this declaration. If the semicolon were present, f would be defined by default as returning an integer.
2.4. The Switch Statement
C is unusual in that the cases in its switch statement can flow into each other. Consider, for example, the following program fragments in C and Pascal:
switch (color) {
case 1: printf ("red");
break;
case 2: printf ("yellow");
break;
case 3: printf ("blue");
break;
}
case color of
1: write ('red');
2: write ('yellow');
3: write ('blue')
end
Both these program fragments do the same thing: print red, yellow, or blue (without starting a new line), depending on whether the variable color is 1, 2, or 3. The program fragments are exactly analogous, with one exception: the Pascal program does not have any part that corresponds to the C break statement. The reason for that is that case labels in C behave as true labels: control can flow unimpeded right through a case label.
Looking at it another way, suppose the C fragment looked more like the Pascal fragment:
switch (color) {
case 1: printf ("red");
case 2: printf ("yellow");
case 3: printf ("blue");
}
and suppose further that color were equal to 2. Then, the program would print yellowblue, because control would pass naturally from the second printf call to the statement after it.
This is both a strength and a weakness of C switch statements. It is a weakness because leaving out a break statement is easy to do, and often gives rise to obscure program misbehavior. It is a strength because by leaving out a break statement deliberately, one can readily express a control structure that is inconvenient to implement otherwise. Specifically, in large switch statements, one often finds that the processing for one of the cases reduces to some other case after a relatively small amount of special handling.
For example, consider a program that is an interpreter for some kind of imaginary machine. Such a program might contain a switch statement to handle each of the various operation codes. On such a machine, it is often true that a subtract operation is identical to an add operation after the sign of the second operand has been inverted. Thus, it is nice to be able to write something like this:
case SUBTRACT:
opnd2 = -opnd2;
/* no break */
case ADD:
. . .
As another example, consider the part of a compiler that skips white space while looking for a token. Here, one would want to treat spaces, tabs, and newlines identically except that a newline should cause a line counter to be incremented:
case 'n':
linecount++;
/* no break */
case 't':
case ' ':
. . .
2.5. Calling Functions
Unlike some other programming languages, C requires a function call to have an argument list, even if there are no arguments. Thus, if f is a function,
f();
is a statement that calls the function, but
f;
does nothing at all. More precisely, it evaluates the address of the function, but does not call it.
2.6. The Dangling else Problem
We would be remiss in leaving any discussion of syntactic pitfalls without mentioning this one. Although it is not unique to C, it has bitten C programmers with many years of experience.
Consider the following program fragment:
if (x == 0)
if (y == 0) error();
else {
z = x + y;
f (&z);
}
The programmer's intention for this fragment is that there should be two main cases: x = 0 and x does not equal to 0. In the first case, the fragment should do nothing at all unless y = 0, in which case it should call error. In the second case, the program should set z = x + y and then call f with the address of z as its argument.
However, the program fragment actually does something quite different. The reason is the rule that an else is always associated with the closest unmatched if. If we were to indent this fragment the way it is actually executed, it would look like this:
if (x == 0) {
if (y == 0)
error();
else {
z = x + y;
f (&z);
}
}
In other words, nothing at all will happen if x does not equal to 0. To get the effect implied by the indentation of the original example, write:
if (x == 0) {
if (y == 0)
error();
} else {
z = x + y;
f (&z);
}
CÏÝÚåÓëȱÏÝ(english)(2)
£Û ×÷ÕߣºAndrew Koenig תÌù×Ô£º±¾Õ¾ µã»÷Êý£º259 ¸üÐÂʱ¼ä£º2003-12-30 ÎÄÕ¼È룺¶«ÄÏ·É £Ý
Linkage BACK-TO-TOP
A C program may consist of several parts that are compiled separately and then bound together by a program usually called a linker, linkage editor, or loader. Because the compiler normally sees only one file at a time, it cannot detect errors whose recognition would require knowledge of several source program files at once.
In this section, we look at some errors of that type. Some C implementations, but not all, have a program called lint that catches many of these errors. It is impossible to overemphasize the importance of using such a program if it is available.
3.1. You Must Check External Types Yourself
Suppose you have a C program divided into two files. One file contains the declaration:
int n;
and the other contains the declaration:
long n;
This is not a valid C program, because the same external name is declared with two different types in the two files. However, many implementations will not detect this error, because the compiler does not know about the contents of either of the two files while it is compiling the other. Thus, the job of checking type consistency can only be done by the linker (or some utility program like lint); if the operating system has a linker that doesn't know about data types, there is little the C compiler can do to force it.
What actually happens when this program is run? There are many possibilities:
1. The implementation is clever enough to detect the type clash. One would then expect to see a diagnostic message explaining that the type of n was given differently in two different files.
2. You are using an implementation in which int and long are really the same type. This is typically true of machines in which 32-bit arithmetic comes most naturally. In this case, your program will probably work as if you had said long (or int) in both declarations. This would be a good example of a program that works only by coincidence.
3. The two instances of n require different amounts of storage, but they happen to share storage in such a way that the values assigned to one are valid for the other. This might happen, for example, if the linker arranged for the int to share storage with the low-order part of the long. Whether or not this happens is obviously machine-and system-dependent. This is an even better example of a program that works only by coincidence.
4. The two instances of n share storage in such a way that assigning a value to one has the effect of apparently assigning a different value to the other. In this case, the program will probably fail.
Another example of this sort of thing happens surprisingly often. One file of a program will contain a declaration like:
char filename[] = "/etc/passwd";
and another will contain this declaration:
char *filename;
Although arrays and pointers behave very similarly in some contexts, they are not the same. In the
first declaration, filename is the name of an array of characters. Although using the name will generate a pointer to the first element of that array, that pointer is generated as needed and not actually kept around.
In the second declaration, filename is the name of a pointer. That pointer points wherever the programmer makes it point. If the programmer doesn't give it a value, it will have a zero (null) value by default.
The two declarations of filename use storage in different ways; they cannot coexist.
One way to avoid type clashes of this sort is to use a tool like lint if it is available. In order to be able to check for type clashes between separately compiled parts of a program, some program must be able to see all the parts at once. The typical compiler does not do this, but lint does.
Another way to avoid these problems is to put external declarations into include files. That way, the type of an external object only appears once.
--------------------------------------------------------------------------------
Semantic Pitfalls BACK-TO-TOP
A sentence can be perfectly spelled and written with impeccable grammar and still be meaningless. In this section, we will look at ways of writing programs that look like they mean one thing but actually mean something quite different.
We will also discuss contexts in which things that look reasonable on the surface actually give undefined results. We will limit ourselves here to things that are not guaranteed to work on any C implementation.
We will leave those that might work on some implementations but not others until section 7, which looks at portability problems.
4.1. Expression Evaluation Sequence
Some C operators always evaluate their operands in a known, specified order. Others don't. Consider, for instance, the following expression:
a < b && c < d
The language definition states that a To evaluate a Only the four C operators &&, ||, ?:, and , specify an order of evaluation. && and || evaluate the left operand first, and the right operand only if necessary. The ?: operator takes three operands: a?b:c evaluates a first, and then evaluates either b or c, depending on the value of a. The , operator evaluates its left operand and discards its value, then evaluates its right operand.
All other C operators evaluate their operands in undefined order. In particular, the assignment operators do not make any guarantees about evaluation order.
For this reason, the following way of copying the first n elements of array x to array y doesn't work:
i = 0;
while (i < n)
y[i] = x[i++];
The trouble is that there is no guarantee that the address of y[i] will be evaluated before i is ncremented.
On some implementations, it will; on others, it won't. This similar version fails for the same reason:
i = 0;
while (i < n)
y[i++] = x[i];
On the other hand, this one will work fine:
i = 0;
while (i < n) {
y[i] = x[i];
i++;
}
This can, of course, be abbreviated:
for (i = 0; i < n; i++)
y[i] = x[i];
4.2. The &&, ||, and ! Operators
C has two classes of logical operators that are occasionally interchangeable: the bitwise operators &, |, and ? and the logical operators &&, ||, and !. A programmer who substitutes one of these operators for the corresponding operator from the other class may be in for a surprise: the program may appear to work correctly after such an interchange but may actually be working only by coincidence.
The &, |, and ?operators treat their operands as a sequence of bits and work on each bit separately. For example, 10&12 is 8 (1000), because & looks at the binary representations of 10 (1010) and 12 (1100) and produces a result that has a bit turned on for each bit that is on in the same position in both operands. Similarly, 10|12 is 14 (1110) and ?0 is --11 (11...110101), at least on a 2's complement machine.
The &&, ||, and ! operators, on the other hand, treat their arguments as if they are either "true'' or "false,'' with the convention that 0 represents "false'' and any other value represents "true.'' These operators return 1 for ``true'' and 0 for ``false,'' and the && and || operators do not even evaluate their right-hand operands if their results can be determined from their left-hand operands.
Thus !10 is zero, because 10 is nonzero, 10&&12 is 1, because both 10 and 12 are nonzero, and 10||12 is also 1, because 10 is nonzero. Moreover, 12 is not even evaluated in the latter expression, nor is f() in 10||f().
Consider the following program fragment to look for a particular element in a table:
i = 0;
while (i < tabsize && tab[i] != x)
i++;
The idea behind this loop is that if i is equal to tabsize when the loop terminates, then the element sought was not found. Otherwise, i contains the element's index.
Suppose that the && were inadvertently replaced by & in this example. Then the loop would probably still appear to work, but would do so only because of two lucky breaks.
The first is that both comparisons in this example are of a sort that yield 0 if the condition is false and 1 if the condition is true. As long as x and y are both 1 or 0, x&y and x&&y will always have the same value. However, if one of the comparisons were to be replaced by one that uses some non-zero value other than 1 to represent ``true,'' then the loop would stop working.
The second lucky break is that looking just one element off the end of an array is usually harmless, provided that the program doesn't change that element. The modified program looks past the end of the array because &, unlike &&, must always evaluate both of its operands. Thus in the last iteration of the loop, the value of tab[i] will be fetched even though i is equal to tabsize. If tabsize is the number of elements in tab, this will fetch a non-existent element of tab.
4.3. Subscripts Start from Zero
In most languages, an array with n elements normally has those elements numbered with subscripts ranging from 1 to n inclusive. Not so in C.
A C array with n elements does not have an element with a subscript of n, as the elements are numbered from 0 through n?. Because of this, programmers coming from other languages must be especially careful when using arrays:
int i, a[10];
for (i=1; i<=10; i++)
a[i] = 0;
This example, intended to set the elements of a to zero, had an unexpected side effect. Because the comparison in the for statement was i<=10 instead of i<10, the non-existent element number 10 of a was set to zero, thus clobbering the word that followed a in memory. The compiler on which this program was run allocates memory for users' variables in decreasing memory locations, so the word after a turned out to be i. Setting i to zero made the loop into an infinite loop.
4.4. C Doesn't Always Cast Actual Parameters
The following simple program fragment fails for two reasons:
double s;
s = sqrt (2);
printf ("%gn", s);
The first reason is that sqrt expects a double value as its argument and it isn't getting one. The second is that it returns a double result but isn't declared as such. One way to correct it is:
double s, sqrt();
s = sqrt (2.0);
printf ("%gn", s);
C has two simple rules that control conversion of function arguments: (1) integer values shorter than an int are converted to int; (2) floating-point values shorter than a double are converted to double. All other values are left unconverted. It is the programmer's responsibility to ensure that the arguments to a function are of the right type.
Therefore, a programmer who uses a function like sqrt, whose parameter is a double, must be careful to pass arguments that are of float or double type only. The constant 2 is an int and is therefore of the wrong type.
When the value of a function is used in an expression, that value is automatically cast to an appropriate type. However, the compiler must know the actual type returned by the function in order to be able to do this. Functions used without further declaration are assumed to return an int, so declarations for such functions are unnecessary. However, sqrt returns a double, so it must be declared as such before it can be used successfully.
In practice, C implementations generally provide a file that can be brought in with an include statement that contains declarations for library functions like sqrt, but writing declarations is still necessary for programmers who write their own functions -- in other words, for anyone who writes non-trivial C programs.
Here is a more spectacular example:
main()
{
int i;
char c;
for (i=0; i<5; i++) {
scanf ("%d", &c);
printf ("%d ", i);
}
printf ("n");
}
Ostensibly, this program reads five numbers from its standard input and writes 0 1 2 3 4 on its standard output. In fact, it doesn't always do that. On one compiler, for example, its output is 0 0 0 0 0 1 2 3 4.
Why? The key is the declaration of c as a char rather than as an int. When you ask scanf to read an integer, it expects a pointer to an integer. What it gets in this case is a pointer to a character. Scanf has no way to tell that it didn't get what it expected: it treats its input as an integer pointer and stores an integer there. Since an integer takes up more memory than a character, this steps on some of the memory near c.
Exactly what is near c is the compiler's business; in this case it turned out to be the low-order part of i. Therefore, each time a value was read for c, it reset i to zero. When the program finally reached end of file, scanf stopped trying to put new values into c, so i could be incremented normally to end the loop.
4.5. Pointers are not Arrays
C programs often adopt the convention that a character string is stored as an array of characters, followed by a null character. Suppose we have two such strings s and t, and we want to concatenate them into a single string r. To do this, we have the usual library functions strcpy and strcat. The following obvious method doesn't work:
char *r;
strcpy (r, s);
strcat (r, t);
The reason it doesn't work is that r is not initialized to point anywhere. Although r is potentially capable of identifying an area of memory, that area doesn't exist until you allocate it.
Let's try again, allocating some memory for r:
char r[100];
strcpy (r, s);
strcat (r, t);
This now works as long as the strings pointed to by s and t aren't too big. Unfortunately, C requires us to state the size of an array as a constant, so there is no way to be certain that r will be big enough. However, most C implementations have a library function called malloc that takes a number and allocates enough memory for that many characters. There is also usually a function called strlen that tells how many characters are in a string. It might seem, therefore, that we could write:
char *r, *malloc();
r = malloc (strlen(s) + strlen(t));
strcpy (r, s);
strcat (r, t);
This example, however, fails for two reasons. First, malloc might run out of memory, an event that it generally signals by quietly returning a null pointer.
Second, and much more important, is that the call to malloc doesn't allocate quite enough memory. Recall the convention that a string is terminated by a null character. The strlen function returns the number of characters in the argument string, excluding the null character at the end. Therefore, if strlen(s) is n, s really requires n+1 characters to contain it. We must therefore allocate one extra character for r. After doing this and checking that malloc worked, we get:
char *r, *malloc();
r = malloc (strlen(s) + strlen(t) + 1);
if (!r) {
complain();
exit (1);
}
strcpy (r, s);
strcat (r, t);
4.6. Eschew Synecdoche
A synecdoche (sin-ECK-duh-key) is a literary device, somewhat like a simile or a metaphor, in which, according to the Oxford English Dictionary, "a more comprehensive term is used for a less comprehensive or vice versa; as whole for part or part for whole, genus for species or species for genus, etc.''
This exactly describes the common C pitfall of confusing a pointer with the data to which it points. This is most common for character strings. For instance:
char *p, *q;
p = "xyz";
It is important to understand that while it is sometimes useful to think of the value of p as the string xyz after the assignment, this is not really true. Instead, the value of p is a pointer to the 0th element of an array of four characters, whose values are 'x', 'y', 'z', and ''. Thus, if we now execute
q = p;
p and q are now two pointers to the same part of memory. The characters in that memory did not get copied by the assignment. The situation now looks like this:
p
|
x y z ''
|
q
The thing to remember is that copying a pointer does not copy the thing it points to.
Thus, if after this we were to execute
q[1] = 'Y';
q would point to memory containing the string xYz. So would p, because p and q point to the same memory.
4.7. The Null Pointer is Not the Null String
The result of converting an integer to a pointer is implementation-dependent, with one important exception. That exception is the constant 0, which is guaranteed to be converted to a pointer that is unequal to any valid pointer. For documentation, this value is often given symbolically:
#define NULL 0
but the effect is the same. The important thing to remember about 0 when used as a pointer is that it must never be dereferenced. In other words, when you have assigned 0 to a pointer variable, you must not ask what is in the memory it points to. It is valid to write:
if (p == (char *) 0) ...
but it is not valid to write:
if (strcmp (p, (char *) 0) == 0) ...
because strcmp always looks at the memory addressed by its arguments.
If p is a null pointer, it is not even valid to say:
printf (p);
or
printf ("%s", p);
4.8. Integer Overflow
The C language definition is very specific about what happens when an integer operation overflows or underflows.
If either operand is unsigned, the result is unsigned, and is defined to be modulo 2 n , where n is the word size. If both operands are signed, the result is undefined.
Suppose, for example, that a and b are two integer variables, known to be non-negative, and you want to test whether a+b might overflow. One obvious way to do it looks something like this:
if (a + b < 0)
complain();
In general, this does not work.
The point is that once a+b has overflowed, all bets are off as to what the result will be. For example, on some machines, an addition operation sets an internal register to one of four states: positive, negative, zero, or overflow. On such a machine, the compiler would have every right to implement the example given above by adding a and b and checking whether this internal register was in negative state afterwards.
If the operation overflowed, the register would be in overflow state, and the test would fail.
One correct way of doing this particular test relies on the fact that unsigned arithmetic is well-defined for all values, as are the conversions between signed and unsigned values:
if ((int) ((unsigned) a + (unsigned) b) < 0)
complain();
4.9. Shift Operators
Two questions seem to cause trouble for people who use shift operators:
1. In a right shift, are vacated bits filled with zeroes or copies of the sign bit?
2. What values are permitted for the shift count?
The answer to the first question is simple but sometimes implementation-dependent. If the item being shifted is unsigned, zeroes are shifted in. If the item is signed, the implementation is permitted to fill vacated bit positions either with zeroes or with copies of the sign bit. If you care about vacated bits in a right shift, declare the variable in question as unsigned. You are then entitled to assume that vacated bits will be set to zero.
The answer to the second question is also simple: if the item being shifted is n bits long, then the shift count must be greater than or equal to zero and strictly less than n. Thus, it is not possible to shift all the bits out of a value in a single operation.
For example, if an int is 32 bits, and n is an int, it is legal to write n<<31 and n<<0 but not n<<32 or n<<-1.
Note that a right shift of a signed integer is generally not equivalent to division by a power of two, even if the implementation copies the sign into vacated bits. To prove this, consider that the value of (-1)>>1 cannot possibly be zero.
|