ABSTRACT The C language is like a carving knife: simple,sharp, and extremely useful in skilled hands. Like any sharp tool, Ccan injure people who don't know how to handle it. 0,Introduction 5,Library Functions1,Lexical Pitfalls 6,The Preprocessor2,Syntactic Pitfalls 7,Portability Pitfalls3,Linkage 8,This Space Available4,Semantic Pitfalls References(5-----end)
-------------------------------------------------------------------------------- Library Functions BACK-TO-TOP Every useful C program must use library functions, because there is no way of doing input or output built into the language. In this section, we look at some cases where some widely-available library functions behave in ways that the programmer might not expect. 5.1. Getc Returns an Integer Consider the following program: #include main() { char c; while ((c = getchar()) != EOF) putchar (c); }This program looks like it should copy its standard input to its standard output. In fact, it doesn't quite do this. The reason is that c is declared as a character rather than as an integer. This means that it is impossible for c to hold every possible character as well as EOF. Thus there are two possibilities. Either some legitimate input character will cause c to take on the same value as EOF, or it will be impossible for c to have the value EOF at all. In the former case, the program will stop copying in the middle of certain files. In the latter case, the program will go into an infinite loop. Actually, there is a third case: the program may work by coincidence. The C Reference Manual defines the result of the expression ((c = getchar()) != EOF)quite rigorously. Section 6.1 states: When a longer integer is converted to a shorter or to a char, it is truncated on the left; excess bits are simply discarded. Section 7.14 states: There are a number of assignment operators, all of which group right-to-left. All require an lvalue as their left operand, and the type of an assignment expression is that of its left operand. The value is the value stored in the left operand after the assignment has taken place. The combined effect of these two sections is to require that the result of getchar be truncated to a character value by discarding the high-order bits, and that this truncated value then be compared with EOF. As part of this comparison, the value of c must be extended to an integer, either by padding on the left with zero bits or by sign extension, as appropriate. However, some compilers do not implement this expression correctly. They properly assign the low-order bits of the value of getchar to c. However, instead of then comparing c to EOF, they compare the entire value of getchar! A compiler that does this will make the sample program shown above appear to work "correctly.'' 5.2. Buffered Output and Memory Allocation When a program produces output, how important is it that a human be able to see that output immediately? It depends on the program. For example, if the output is going to a terminal and is asking the person sitting at that terminal to answer a question, it is crucial that the person see the output in order to be able to know what to type. On the other hand, if the output is going to a file, from where it will eventually be sent to a line printer, it is only important that all the output get there eventually. It is often more expensive to arrange for output to appear immediately than it is to save it up for a while and write it later on in a large chunk. For this reason, C implementations typically afford the programmer some control over how much output is to be produced before it is actually written. That control is often vested in a library function called setbuf. If buf is a character array of appropriate size, then setbuf (stdout, buf);tells the I/O library that all output written to stdout should henceforth use buf as an output buffer, and that output directed to stdout should not actually be written until buf becomes full or until the programmer directs it to be written by calling fflush. The appropriate size for such a buffer is defined as BUFSIZ in . Thus, the following program illustrates the obvious way to use setbuf in a program that copies its standard input to its standard output: #include main() { int c; char buf[BUFSIZ]; setbuf (stdout, buf); while ((c = getchar()) != EOF) putchar (c); }Unfortunately, this program is wrong, for a subtle reason. To see where the trouble lies, ask when the buffer is flushed for the last time. Answer: after the main program has finished, as part of the cleaning up that the library does before handing control back to the operating system. But by that time, the buffer has already been freed! There are two ways to prevent this sort of trouble. First, make the buffer static, either by declaring it explicitly as static: static char buf[BUFSIZ];or by moving the declaration outside the main program entirely. Another possibility is to allocate the buffer dynamically and never free it: char *malloc(); setbuf (stdout, malloc (BUFSIZ));Note that in this latter case, it is unnecessary to check if malloc was successful, because if it fails it will return a null pointer. A null pointer is an acceptable second argument to setbuf; it requests that stdout be unbuffered. This will work slowly, but it will work. -------------------------------------------------------------------------------- The Preprocessor BACK-TO-TOP The programs we run are not the programs we write: they are first transformed by the C preprocessor. The preprocessor gives us a way of abbreviating things that is important for two major reasons (and several minor ones). First, we may want to be able to change all instances of a particular quantity, such as the size of a table, by changing one number and recompiling the program. Second, we may want to define things that appear to be functions but do not have the execution overhead normally associated with a function call. For example, getchar and putchar are usually implemented as macros to avoid having to call a function for each character of input or output. 6.1. Macros are not Functions Because macros can be made to appear almost as if they were functions, programmers are sometimes tempted to regard them as truly equivalent. Thus, one sees things like this: #define max(a,b) ((a)>(b)?(a):(b))Notice all the parentheses in the macro body. They defend against the possibility that a or b might be expressions that contain operators of lower precedence than >. The main problem, though, with defining things like max as macros is that an operand that is used twice may be evaluated twice. Thus, in this example, if a is greater than b, a will be evaluated twice: once during the comparison, and again to calculate the value yielded by max. Not only can this be inefficient, it can also be wrong: biggest = x[0]; i = 1; while (i < n) biggest = max (biggest, x[i++]);This would work fine if max were a true function, but fails with max a macro. Suppose, for example, that x[0] is 2, x[1] is 3, and x[2] is 1. Look at what happens during the first iteration of the loop. The assignment statement expands into: biggest = ((biggest)>(x[i++])?(biggest):(x[i++]));First, biggest is compared to x[i++]. Since i is 1 and x[1] is 3, the relation is false. As a side effect, i is incremented to 2. Because the relation is false, the value of x[i++] is now assigned to biggest. However, i is now 2, so the value assigned to biggest is the value of x[2], which is 1. One way around these worries is to ensure that the arguments to the max macro do not have any side effects: biggest = x[0]; for (i = 1; i < n; i++) biggest = max (biggest, x[i]);Here is another example of the hazards of mixing side effects and macros. This is the definition of the putc macro from in the Eighth Edition of the Unix system: #define putc(x,p) ((p)?_cnt>=0?(*(p)?_ptr++=(x)):_flsbuf(x,p))The first argument to putc is a character to be written to a file; the second argument is a pointer to an internal data structure that describes the file. Notice that the first argument, which could easily be something like *z++, is carefully evaluated only once, even though it appears in two separate places in the macro body, while the second argument is evaluated twice (in the macro body, x appears twice, but since the two occurrences are on opposite sides of a : operator, exactly one of them will be evaluated in any single instance of putc). Since it is unusual for the file argument to putc to have side effects, this rarely causes trouble. Nevertheless, it is documented in the user's manual: "Because it is implemented as a macro, putc treats a stream argument with side effects improperly. In particular, putc(c,*f++) doesn't work sensibly.'' Notice that putc(*c++,f) works fine in this implementation. Some C implementations are less careful. For instance, not everyone handles putc(*c++,f) correctly. As another example, consider the toupper function that appears in many C libraries. It translates a lower-case letter to the corresponding upper-case letter while leaving other characters unchanged. If we assume that all the lower-case letters and all the upper-case letters are contiguous (with a possible gap between the cases), we get the following function: toupper(c) { if (c >= 'a' && c <= 'z') c += 'A' ?'a'; return c; }In most C implementations, the subroutine call overhead is much longer than the actual calculations, so the implementor is tempted to make it a macro: #define toupper(c) ((c)>='a' && (c)<='z'? (c)+('A'?a'): (c))This is indeed faster than the function in many cases. However, it will cause a surprise for anyone who tries to use toupper(*p++). Another thing to watch out for when using macros is that they may generate very large expressions indeed. For example, look again at the definition of max: #define max(a,b) ((a)>(b)?(a):(b))Suppose we want to use this definition to find the largest of a, b, c, and d. If we write the obvious: max(a,max(b,max(c,d))) this expands to: ((a)>(((b)>(((c)>(d)?(c):(d)))?(b):(((c)>(d)?(c):(d)))))? (a):(((b)>(((c)>(d)?(c):(d)))?(b):(((c)>(d)?(c):(d))))))which is surprisingly large. We can make it a little less large by balancing the operands: max(max(a,b),max(c,d))which gives: ((((a)>(b)?(a):(b)))>(((c)>(d)?(c):(d)))? (((a)>(b)?(a):(b))):(((c)>(d)?(c):(d))))Somehow, though, it seems easier to write: biggest = a; if (biggest < b) biggest = b; if (biggest < c) biggest = c; if (biggest < d) biggest = d;6.2. Macros are not Type Definitions One common use of macros is to permit several things in diverse places to be the same type: #define FOOTYPE struct foo FOOTYPE a; FOOTYPE b, c;This lets the programmer change the types of a, b, and c just by changing one line of the program, even if a, b, and c are declared in widely different places. Using a macro definition for this has the advantage of portability -- any C compiler supports it. Most C compilers also support another way of doing this: typedef struct foo FOOTYPE;This defines FOOTYPE as a new type that is equivalent to struct foo. These two ways of naming a type may appear to be equivalent, but the typedef is more flexible. Consider, for example, the following: #define T1 struct foo * typedef struct foo *T2;These definitions make T1 and T2 conceptually equivalent to a pointer to a struct foo. But look what happens when we try to use them with more than one variable: T1 a, b; T2 c, d;The first declaration gets expanded to struct foo * a, b;This defines a to be a pointer to a structure, but defines b to be a structure (not a pointer). The second declaration, in contrast, defines both c and d as pointers to structures, because T2 behaves as a true type. -------------------------------------------------------------------------------- Portability Pitfalls BACK-TO-TOP C has been implemented by many people to run on many machines. Indeed, one of the reasons to write programs in C in the first place is that it is easy to move them from one programming environment to another. However, because there are so many implementors, they do not all talk to each other. Moreover, different systems have different requirements, so it is reasonable to expect C implementations to differ slightly between one machine and another. Because so many of the early C implementations were associated with the UNIX operating system, the nature of many of these functions was shaped by that system. When people started implementing C under other systems, they tried to make the library behave in ways that would be familiar to programmers used to the UNIX system. They did not always succeed. What is more, as more people in different parts of the world started working on different versions of the UNIX system, the exact nature of some of the library functions inevitably diverged. Today, a C programmer who wishes to write programs useful in someone else's environment must know about many of these subtle differences. 7.1. What's in a Name? Some C compilers treat all the characters of an identifier as being significant. Others ignore characters past some limit when storing identifiers. C compilers usually produce object programs that must then be processed by loaders in order to be able to access library subroutines. Loaders, in turn, often impose their own restrictions on the kinds of names they can handle. One common loader restriction is that letters in external names must be in upper case only. When faced with such a restriction, it is reasonable for a C implementor to force all external names to upper case. Restrictions of this sort are blessed by section 2.1 the C reference manual: An identifier is a sequence of letters and digits; the first character must be a letter. The underscore _ counts as as a letter. Upper and lower case letters are different. No more than the first eight characters are significant, although more may be used. External identifiers, which are used by various assemblers and loaders, are more restricted: Here, the reference manual goes on to give examples of various implementations that restrict external identifiers to a single case, or to fewer than eight characters, or both. Because of all this, it is important to be careful when choosing identifiers in programs intended to be portable. Having two subroutines named, say print_fields and print_float would not be a very good idea. As a striking example, consider the following function: char * Malloc (n) unsigned n; { char *p, *malloc(); p = malloc (n); if (p == NULL) panic ("out of memory"); return p; }This function is a simple way of ensuring that running out of memory will not go undetected. The idea is for the program to allocate memory by calling Malloc instead of malloc. If malloc ever fails, the result will be to call panic which will presumably terminate the program with an appropriate error message. Consider, however, what happens when this function is used on a system that ignores case distinctions in external identifiers. In effect, the names malloc and Malloc become equivalent. In other words, the library function malloc is effectively replaced by the Malloc function above, which when it calls malloc is really calling itself. The result, of course, is that the first attempt to allocate memory results in a recursion loop and consequent mayhem, even though the function will work on an implementation that preserves case distinctions. 7.2. How Big is an Integer? C provides the programmer with three sizes of integers: ordinary, short, and long, and with characters, which behave as if they were small integers. The language definition does not guarantee much about the relative sizes of the various kinds of integer: 1. The four sizes of integers are non-decreasing. 2. An ordinary integer is large enough to contain any array subscript. 3. The size of a character is natural for the particular hardware. Most modern machines have 8-bit characters, though a few have 7-or 9-bit characters, so characters are usually 7, 8, or 9 bits. Long integers are usually at least 32 bits long, so that a long integer can be used to represent the size of a file. Ordinary integers are usually at least 16 bits long, because shorter integers would impose too much of a restriction on the maximum size of an array. Short integers are almost always exactly 16 bits long. What does this all mean in practice? The most important thing is that one cannot count on having any particular precision available. Informally, one can probably expect 16 bits for a short or an ordinary integer, and 32 bits for a long integer, but not even those sizes are guaranteed. One can certainly use ordinary integers to express table sizes and subscripts, but what about a variable that must be able to hold values up to ten million? The most portable way to do that is probably to define a "new'' type: typedef long tenmil;Now one can use this type to declare a variable of that width and know that, at worst, one will have to change a single type definition to get all those variables to be the right type. 7.3. Are Characters Signed or Unsigned? Most modern computers support 8-bit characters, so most modern C compilers implement characters as 8-bit integers. However, not all compilers interpret those 8-bit quantities the same way. The issue becomes important only when converting a char quantity to a larger integer. Going the other way, the results are well-defined: excess bits are simply discarded. But a compiler converting a char to an int has a choice: should it treat the char as a signed or an unsigned quantity? If the former, it should expand the char to an int by replicating the sign bit; if the latter, it should fill the extra bit positions with zeroes. The results of this decision are important to virtually anyone who deals with characters with their high-order bits turned on. It determines whether 8-bit characters are going to be considered to range from -128 through 127 or from 0 through 255. This, in turn, affects the way the programmer will design things like hash tables and translate tables. If you care whether a character value with the high-order bit on is treated as a negative number, you should probably declare it as unsigned char. Such values are guaranteed to be zero-extended when converted to integer, whereas ordinary char variables may be signed in one implementation and unsigned in another. Incidentally, it is a common misconception that if c is a character variable, one can obtain the unsigned integer equivalent of c by writing (unsigned) c. This fails because a char quantity is converted to int before any operator is applied to it, even a cast. Thus c is converted first to a signed integer and then to an unsigned integer, with possibly unexpected results. The right way to do it is (unsigned char) c. 7.4. Are Right Shifts Signed or Unsigned? This bears repeating: a program that cares how shifts are done had better declare the quantities being shifted as unsigned. 7.5. How Does Division Truncate? Suppose we divide a by b to give a quotient q and remainder r: q = a / b; r = a % b;For the moment, suppose also that b>0. What relationships might we want to hold between a, b, p, and q? 1. Most important, we want q*b + r == a, because this is the relation that defines the remainder. 2. If we change the sign of a, we want that to change the sign of q, but not the absolute value. 3. We want to ensure that r>=0 and r These three properties are clearly desirable for integer division and remainder operations. Unfortunately, they cannot all be true at once. Consider 3/2, giving a quotient of 1 and a remainder of 1. This satisfies property 1. What should be the value of - 3/2? Property 2 suggests that it should be - 1, but if that is so, the remainder must also be -1, which violates property 3. Alternatively, we can satisfy property 3 by making the remainder 1, in which case property 1 demands that the quotient be - 2. This violates property 2. Thus C, and any language that implements truncating integer division, must give up at least one of these three principles. Most programming languages give up number 3, saying instead that the remainder has the same sign as the dividend. This makes it possible to preserve properties 1 and 2. Most C implementations do this in practice, also. However, the C language definition only guarantees property 1, along with the property that |r|< |b| and that r is not less than 0 whenever a is not less than 0 and b > 0. This property is less restrictive than either property 2 or property 3, and actually permits some rather strange implementations that would be unlikely to occur in practice (such as an implementation that always truncates the quotient away from zero). Despite its sometimes unwanted flexibility, the C definition is enough that we can usually make integer division do what we want, provided that we know what we want. Suppose, for example, that we have a number n that represents some function of the characters in an identifier, and we want to use division to obtain a hash table entry h such that h is not less than 0 and h h = n % HASHSIZE;However, if n might be negative, this is not good enough, because h might also be negative. However, we know that h >-HASHSIZE, so we can write: h = n % HASHSIZE; if (h < 0) h += HASHSIZE;Better yet, declare n as unsigned. 7.6. How Big is a Random Number? This size ambiguity has affected library design as well. When the only C implementation ran on the PDP-11 computer, there was a function called rand that returned a (pseudo-) random non-negative integer. PDP-11 integers were 16 bits long, including the sign, so rand would return an integer between 0 and 2^15 - 1. When C was implemented on the VAX-11, integers were 32 bits long. What was the range of the rand function on the VAX-11? For their system, the people at the University of California took the view that rand should return a value that ranges over all possible non-negative integers, so their version of rand returns an integer between 0 and 2^31 - 1. The people at AT&T, on the other hand, decided that a PDP-11 program that expected the result of rand to be less than 2^15 would be easier to transport to a VAX-11 if the rand function returned a value between 0 and 2^15 there, too. As a result, it is now difficult to write a program that uses rand without tailoring it to the implementation. CÏÝÚåÓëȱÏÝ(english)(3) £Û ×÷ÕߣºAndrew koenig תÌù×Ô£º±¾Õ¾ µã»÷Êý£º355 ¸üÐÂʱ¼ä£º2003-12-30 ÎÄÕ¼È룺¶«ÄÏ·É £Ý 7.7. Case Conversion The toupper and tolower functions have a similar history. They were originally written as macros: #define toupper(c) ((c)+'A'?a') #define tolower(c) ((c)+'a'?A')When given a lower-case letter as input toupper yields the corresponding upper-case letter. Tolower does the opposite. Both these macros depend on the implementation's character set to the extent that they demand that the difference between an upper-case letter and the corresponding lower-case letter be the same constant for all letters. This assumption is valid for both the ASCII and EBCDIC character sets, and probably isn't too dangerous, because the non-portability of these macro definitions can be encapsulated in the single file that contains them. These macros do have one disadvantage, though: when given something that is not a letter of the appropriate case, they return garbage. Thus, the following innocent program fragment to convert a file to lower case doesn't work with these macros: int c; while ((c = getchar()) != EOF) putchar (tolower (c));Instead, one must write: int c; while ((c = getchar()) != EOF) putchar (isupper (c)? tolower (c): c);At one point, some enterprising soul in the UNIX development organization at AT&T noticed that most uses of toupper and tolower were preceded by tests to ensure that their arguments were appropriate. He considered rewriting the macros this way: #define toupper(c) ((c) >= 'a' && (c) <= 'z'? (c) + 'A' ?'a': (c)) #define tolower(c) ((c) >= 'A' && (c) <= 'Z'? (c) + 'a' ?'A': (c))but realized that this would cause c to be evaluated anywhere between one and three times for each call, which would play havoc with expressions like toupper(*p++). Instead, he decided to rewrite toupper and tolower as functions. Toupper now looked something like this: int toupper (c) int c; { if (c >= 'a' && c <= 'z') return c + 'A' ?'a'; return c; }and tolower looked similar. This change had the advantage of robustness, at the cost of introducing function call overhead into each use of these functions. Our hero realized that some people might not be willing to pay the cost of this overhead, so he re-introduced the macros with new names: #define _toupper(c) ((c)+'A'?a') #define _tolower(c) ((c)+'a'?A')This gave users a choice of convenience or speed. There was just one problem in all this: the people at Berkeley never followed suit, nor did some other C implementors. This means that a program written on an AT&T system that uses toupper or tolower, and assumes that it will be able to pass an argument that is not a letter of the appropriate case, may stop working on some other C implementation. This sort of failure is very hard to trace for someone who does not know this bit of history. 7.8. Free First, then Reallocate Most C implementations provide users with three memory allocation functions called malloc, realloc, and free. Calling malloc(n) returns a pointer to n characters of newly-allocated memory that the programmer can use. Giving free a pointer to memory previously returned by malloc makes that memory available for re-use. Calling realloc with a pointer to an allocated area and a new size stretchesm or shrinks the memory to the new size, possibly copying it in the process. Or so one might think. The truth is actually somewhat more subtle. Here is an excerpt from the description of realloc that appears in the System V Interface Definition: Realloc changes the size of the block pointed to by ptr to size bytes and returns a pointer to the (possibly moved) block. The contents will be unchanged up to the lesser of the new and old sizes. The Seventh Edition of the reference manual for the UNIX system contains a copy of the same paragraph. In addition, it contains a second paragraph describing realloc: Realloc also works if ptr points to a block freed since the last call of malloc, realloc, or calloc; thus sequences of free, malloc and realloc can exploit the search strategy of malloc to do storage compaction. Thus, the following is legal under the Seventh Edition: free (p); p = realloc (p, newsize);This idiosyncrasy remains in systems derived from the Seventh Edition: it is possible to free a storage area and then reallocate it. By implication, freeing memory on these systems is guaranteed not to change its contents until the next time memory is allocated. Thus, on these systems, one can free all the elements of a list by the following curious means: for (p = head; p != NULL; p = p->next) free ((char *) p);without worrying that the call to free might invalidate p->next. Needless to say, this technique is not recommended, if only because not all C implementations preserve memory long enough after it has been freed. However, the Seventh Edition manual leaves one thing unstated: the original implementation of realloc actually required that the area given to it for reallocation be free first. For this reason, there are many C programs floating around that free memory first and then reallocate it, and this is something to watch out for when moving a C program to another implementation. 7.9. An Example of Portability Problems Let's take a look at a problem that has been solved many times by many people. The following program takes two arguments: a long integer and a (pointer to a) function. It converts the integer to decimal and calls the given function with each character of the decimal representation. void printnum (n, p) long n; void (*p)(); { if (n < 0) { (*p) ('?); n = -n; } if (n >= 10) printnum (n/10, p); (*p) (n % 10 + '0'); }This program is fairly straightforward. First we check if n is negative; if so, we print a sign and make n positive. Next, we test if n is not less than 10. If so, its decimal representation has two or more digits, so we call printnum recursively to print all but the last digit. Finally, we print the last digit. This program, for all its simplicity, has several portability problems. The first is the method it uses to convert the low-order decimal digit of n to character form. Using n%10 to get the value of the low-order digit is fine, but adding '0' to it to get the corresponding character representation is not. This addition assumes that the machine collating sequence has all the digits in sequence with no gaps, so that '0'+5 has the same value as '5', and so on. This assumption, while true of the ASCII and EBCDIC character sets, might not be true for some machines. The way to avoid that problem is to use a table: void printnum (n, p) long n; void (*p)(); { if (n < 0) { (*p) ('-'); n =-n; } if (n >= 10) printnum (n/10, p); (*p) ("0123456789"[n % 10]); }The next problem involves what happens if n < 0. The program prints a negative sign and sets n to -n. This assignment might overflow, because 2's complement machines generally allow more negative values than positive values to be represented. In particular, if a (long) integer is k bits plus one extra bit for the sign, - 2^k can be represented but 2^k cannot. There are several ways around this problem. The most obvious one is to assign n to an unsigned long value and be done with it. However, some C compilers do not implement unsigned long, so let us see how we can get along without it. In both 1's complement and 2's complement machines, changing the sign of a positive integer is guaranteed not to overflow. The only trouble comes when changing the sign of a negative value. Therefore, we can avoid trouble by making sure we do not attempt to make n positive. Of course, once we have printed the sign of a negative value, we would like to be able to treat negative and positive numbers the same way. The way to do that is to force n to be negative after printing the sign, and to do all our arithmetic with negative values. If we do this, we will have to ensure that the part of the program that prints the sign is executed only once; the easiest way to do that is to split the program into two functions: void printnum (n, p) long n; void (*p)(); { void printneg(); if (n < 0) { (*p) ('-'); printneg (n, p); } else printneg (-n, p); } void printneg (n, p) long n; void (*p)(); { if (n<=-10) printneg (n/10, p); (*p) ("0123456789"[-(n % 10)]); } Printnum now just checks if the number being printed is negative; if so it prints a negative sign. In either case, it calls printneg with the negative absolute value of n. We have also modified the body of printneg to cater to the fact that n will always be a negative number or zero. Or have we? We have used n/10 and n%10 to represent the leading digits and the trailing digit of n (with suitable sign changes). Recall that integer division behaves in a somewhat implementation-dependent way when one of the operands is negative. For that reason, it might actually be that n%10 is positive! In that case, -(n%10) would be negative, and we would run off the end of our digit array. We cater to this problem by creating two temporary variables to hold the quotient and remainder. After we do the division, we check that the remainder is in range and adjust both variables if not. Printnum has not changed, so we show only printneg: void printneg (n, p) long n; void (*p)(); { long q; int r; q = n / 10; r = n % 10; if (r > 0) { r - =10; q++; } if (n <= -10) printneg (q, p); (*p) ("0123456789"[-r]); } -------------------------------------------------------------------------------- This Space Available BACK-TO-TOP There are many ways for C programmers to go astray that have not been mentioned in this paper. If you find one, please contact the author. It may well be included, with an acknowledging footnote, in a future revision. -------------------------------------------------------------------------------- References BACK-TO-TOP The C Programming Language (Kernighan and Ritchie, Prentice-Hall 1978) is the definitive work on C. It contains both an excellent tutorial, aimed at people who are already familiar with other high-level languages, and a reference manual that describes the entire language succinctly. While the language has expanded slightly since 1978, this book is still the last word on most subjects. This book also contains the "C Reference Manual'' we have mentioned several times in this paper. The C Puzzle Book (Feuer, Prentice-Hall, 1982) is an unusual way to hone one's syntactic skills. The book is a collection of puzzles (and answers) whose solutions test the reader's knowledge of C's fine points. C: A Reference Manual (Harbison and Steele, Prentice Hall 1984) is mostly intended as a reference source for implementors. Other users may also find it useful, particularly because of its meticulous cross references. -------------------------------------------------------------------------------- Formated by Qin Shengchao, Revised by Qiu Zongyan. Peking University, 2001/2/10(完)计算机基础教程网
C traps and pitfalls2
[转帖]DataGrid的自定义分页UserCont…
自定义 StringTable 的自动完成功能 …
Using the Command pattern for undo…
PickColor Control 2004 Source Code…
WinForm C#: Simple Runtime Control…
Nucleus.MockAOP.Net:OpenSource .N…
在 Intranet 环境中保护 .NET Web 应…
关于TreeView 的使用…
Together for .net2.0 vs rational X…
Coalesys WebMenu for ASP.NET 2.1使…
NET Framework…
ORACLE 常用的SQL语法和数据对象…
Grasshopper简介(节选)
通过.NET访问 Oracle数据库…
Asp.Net中的脚本回调和Server.Transf…
如何设置tabcontrol控件的tabPage的t…
浅谈VB.NET文章系列之一 --通过例子,…
C#版MultiSelected DataGrid…
P&P Enterprise Library Extensions…
通过例子,浅谈反射(Reflection)的应…
自定义 StringTable 的自动完成功能 …
Using the Command pattern for undo…
PickColor Control 2004 Source Code…
WinForm C#: Simple Runtime Control…
Nucleus.MockAOP.Net:OpenSource .N…
在 Intranet 环境中保护 .NET Web 应…
关于TreeView 的使用…
Together for .net2.0 vs rational X…
Coalesys WebMenu for ASP.NET 2.1使…
NET Framework…
ORACLE 常用的SQL语法和数据对象…
Grasshopper简介(节选)
通过.NET访问 Oracle数据库…
Asp.Net中的脚本回调和Server.Transf…
如何设置tabcontrol控件的tabPage的t…
浅谈VB.NET文章系列之一 --通过例子,…
C#版MultiSelected DataGrid…
P&P Enterprise Library Extensions…
通过例子,浅谈反射(Reflection)的应…
相关栏目导航
