天天看點

程式設計實踐中C語言的一些常見細節一、程式設計細節:二、容易被忽視的定義 三、補充四、後記

對于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

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

結果:

  編譯器Error。

細節2:printf使用了格式化控制符%d但沒有對應參數

來源:某公司面試題

  編譯器warning: too few arguments for format

  運作時顯示一個随機值。

對照:

  使用clang,提示 1 diagnostic generated.

  運作時總是顯示0。

分析:

  K&R提到,如果參數不夠,會FAIL。C99則把這認定為未定義行為(可參見C99标準中的fprintf部分,它的行為與printf類似)。

相關:

  %s對于" "和""的處理示範。我不确定是否實作相關或者未定義行為。

  輸出:

 | |

細節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的是否有符号。

  或者,你也可以使用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)

  另外,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)

  不過我還是在自己的平台上測試了一下:

  輸出:6 5 8

細節20:switch () ... case ...語句中,switch後必須是整數表達式,case 後必須是整型常量或者常量表達式。(C99)

細節21:無參數的函數,其聲明的參數表請用(void),有參數就說明它們。直接用func()進行聲明隻是為了與較老的程式相容,這會導緻函數參數檢查被關閉,最好不要這麼做。(K&R)

  以下代碼運作無誤(CARM):

補充:

  C++中int f()聲明等價于int f(void)。

細節22:對于一個return值類型為double的函數func(),使用int a = (int) func()可以屏蔽warning。(K&R)

  下面這兩種程式設計實踐哪個更好?

void會自動轉換為所需類型;

如果忘記包含<stdlib.h>這會隐藏一個導緻崩潰的bug;

如果指針類型比較複雜而不僅僅是int*,會導緻該行過長,降低了可讀性;  

<code>前後進行了重複,一般情況下是不好的。</code>

  關于第一條,K&amp;R提到,Any pointer can be cast to void* and back again without loss of information。

  關于第四條,K&amp;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支援變長數組,即運作時才決定大小的數組。

更多細節:

  (CARM)

  使用typedef定義變長數組時,隻求值一次。 

  變長數組可以作為函數參數類型,但其長度參數必須先于數組名出現。

細節24:static聲明将變量或函數的作用域限制為它們出現的檔案的其餘部分。(K&amp;R)

  不要與C++中的static搞混,後者除了這種功能,還用于修飾靜态成員變量/函數。(我的這個叙述存疑)

細節25:register隻用于修飾自動變量和函數形參。(K&amp;R)同時,register是函數參數中唯一可以出現的存儲類指定符。

細節26:未顯式初始化時,外部變量和靜态變量都被初始化為0,而自動變量與寄存器變量中的值未定義,即“垃圾”。前兩者必須用常量表達式初始化。(K&amp;R)

細節27:數組初始化時,如果初始化符比數組容量小,未指定的元素在作為外部變量、靜态變量、自動變量時被初始化為0。(K&amp;R)

細節28:取位址運算符&amp;隻能用于記憶體中的對象(變量和數組元素),不能對表達式、常量或寄存器變量進行操作。(K&amp;R)

細節29:标準要求main函數參數表中argv[argc]為null指針。(K&amp;R、C99)

細節30:struct point *pp,可以用(*pp).x通路它的成員。(不僅限于pp-&gt;x) (K&amp;R)

細節31:sizeof()不能用于#if,但可以用于#define。(K&amp;R)

程式設計實踐中C語言的一些常見細節一、程式設計細節:二、容易被忽視的定義 三、補充四、後記
程式設計實踐中C語言的一些常見細節一、程式設計細節:二、容易被忽視的定義 三、補充四、後記

細節32:聯合union的大小要足以容納其最大的成員,但具體的大小是取決于實作的。聯合隻能用第一個成員類型初始化。(K&amp;R)聯合的尾部可能需要進行填充。(CARM)

細節33:字段(bit-fields)幾乎所有屬性都取決于實作;字段不是數組,也沒有位址,不能使用&amp;運算符。(K&amp;R)

測試:

  對字段使用&amp;編譯器直接報Error。

細節34:scanf使用%c讀取下一個字元(預設為1)存入指定位置。通常不跳過空白符(空格、制表符、換行符)。為讀入下一個非空白符,使用%1s。

細節35:不确定輸入格式時的一個小技巧(K&amp;R)

原理:

  scanf函數使用完了格式輸入串或當一些輸入無法與控制說明相比對時,就停止運作,并傳回成功比對和指派的輸入項的個數。

以下部分來自于我讀CARM時的筆記,重要性個人認為不如前35條。

細節36:如果不發生溢出,整型常量的值總是非負數;如果前面出現符号則是對常量的一進制運算符,不是常量的一部分;浮點型常量同理。

細節37:多字元常量,含義由實作定義。

細節38:标準C允許對包含相同字元的兩個字元串型常量使用同一存儲空間。如果在隻讀記憶體中配置設定,則下面指派會産生錯誤。

輸出:

  另外可以看出,字元串常量傳回的是位址:char *string1 = "abcd".

細節39:單字元常量在C中是int型,而C++是char型。

//.c結尾,gcc編譯 sizeof('a'):4 //.cpp結尾,g++編譯 sizeof('a'):1

細節40:struct的指定初始化(C99新增)

細節41:标準C中,可以用void *作為通用對象指針,但沒有通用函數指針。

分析:後者的差別在于,下面被注釋掉的代碼無法通過編譯,而剩餘部分無誤。

細節42:結構不能比較相等性。如果需要,請逐個成員比較。

細節43:typedef名稱不能與其他類型說明符一起使用

但是可以與類型限制符一起使用

細節44:結構類型定義或聯合類型定義中類型說明符的每一次出現都引入一個新的結構類型或聯合類型。

分析:以下x、y、u的類型各不同,但u和v類型相同。

細節45:如果結構和聯合表達式是左值,則直接成員選擇表達式的結果為左值(隻有函數傳回的結構和聯合值才不是左值)。 

關于左值,請見第二部分。

細節46:如何避免放棄值的警告?

下列是雖然有效但可能引起警告消息的語句:

為避免放棄值的警告,可以将其轉化為void類型以表示故意要放棄這個值:

細節47:C99不再允許main省略傳回值類型。

測試:gcc4.4.3使用-std=c99,提示warning: return type defaults to ‘int’

 細節48:求值的順序與尋常雙目轉換,以下兩個表達式并不等價

  求值時會進行尋常雙目轉換,規則如下

程式設計實踐中C語言的一些常見細節一、程式設計細節:二、容易被忽視的定義 三、補充四、後記
程式設計實踐中C語言的一些常見細節一、程式設計細節:二、容易被忽視的定義 三、補充四、後記

細節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&amp;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類型。

程式設計實踐中C語言的一些常見細節一、程式設計細節:二、容易被忽視的定義 三、補充四、後記
程式設計實踐中C語言的一些常見細節一、程式設計細節:二、容易被忽視的定義 三、補充四、後記

 說明:

  下面的語句是沒有任何問題的,盡管以前從未想過。

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&amp;&amp; (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)之間,同一個資料對象的值最多可以通過表達式求值改變一次。
再比如,兩個int類型資料相加,其前提條件是結果必須在int類型可以表示的範圍之内,否則就成了一種未定義行為。 
指針可以與整數做加、減運算是有前提的。前提之一是這個指針必須是指向資料對象(Object)。例如: int i    &amp;i這個指針可以+0、+1。但是指向函數的指針或指向void類型的指針沒有加減法運算。   前提之二是這個指針必須指向數組元素(單個Object視同一個元素的數組)或指向數組最後一個元素之後的那個位置。例如: int a[2]    &amp;a[0]、&amp;a[1]、&amp;a[1]+1(即a、a+1、a+2)這些指針可以進行加減法運算。 第三,指針進行加減法運算的結果必須也指向數組元素或指向數組最後一個元素之後的那個位置。例如,對于指向a[0]的指針a,隻能+0、+1、+2,對于a+2這個指針,隻能-0、-1、-2。如果運算結果不是指向數組元素或指向數組元素最後一個元素之後的位置的情況,C語言并沒有規定這種運算行為的結果是什麼,換句話說這是一種未定義行為(Undefined Behavior,後面簡稱UB)。

2.C99标準新增了哪些重要特性?

  習慣于使用隻支援老标準的編譯器的讀者不妨看看,這些新特性有的還是挺友善的。更不用說C11已經釋出很長時間了。

<a href="http://blog.163.com/zhaojie_ding/blog/static/17297289520115210564890?suggestedreading" target="_blank">C99的新特性(2)</a>

3.關于二維數組

  寫了幾年程式,接觸了一些語言;回顧下,還是C用得最多,也最熟悉。臨近找工作,回顧下之前系統看過幾遍的K&amp;R以及CARM,希望能及時掃除盲點,也希望本文能對C語言的使用者有所幫助。書中還有很多優秀代碼、細緻的說明和程式設計思想,不過限于篇幅,以及與主題關系不大,隻好割愛,建議有空一定要好好讀讀。

本文轉自五嶽部落格園部落格,原文連結:www.cnblogs.com/wuyuegb2312/p/3302561.html,如需轉載請自行聯系原作者

繼續閱讀