對於C語言,不同的編譯器采用了不同的實現,並且在不同平台上表現也不同。脫離具體環境探討C的細節行為是沒有意義的,以下是我所使用的環境,大部分內容都經過測試,且所有測試結果基於這個環境獲得,為簡化起見,省略了異常處理。我不希望讀者死記硬背這些細節,而是能在自己的平台上進行實驗從而獲得對應的結果。另外,本文僅僅關注於C,可能會考慮C++的表現,但在C++和C#環境下的編譯器所獲得的看似C代碼而實不同的結果不作為參考。基礎的東西比如“函數參數傳值”、“轉義字符”、“else的最近配對”、“case的下落(fall through)”、“符號常量NULL代表常量0”、“restrict關鍵字”、“使用%p輸出指針”、“const的指針常量和常量指針”等本文不會重復。
了解這些細節並在自己的平台上進行實驗並不是鼓勵你去寫模棱兩可、過於依賴平台和實現的代碼(除非有非這么做不可的必要),而是對這種代碼有鑒別能力和理解能力,盡量避免和修正。
另外,在其他平台上的不同行為歡迎列出,但不會對原文中實現相關、機器相關的細節的具體表現進行補充說明。
如有錯誤,懇請指正。由於目前時間有限,可能不能及時回復,請諒解。
編譯器:gcc 4.4.3,默認無任何編譯選項。
編譯環境:Ubuntu10.04LTS,32x86
標准:默認為ISO C99的GNU方言,不使用任何-std=選項
以下是該環境中man gcc的部分結果:
-std=
Determine the language standard. This option is currently only supported when compiling C or C++.The compiler can accept several base standards, such as c89 or c++98, and GNU dialects of those standards,
such as gnu89 or gnu++98. By specifying a base standard, the compiler will accept all programs following
that standard and those using GNU extensions that do not contradict it. For example, -std=c89 turns off
certain features of GCC that are incompatible with ISO C90, such as the "asm" and "typeof" keywords, but
not other GNU extensions that do not have a meaning in ISO C90, such as omitting the middle term of a "?:"
expression. On the other hand, by specifying a GNU dialect of a standard, all features the compiler
support are enabled, even when those features change the meaning of the base standard and some strict-
conforming programs may be rejected. The particular standard is used by -pedantic to identify which
features are GNU extensions given that version of the standard. For example -std=gnu89 -pedantic would
warn about C++ style // comments, while -std=gnu99 -pedantic would not.A value for this option must be provided; possible values are
...
gnu89
GNU dialect of ISO C90 (including some C99 features). This is the default for C code.gnu99
gnu9x
GNU dialect of ISO C99. When ISO C99 is fully implemented in GCC, this will become the default. The name gnu9x is deprecated....
gcc4.4.3是2010年發布的,編譯器所采用標准的判斷來自於最后一行。
另外,為了進行對照,個別實例會使用Clang進行補充。
主要參考資料:
為了不因不同標准而導致混淆,對於參考資料的引用都將注明出處的簡稱。
1.The C Programming Language 2nd edition,《C程序設計語言(英文版·第2版)》,Brian W. Kernighan & Dennis M. Ritchie 著,以下簡稱K&R
此書被譽為C語言聖經。第2版針對的是1988年的ANSI C,因此並沒有一些后續C標准的變化細節(The C Programming Language 英文維基)。
2.C: A Reference Manual 5th edition,《C語言參考手冊(原書第五版)》,Samuel Harbison III & Guy L. Steele Jr.著,徐波譯,機械工業出版社,以下簡稱CARM
優秀的案頭參考手冊。涵蓋了傳統C、C89、C89修正案1和C99(此書譯者序)。 遺憾的是中文版沒有英文版的術語索引(Index)。
3.ISO/IEC 9899:1999,以下簡稱C99或C99標准
由於使用的編譯器和環境而作為權威的參考。
一、編程細節:
細節1:printf的參數必須使用\n換行(newline)而不是在參數里使用回車。
來源:K&R 1.1,P7
printf("hello,world ");
結果:
編譯器Error。
細節2:printf使用了格式化控制符%d但沒有對應參數
來源:某公司面試題
printf("%d\n");
結果:
編譯器warning: too few arguments for format
運行時顯示一個隨機值。
對照:
使用clang,提示 1 diagnostic generated.
運行時總是顯示0。
分析:
K&R提到,如果參數不夠,會FAIL。C99則把這認定為未定義行為(可參見C99標准中的fprintf部分,它的行為與printf類似)。
相關:
%s對於" "和""的處理演示。我不確定是否實現相關或者未定義行為。
printf("%s|\n%s|\n"," ","");
輸出:
|
|
細節3: getchar()返回值是int,而非char;兼談char型是否有符號及EOF的值
分析:
(c =getchar())!=EOF常用於判斷輸入是否結束,而char的范圍不一定能容納EOF,因此用int接收返回值。
C99:char用於存放基本執行字符集(basic execution character set)時,其值應(is guaranteed to)為正(但0字符應(shall)在基本執行字符集,似乎有點沖突,或許shall可以作為“可以”?)。其他存放於char的字符的值由實現定義。
EOF具體的值在<stdio.h>中定義,但具體數值不重要,只要和char不同即可(K&R)。C99標准將其實現為一個int型負值的宏。
有的實現將EOF定義為-1,這對char是unsigned時和上面的要求相同。有的編輯器將char實現為signed char(如gcc4.4.3),在這種情況下或許使用char型也可以接受getchar()的返回值,但可移植性就不如用int更好。你可以在自己的環境里試試char的是否有符號。
char c; c = -1; if (c<0) printf("oops,char is signed.\n"); else printf("char is unsigned.\n");
或者,你也可以使用signed char和unsigned char這樣的聲明來提高可移植性。(P44,K&R)
至於怎么輸入一個無法輸入的EOF?試試Ctrl+Z或者Ctrl+D吧,這也是和平台實現相關的。
細節4:i++,++i;副作用side effect
來看看K&R的英文描述:But the expression ++n increments n before its value is used, while n++ increments n after its value has been used.很清晰對不對?
另外,自增和自減運算符只能用於變量,(i+j)++是非法的。
CARM明確說明它們的操作數必須是可修改的左值,可以是任何算術類型或指針類型。
細節5:char、short、int和long的精度;float、double、long double
分析:
標准C指定了char至少必須達到8位、short至少為16位、long至少32位、long long至少64位,int是16位還是32位以及前幾個的具體精度與機器位數和實現有關,可以在<limits.h>中查看它們的范圍。(CARM)
一些具體實現里這些數據類型的精度:將 Linux 應用程序移植到 64 位系統上,如果你之前有記住所有實現中數據長度的雄心壯志,看到這個表也會放棄吧?了解自己常用平台上的即可,而且,要非常熟悉。對自己平台都不了解,空談標准、大小關系,沒什么意思。(我以前犯過這個錯誤)
另外,short和long后面的int可以省略。(K&R)
float、double、long double的大小是實現定義的,它們可能是3種、2種或者同1種類型(K&R)。
細節6:C中到底有沒有bool型
分析:
C99標准提供了宏bool,它將被展開為_Bool。使用這個類型以及true和false需要<stdbool.h>的支持。其大小與實現相關,我的環境中測試的結果是1個字節。
使用這個宏的好處是,再也不用自己#define TRUE 1等等這樣定義了。
當然,如果你遇到了一些死板的筆試題問你C是否有bool型?並且,恰好是單選、同時其他選項無比正確、明擺着在誘拐你選擇這一項,那只好舍棄節操委曲求全地說“沒有”了。
細節7:邏輯求值中||和&&的終止條件
分析:
從左往右,一旦整個表達式結果可得即停止運算(K&R)。即一系列||中有一個為真時,后續則不再計算,&&則相反。
順便提一下它們的結合性都是從左到右,而&&高於||。(CARM)
我就不在這里刻意地構造復雜的&&和||表達式來考驗自己和諸位讀者的能力了。為了代碼可讀性,實踐中我也不會刻意地把邏輯表達式弄得太復雜,看情況加括號便是。
細節8:函數定義中,如果返回值類型為int,那么它可以被省略。(K&R,已測)
細節9:extern變量
分析:
(K&R)
在函數“外部”定義的變量,定義時不需要加extern關鍵字。如果函數需要使用,需要一個顯式或隱式的extern的聲明。
簡而言之,一種用法是在函數內使用extern聲明;
另一種是將變量定義在源文件的所有函數之前,這時函數中使用這個變量時就不需要再進行聲明,這只適用於單一文件。
多文件時,最好把各個文件都會用到的外部變量寫入.h文件,並進行頭文件包含,這時函數內使用外部變量可以省略extern聲明。
請注意定義和聲明的區別。前者指變量被創建或分配空間的位置(the place where the variable is created or assigned storage),后者是陳述變量特性但不分配空間的代碼中的地方。
細節10:strlen()不計算'\0'。(K&R)
細節11:枚舉名必須不同,但值可以相同。(K&R)
細節12:取模%不能用於float和double。負數運算時,/的截取方向和%的符號取決於機器,其上溢和下溢時采取的動作也取決於機器。(K&R)
細節13:>、>=、<、<=比==和!=高一級。
細節14:常用的c + 'a' - 'A'這種大小寫轉換等類似形式在ASCII中是適用的,但在EBCDIC編碼中是不適用的。(K&R)
細節15:移位運算
<<和>>的兩個操作數都是整數,並且右操作數應該是(must be)非負的。(K&R)
事實上,C99表示,如果右操作數為負,或者移位的位數大於數據的位數,是未定義行為。更詳細的規定:
對於E1<<E2,如果E1是無符號型,那么結果是E1 * 2E2,當超過該類型最大值時取模;如果E1是有符號型且非負,並且E1 * 2E2可以在該類型中表示,那么它就是結果,其它情況下則是未定義行為。
對於E1>>E2,如果E1是無符號型或者E1有符號且非負,那么結果是E1除以2的E2次冪的整數除法結果;如果E1有符號且為負值,結果值是實現定義的。
細節16:取反的好處——更獨立於字長
分析:
為取得x的最低六位,與x &~077相比,x &0177700假定x是16位的,可移植性顯然不如前者。
細節17:賦值表達式相當於自帶括號,即 x *= y+1相當於x = x*(y+1),而非x = x *y +1。賦值語句的值是左分量的值。(K&R)
細節18:三目表達式expr1 ?expr2 :expr3 的求值順序和表達式的值與類型
分析:(K&R)
先計算,expr1 ,非0時計算expr2 ,並作為表達式的值;為0時計算expr3並作為表達式的值。
表達式的值的類型由expr2和expr3二者的類型共同決定,其轉換規則與一般的不同類型值進行運算的轉換規則一致。
細節19:求值順序與副作用
分析:
C並沒有指定一個運算符兩邊運算數的計算順序(&& , || , ?:以及','除外),即類似於x = f()+g()的表達式中,f()和g()的計算順序未知先后。(K&R)另外,這里的','不是函數參數聲明中的',',前者由左向右計算,后者不保證運算順序。(K&R)
同樣地,參數的計算順序也是未知的,比如printf("%d %d\n",++n,power(2,n));它的具體結果和編譯器有關。(K&R)
對於第二條,如果你以關鍵詞“printf” "參數壓棧"進行搜索,會發現廣為流傳的說法“printf參數壓棧從右向左”。
副作用(side effect)——作為表達式的副產品,改變了變量的值。a[i]=i++,數組的下標是新值還是舊值,不同的編譯器有不同的解釋。標准明確規定了所有變元的副作用必須在該函數調用前生效,但對於上文printf的解釋沒有什么好處。(K&R)
不過我還是在自己的平台上測試了一下:
int i,j,k; i=j=6; k=2; printf("%d %d %d\n",i++,--j,k+=j);
輸出:6 5 8
細節20:switch () ... case ...語句中,switch后必須是整數表達式,case 后必須是整型常量或者常量表達式。(C99)
細節21:無參數的函數,其聲明的參數表請用(void),有參數就說明它們。直接用func()進行聲明只是為了與較老的程序兼容,這會導致函數參數檢查被關閉,最好不要這么做。(K&R)
分析:
以下代碼運行無誤(CARM):
int f() { printf("in f()\n"); return 0; } main(){ f(1,2); return 0; }
補充:
C++中int f()聲明等價於int f(void)。
細節22:對於一個return值類型為double的函數func(),使用int a = (int) func()可以屏蔽warning。(K&R)
補充:
下面這兩種編程實踐哪個更好?
int*sieve = malloc(sizeof(int)*length); //case 1 int *sieve = (int *)malloc(sizeof(int)*length);//case 2
比較信服的答案是,第一種更好:http://stackoverflow.com/questions/605845/do-i-cast-the-result-of-malloc
-
- void會自動轉換為所需類型;
- 如果忘記包含<stdlib.h>這會隱藏一個導致崩潰的bug;
- 如果指針類型比較復雜而不僅僅是int*,會導致該行過長,降低了可讀性;
前后進行了重復,一般情況下是不好的。
關於第一條,K&R提到,Any pointer can be cast to void* and back again without loss of information。
關於第四條,K&R還有一例可證:yyval[yypv[p3+p4] + yypv[p1+p2]] += 2要強於yyval[yypv[p3+p4] + yypv[p1+p2]] = yyval[yypv[p3+p4] + yypv[p1+p2]] + 2。雖然你在第一次編碼時可以用復制粘貼的方式保證前后一致,但如果其中有錯誤,或者要進行修改,那么你要付出兩倍的工作量。
細節23:C99支持變長數組,即運行時才決定大小的數組。
scanf("%d",&n); int array2[n];
更多細節:
(CARM)
使用typedef定義變長數組時,只求值一次。
/*假定此時n=5*/ typedef int[n] vector; n+=1; vector a; //a的容量是5 int b[n]; //b的容量是6
變長數組可以作為函數參數類型,但其長度參數必須先於數組名出現。
void f(int r,int c, int a[r][c]) //OK void f(int a[r][c],int r,int c) //WRONG
細節24:static聲明將變量或函數的作用域限制為它們出現的文件的其余部分。(K&R)
分析:
不要與C++中的static搞混,后者除了這種功能,還用於修飾靜態成員變量/函數。(我的這個敘述存疑)
細節25:register只用於修飾自動變量和函數形參。(K&R)同時,register是函數參數中唯一可以出現的存儲類指定符。
細節26:未顯式初始化時,外部變量和靜態變量都被初始化為0,而自動變量與寄存器變量中的值未定義,即“垃圾”。前兩者必須用常量表達式初始化。(K&R)
細節27:數組初始化時,如果初始化符比數組容量小,未指定的元素在作為外部變量、靜態變量、自動變量時被初始化為0。(K&R)
細節28:取地址運算符&只能用於內存中的對象(變量和數組元素),不能對表達式、常量或寄存器變量進行操作。(K&R)
細節29:標准要求main函數參數表中argv[argc]為null指針。(K&R、C99)
細節30:struct point *pp,可以用(*pp).x訪問它的成員。(不僅限於pp->x) (K&R)
細節31:sizeof()不能用於#if,但可以用於#define。(K&R)
補充:
(CARM)
細節32:聯合union的大小要足以容納其最大的成員,但具體的大小是取決於實現的。聯合只能用第一個成員類型初始化。(K&R)聯合的尾部可能需要進行填充。(CARM)
細節33:字段(bit-fields)幾乎所有屬性都取決於實現;字段不是數組,也沒有地址,不能使用&運算符。(K&R)
測試:
對字段使用&編譯器直接報Error。
細節34:scanf使用%c讀取下一個字符(缺省為1)存入指定位置。通常不跳過空白符(空格、制表符、換行符)。為讀入下一個非空白符,使用%1s。
細節35:不確定輸入格式時的一個小技巧(K&R)
while (getline(line, sizeof(line)) > 0) { if (sscanf(line, "%d %s %d", &day, monthname, &year) == 3) printf("valid: %s\n", line); /* 25 Dec 1988 form */ else if (sscanf(line, "%d/%d/%d", &month, &day, &year) == 3) printf("valid: %s\n", line); /* mm/dd/yy form */ else printf("invalid: %s\n", line); /* invalid form */ }
原理:
scanf函數使用完了格式輸入串或當一些輸入無法與控制說明相匹配時,就停止運行,並返回成功匹配和賦值的輸入項的個數。
以下部分來自於我讀CARM時的筆記,重要性個人認為不如前35條。
細節36:如果不發生溢出,整型常量的值總是非負數;如果前面出現符號則是對常量的一元運算符,不是常量的一部分;浮點型常量同理。
細節37:多字符常量,含義由實現定義。
細節38:標准C允許對包含相同字符的兩個字符串型常量使用同一存儲空間。如果在只讀內存中分配,則下面賦值會產生錯誤。
測試:
#include <stdio.h> char *string1,*string2; int main() { string1 = "abcd"; string2 = "abcd"; if (string1==string2) printf("Strings are shared.\n"); else printf("Strings are not shared.\n"); string1[0] = '1'; if(*string1=='1') printf("Strings writable\n"); else printf("Strings are not writable\n"); return 0; }
輸出:
Strings are shared.
段錯誤
另外可以看出,字符串常量返回的是地址:char *string1 = "abcd".
細節39:單字符常量在C中是int型,而C++是char型。
測試:
#include <stdio.h> main() { printf("sizeof('a'):%d\n",sizeof('a')); }
結果:
//.c結尾,gcc編譯
sizeof('a'):4
//.cpp結尾,g++編譯
sizeof('a'):1
細節40:struct的指定初始化(C99新增)
分析:
struct S {int a;float b;char c[4]}; struct S s1 = {.c="abc"}; /* {0,0.0,"abc"}*/
細節41:標准C中,可以用void *作為通用對象指針,但沒有通用函數指針。
分析:后者的區別在於,下面被注釋掉的代碼無法通過編譯,而剩余部分無誤。
#include <stdio.h> int f1(int a) { return 1; } int f2(double b) { return 2; } int main() { //void *p; //p = f1; //printf("%d\n",p(0)); //p = f2; //printf("%d\n",p(0)); int (*p1)(int); int (*p2)(double); p1 = f1; printf("%d\n",p1(0)); p2 = f2; printf("%d\n",p2(0)); return 0; }
細節42:結構不能比較相等性。如果需要,請逐個成員比較。
細節43:typedef名稱不能與其他類型說明符一起使用
typedef long int bigint; unsigned bigint x; /*invalid*/
但是可以與類型限制符一起使用
const bigint x; /*OK*/
細節44:結構類型定義或聯合類型定義中類型說明符的每一次出現都引入一個新的結構類型或聯合類型。
分析:以下x、y、u的類型各不同,但u和v類型相同。
struct {int a;int b;} x; struct {int a;int b;} y; struct S {int a;int b;} u; struct S v;
細節45:如果結構和聯合表達式是左值,則直接成員選擇表達式的結果為左值(只有函數返回的結構和聯合值才不是左值)。
關於左值,請見第二部分。
細節46:如何避免放棄值的警告?
下列是雖然有效但可能引起警告消息的語句:
extern int g(); g(x); //the result of g is discarded x+7; //Addition has no defined side effects x + (a*=2);// "+"is discarded
為避免放棄值的警告,可以將其轉化為void類型以表示故意要放棄這個值:
extern int g(); (void)g(x); //the result of g is discarded (void)(x+7); //Addition has no defined side effects
細節47:C99不再允許main省略返回值類型。
測試:gcc4.4.3使用-std=c99,提示warning: return type defaults to ‘int’
細節48:求值的順序與尋常雙目轉換,以下兩個表達式並不等價
(1.0+ -3) +(unsigned)1;//Result is -1.0 1.0 +(-3 + (unsigned)1);//Result is large
分析:
求值時會進行尋常雙目轉換,規則如下
細節49:當源和目的地址有公共存儲空間時
以下函數的行為是未定義的
strcat,strncat,wcscat,wcsncat
strcpy,strncpy,wcscpy,wcsncpy
memcpy,memccpy
以下函數可以正常工作
memmove,wmmove
memmove“像”是借助了一塊臨時存儲區,實際上它的實現不需要。
細節50:兩字符串相等時,strcmp()返回0。因此if(!strcmp(s1,s2))表示兩字符串相等時的條件。
細節51:逗號表達式的值是它的右操作數的值,即r = (a,b,...,c);等價於a;b;...r=c;
細節52:如果一個頂層聲明具有類型限制符const,但沒有顯式的存儲類別,在C中被認為是extern,C++則認為是static。
二、容易被忽視的定義
1.文本流(text stream)
一系列被分割成幾行的字符序列。每行有0個或多個字符,以換行符(newline)結束。 (K&R、C99同,后者原文:A text stream is an ordered sequence of characters composed into lines , each line consisting of zero or more characters plus a terminating new-line character.)
2.對象和左值
(來自CARM)
對象(object)是一塊內存區域,可以讀取它的值或者向它存儲數據。左值(lvalue)是一種表達式,可以讀取或修改它所引用的對象。只有左值表達式可以作為賦值操作符的左操作數,不屬於左值的表達式有時稱為右值(rvalue),因為它只能出現在賦值操作符的右邊。左值可以是對象或不完整類型,但不能是void類型。
說明:
下面的語句是沒有任何問題的,盡管以前從未想過。
int a; (a) = 1;
3.序列點
這里直接是C99的相關解釋
Accessing a volatile object, modifying an object, modifying a file, or calling a function
that does any of those operations are all side effects ,which are changes in the state of
the execution environment. Evaluation of an expression may produce side effects. At
certain specified points in the execution sequence called sequence points , all side effects
of previous evaluations shall be complete and no side effects of subsequent evaluations
shall have taken place.
以及所有的序列點總結(C99附錄C)
The following are the sequence points described in 5.1.2.3:
— The call to a function, after the arguments have been evaluated (6.5.2.2).
— The end of the first operand of the following operators: logicalAND&& (6.5.13);
logical OR||(6.5.14); conditional ? (6.5.15); comma , (6.5.17).
— The end of a full declarator: declarators (6.7.5);
— The end of a full expression: an initializer (6.7.8); the expression in an expression
statement (6.8.3); the controlling expression of a selection statement (ifor switch)
(6.8.4); the controlling expression of a whileor dostatement (6.8.5); each of the
expressions of a for statement (6.8.5.3); the expression in a return statement
(6.8.6.4).
— Immediately before a library function returns (7.1.4).
— After the actions associated with each formatted input/output function conversion
specifier (7.19.6, 7.24.2).
— Immediately before and immediately after each call to a comparison function, and
also between any call to a comparison function and any movement of the objects
passed as arguments to that call (7.20.5).
三、補充
1.再談未定義行為
本來是想搞一個未定義行為總收集的,但無奈實在太多,時間有限,只能作罷。有興趣尋根問底的可以去查閱C99或最新的C11標准的附錄J.2。下面收集了一些探討常見未定義行為的文章鏈接,有興趣可以去研究下:
在表達式求值時,如果發生了什么意外情況,比如1/0,這在數學上就沒有解釋,或者求值結果不在對應類型所能表示范圍內( 1 + INT_MAX就是這種情況,兩個int類型數據相加應該得到一個int類型的值,但現在這個值卻超出了int類型的表示范圍),那么這個表達式究竟是什么意思,C語言說它不知道。
...
這句話的意思是說,在相鄰兩個序點(sequence point)之間,同一個數據對象的值最多可以通過表達式求值改變一次。
http://www.cnblogs.com/pmer/archive/2013/01/02/2842516.html
再比如,兩個int類型數據相加,其前提條件是結果必須在int類型可以表示的范圍之內,否則就成了一種未定義行為。
http://www.cnblogs.com/pmer/archive/2012/01/16/2324058.html
指針可以與整數做加、減運算是有前提的。前提之一是這個指針必須是指向數據對象(Object)。例如:
int i
&i這個指針可以+0、+1。但是指向函數的指針或指向void類型的指針沒有加減法運算。
前提之二是這個指針必須指向數組元素(單個Object視同一個元素的數組)或指向數組最后一個元素之后的那個位置。例如:
int a[2]
&a[0]、&a[1]、&a[1]+1(即a、a+1、a+2)這些指針可以進行加減法運算。
第三,指針進行加減法運算的結果必須也指向數組元素或指向數組最后一個元素之后的那個位置。例如,對於指向a[0]的指針a,只能+0、+1、+2,對於a+2這個指針,只能-0、-1、-2。如果運算結果不是指向數組元素或指向數組元素最后一個元素之后的位置的情況,C語言並沒有規定這種運算行為的結果是什么,換句話說這是一種未定義行為(Undefined Behavior,后面簡稱UB)。
http://www.cnblogs.com/pmer/archive/2012/05/18/2507971.html
2.C99標准新增了哪些重要特性?
習慣於使用只支持老標准的編譯器的讀者不妨看看,這些新特性有的還是挺方便的。更不用說C11已經發布很長時間了。
3.關於二維數組
這個比較容易讓人迷惑,舊作一篇供參考:二維數組作為函數參數傳遞剖析(C語言)(6.19更新第5種)
四、后記
寫了幾年程序,接觸了一些語言;回顧下,還是C用得最多,也最熟悉。臨近找工作,回顧下之前系統看過幾遍的K&R以及CARM,希望能及時掃除盲點,也希望本文能對C語言的使用者有所幫助。書中還有很多優秀代碼、細致的說明和程序設計思想,不過限於篇幅,以及與主題關系不大,只好割愛,建議有空一定要好好讀讀。