[分享] ptt C 語言新手十誡 - 一個小小工程師的心情抒發天地

文章推薦指數: 80 %
投票人數:10人

C 語言新手十誡(The Ten Commandments for Newbie C Programmers) 本篇旨在提醒新手, ... 不能在函式中回傳一個指向區域性自動變數的指標; 6. 關閉廣告 一個小小工程師的心情抒發天地 跳到主文 Writethecode,changetheworld 部落格全站分類:心情日記 相簿 部落格 留言 名片 Dec07Thu201714:53 [分享]pttC語言新手十誡 C語言新手十誡(TheTenCommandmentsforNewbieCProgrammers)   本篇旨在提醒新手,避免初學常犯的錯誤(其實老手也常犯:-Q)。

但不能取代完整的學習,請自己好好研讀一兩本C語言的好書,並多多實作練習。

強烈建議新手先看過此文再發問,你的問題極可能此文已經提出並解答了。

以下所舉的錯誤例子如果在你的電腦上印出和正確例子相同的結果,那只是不足為恃的一時僥倖。

不守十誡者,輕則執行結果的輸出數據錯誤,或是程式當掉,重則引爆核彈、毀滅地球(如果你的C程式是用來控制核彈發射器的話)。

1.不可以使用尚未給予適當初值的變數 2.不能存取超過陣列既定範圍的空間 3.不可以提取不知指向何方的指標 4.不要試圖用char*去更改一個"字串常數" 5.不能在函式中回傳一個指向區域性自動變數的指標 6.不可以只做malloc(),而不做相應的free() 7.在數值運算、賦值或比較中不可以隨意混用不同型別的數值 8.在一個運算式中,不能對一個基本型態的變數修改其值超過一次以上 9.在Macro定義中,務必為它的參數個別加上括號 A(10).不可以在stack設置過大的變數 B(11).使用浮點數精確度造成的誤差問題 C(12).不要猜想二維陣列可以用pointertopointer來傳遞 D(13).函式內new出來的空間記得要讓主程式的指標接住 01.你不可以使用尚未給予適當初值的變數 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 錯誤例子: intaccumulate(intmax)/*從1累加到max,傳回結果*/ { intsum;/*未給予初值的區域變數,其內容值是垃圾*/ intnum; for(num=1;num<=max;num++){sum+=num;} returnsum; } 正確例子: intaccumulate(intmax) { intsum=0;/*正確的賦予適當的初值*/ intnum; for(num=1;num<=max;num++){sum+=num;} returnsum; } 02.你不可以存取超過陣列既定範圍的空間 1 2 3 4 5 6 7 8 9 錯誤例子: intstr[5]; inti; for(i=0;i<=5;i++)str[i]=i; 正確例子: intstr[5]; inti; for(i=0;i<5;i++)str[i]=i; 說明:宣告陣列時,所給的陣列元素個數值如果是N,那麼我們在後面 透過[索引值]存取其元素時,所能使用的索引值範圍是從0到N-1 C/C++為了執行效率,並不會自動檢查陣列索引值是否超過陣列邊界,我們要自己來確保不會越界。

一旦越界,操作的不再是合法的空間,將導致無法預期的後果。

03.你不可以提取(dereference)不知指向何方的指標(包含null指標)。

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 錯誤例子: char*pc1;/*未給予初值,不知指向何方*/ char*pc2=0;/*pc2起始化為nullpointer*/ *pc1='a';/*將'a'寫到不知何方,錯誤*/ *pc2='b';/*將'b'寫到「位址0」,錯誤*/ 正確例子: charc;/*c的內容尚未起始化*/ char*pc1=&c;/*pc1指向字元變數c*/ *pc1='a';/*c的內容變為'a'*/ /*動態分配10個char(其值未定),並將第一個char的位址賦值給pc2*/ char*pc2=(char*)malloc(10); pc2[0]='b';/*動態配置來的第0個字元,內容變為'b' free(pc2); 說明:指標變數必需先指向某個可以合法操作的空間,才能進行操作。

(使用者記得要檢查malloc回傳是否為NULL,礙於篇幅本文假定使用上皆合法,也有正確歸還記憶體) 1 2 3 4 5 6 7 8 9 10 11 12 13 錯誤例子: char*name;/*name尚未指向有效的空間*/ printf("Yourname,please:"); gets(name);/*您確定要寫入的那塊空間合法嗎???*/ printf("Hello,%s\n",name); 正確例子: /*如果編譯期就能決定字串的最大空間,那就不要宣告成char*改用char[]*/ charname[21];/*可讀入字串最長20個字元,保留一格空間放'\0'*/ printf("Yourname,please:"); gets(name); printf("Hello,%s\n",name); 正確例子(2): 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 /*若是在執行時期才能決定字串的最大空間,則需利用malloc()函式來動態分配空間*/ size_tlength; char*name; printf("請輸入字串的最大長度(含null字元):"); scanf("%u",&length); name=(char*)malloc(length); printf("Yourname,please:"); scanf("%s",name); printf("Hello,%s\n",name); /*最後記得free()掉malloc()所分配的空間*/ free(name); 04.你不可以試圖用char*去更改一個"字串常數"   1 2 3 4 5 6 7 8 9 10 11 12 錯誤例子: char*pc="john";/*pc現在指著一個字串常數*/ *pc='J';/*但是pc沒有權利去更改這個常數!*/ 正確例子: charpc[]="john";/*pc現在是個合法的陣列,裡面住著字串john*/ /*也就是pc[0]='j',pc[1]='o',pc[2]='h',pc[3]='n',pc[4]='\0'*/ *pc='J'; pc[2]='H'; 說明:字串常數的內容是"唯讀"的。

您有使用權,但是沒有更改的權利。

若您希望使用可以更改的字串,那您應該將其放在合法空間 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 錯誤例子: char*s1="Hello,"; char*s2="world!"; /*strcat()不會另行配置空間,只會將資料附加到s1所指唯讀字串的後面,造成寫入到程式無權碰觸的記憶體空間*/ strcat(s1,s2); 正確例子(2): /*s1宣告成陣列,並保留足夠空間存放後續要附加的內容*/ chars1[20]="Hello,"; char*s2="world!"; /*因為strcat()的返回值等於第一個參數值,所以s3就不需要了*/ strcat(s1,s2); 05.你不可以在函式中回傳一個指向區域性自動變數的指標。

否則,會得到垃圾值 [感謝gocpp網友提供程式例子] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 錯誤例子: char*getstr(char*name) { charbuf[30]="hello,";/*將字串常數"hello,"的內容複製到buf陣列*/ strcat(buf,name); returnbuf; } 說明:區域性自動變數,將會在離開該區域時(本例中就是從getstr函式返回時) 被消滅,因此呼叫端得到的指標所指的字串內容就失效了。

正確例子: voidgetstr(charbuf[],intbuflen,charconst*name) { charconsts[]="hello,"; strcpy(buf,s); strcat(buf,name); }   1 2 3 4 5 6 7 8 9 10 11 正確例子: int*foo() { int*pInteger=(int*)malloc(10*sizeof(int)); returnpInteger; } intmain() { int*pFromfoo=foo(); } 說明:上例雖然回傳了函式中的指標,但由於指標內容所指的位址並非區域變數, 而是用動態的方式抓取而得,換句話說這塊空間是長在heap而非stack, 又因 heap空間並不會自動回收,因此這塊空間在離開函式後,依然有效 (但是這個例子可能會因為programmer的疏忽,忘記free而造成memoryleak) [針對字串操作,C++提供了更方便安全更直觀的stringclass,能用就盡量用]     1 2 3 4 5 6 7 8 9 正確例子: #include/*並非#include*/ usingstd::string; stringgetstr(stringconst&name) { returnstring("hello,")+=name; } 06.你不可以只做malloc(),而不做相應的free().否則會造成記憶體漏失 但若不是用malloc()所得到的記憶體,則不可以free()。

已經free()了所指記憶體的指標,在它指向另一塊有效的動態分配得來的空間之前,不可以再被free(),也不可以提取(dereference)這個指標。

[C++]你不可以只做new,而不做相應的delete 注:new與delete對應,new[]與delete[]對應,不可混用 (切記,做了幾次new,就必須做幾次delete) 小技巧:可在delete之後將指標指到0,由於delete本身會先做檢查,因此可以避免掉多次delete的錯誤 1 2 3 4 5 正確例子: int*ptr=newint(99); deleteptr; ptr=NULL; deleteptr;/*delete只會處理指向非NULL的指標*/ 07.你不可以在數值運算、賦值或比較中隨意混用不同型別的數值,而不謹慎考 慮數值型別轉換可能帶來的「意外驚喜」(錯愕)。

必須隨時注意數值運算的結果,其範圍是否會超出變數的型別 1 2 3 4 5 6 7 8 9 10 11 12 13 14 錯誤例子: unsignedintsum=2000000000+2000000000;/*超出int存放範圍*/ unsignedintsum=(unsignedint)(2000000000+2000000000); doublef=10/3; 正確例子: /*全部都用unsignedint,注意數字後面的u,大寫U也成*/ unsignedintsum=2000000000u+2000000000u; /*或是用顯式的轉型*/ unsignedintsum=(unsignedint)2000000000+2000000000; doublef=10.0/3.0;     1 2 3 4 錯誤例子: unsignedinta=0; intb[10]; for(inti=9;i>=a;i--){b[i]=0;} 說明:由於int與unsigned共同運算的時候,會提升int為unsigned,因此迴圈條件永遠滿足,與預期行為不符 1 2 3 4 5 6 錯誤例子:(感謝sekya網友提供) unsignedchara=0x80;/*noproblem*/ charb=0x80;/*implementation-definedresult*/ if(b==0x80){/*不一定恒真*/ printf("bok\n"); } 說明:語言並未規定char天生為unsigned或signed,因此將0x80放入char型態的變數,將會視各家編譯器不同作法而有不同結果 08.你不可以在一個運算式(expression)中,對一個基本型態的變數修改其值超過一次以上。

否則,將導致未定義的行為(undefinedbehavior) 1 2 3 4 5 6 7 8 錯誤例子: inti=7; intj=++i+i++; 正確例子: inti=7; intj=++i; j+=i++; 你也不可以在一個運算式(expression)中,對一個基本型態的變數修改其值,而且還在同一個式子的其他地方為了其他目的而存取該變數的值。

(其他目的,是指不是為了計算這個變數的新值的目的)。

否則,將導致未定義的行為。

1 2 錯誤例子: x=x++;   1 2 3 4 5 6 7 8 9 10 錯誤例子: intarr[5]; inti=0; arr[i]=i++; 正確例子: intarr[5]; inti=0; arr[i]=i; i++;   1 2 3 4 5 6 7 8 錯誤例子: inti=10; cout< #defineSQUARE(x)(x*x) intmain() { printf("%d\n",SQUARE(10-5)); return0; } 正確例子: #include #defineSQUARE(x)((x)*(x)) intmain() { printf("%d\n",SQUARE(10-5)); return0; } 說明:如果是用C++,請多多利用inlinefunction來取代上述的macro,以免除macro定義的種種危險性。

如: inlineintsquare(intx){returnx*x;} macro定義出的「偽函式」至少缺乏下列數項函式本有的能力: (1)無法進行參數型別的檢查。

(2)無法遞迴呼叫。

(3)無法用&加在macroname之前,取得函式位址。

(4)呼叫時往往不能使用具有sideeffect的引數。

例如: 1 2 3 4 5 6 7 8 錯誤例子:(感謝yaca網友提供) #defineMACRO(x)(((x)*(x))-((x)*(x))) intmain() { intx=3; printf("%d\n",MACRO(++x)); return0; } 備註:副作用(sideeffect) 表达式有两种功能:每个表达式都产生一个值(value),同时可能包含副作用(sideeffect)。

副作用是指改变了某些变量的值。

1:20//这个表达式的值是20;它没有副作用,因为它没有改变任何变量的值。

2:x=5//这个表达式的值是5;它有一个副作用,因为它改变了变量x的值。

3:x=y++//这个表达示有两个副作用,因为改变了两个变量的值。

4:x=x++//这个表达式也有两个副作用,因为变量x的值发生了两次改变。

10.不可在stack設置過大的變數,否則會造成stackoverflow (感謝VictorTom版友幫忙) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 錯誤例子: intarray[10000000];//僅舉例說明 /*說明:由於編譯器會自行決定stack的上限,某些預設是數KB或數十KB 當變數所需的空間過大時,很容易造成stackoverflow,程式亦隨之 當掉,若真正需要如此大的空間,那麼建議配置在heap上,或是採用 static/globlavariable,亦或是改變編譯器的設定 使用heap時,雖然整個process可用的空間是有限的,但採用動態抓取 的方式,new無法配置時會丟出std::bad_alloc例外,malloc無法配置 時會回傳null,不會影響到正常使用下的程式功能*/ 正確例子: int*array=(int*)malloc(10000000*sizeof(int)); 說明:由於此時stack上只需配置一個int*的空間,可避免stackoverflow 更多說明請參考精華區z-10-13 11.使用浮點數千萬要注意精確度所造成的誤差問題   根據IEEE754的規範,又電腦中是用有限的二進位儲存數字,因此常有可 能因為精確度而造成誤差,例如加減乘除,等號大小判斷,分配律等數學上 常用到的操作,很有可能因此而出錯(不成立)   更詳細的說明可以參考精華區z-8-11 或參考冼鏡光老師所發表的一文"使用浮點數最最基本的觀念" http://blog.dcview.com/article.php?a=VmgBZFE5AzI%3D 12.不要猜想二維陣列可以用pointertopointer來傳遞 (感謝loveme00835legnaleurc版友的幫忙)   首先必須有個觀念,C語言中陣列是無法直接拿來傳遞的! 不過這時候會有人跳出來反駁: 1 2 3 4 voidpass1DArray(intarray[]); inta[10]; pass1DArray(a);/*可以合法編譯,而且執行結果正確!!*/ 事實上,編譯器會這麼看待 1 2 3 4 voidpass1DArray(int*array); inta[10]; pass1DArray(&a[0]); 我們可以順便看出來,array變數本身可以decay成記憶體起頭的位置 因此我們可以int*p=a;這種方式,拿指標去接陣列。

也因為上述的例子,許多人以為那二維陣列是不是也可以改成int** 1 2 3 4 5 6 7 錯誤例子: voidpass2DArray(int**array); inta[5][10]; pass2DArray(a); /*這時候編譯器就會報錯啦*/ /*expected‘int**’butargumentisoftype‘int(*)[10]’*/   在一維陣列中,指標的移動操作,會剛好覆蓋到陣列的範圍,例如,宣告了一個a[10],那我可以把a當成指標來操作*a至*(a+9) 因此我們可以得到一個概念,在操作的時候,可以decay成指標來使用,也就是我可以把一個陣列當成一個指標來使用(again,陣列!=指標) 但是多維陣列中,無法如此使用,事實上這也很直觀,試圖拿一個pointertopointertoint來操作一個int二維陣列,這是不合理的! 儘管我們無法將二維陣列直接decay成兩個指標,但是我們可以換個角度想,二維陣列可以看成"外層大的一維陣列,每一維內層各又包含著一維陣列" 如果想通了這一點,我們可以仿造之前的規則,把外層大的一維陣列decay成指標,該指標指向內層的一維陣列 1 2 3 4 voidpass2DArray(int(*array)[10]);//array是個指標,指向int[10] inta[5][10]; pass2DArray(a);   這時候就很好理解了,函數pass2DArray內的array[0]會代表什麼呢?答案是它代表著a[0]外層的那一維陣列,裡面包含著內層[0]~[9],也因此array[0][2]就會對應到a[0][2],array[4][9]對應到a[4][9] 結論就是,只有最外層的那一維陣列可以decay成指標,其他維陣列都要明確的指出陣列大小,這樣多維陣列的傳遞就不會有問題了 也因為剛剛的例子,我們可以清楚的知道在傳遞陣列時,實際行為是在傳遞指標,也因此如果我們想用sizeof來求得陣列元素個數,那是不可行的 1 2 3 4 錯誤例子: voidprint1DArraySize(int*arr){ printf("%u",sizeof(arr)/sizeof(arr[0]));/*sizeof(arr)只是*/ } 受此限制,我們必須手動傳入大小 1 voidprint1DArraySize(int*arr,size_tarrSize); 註:typedefunsignedintsize_t與平台無關的,表示0-MAXINT的無號整數 C++提供reference的機制,使得我們不需再這麼麻煩,可以直接傳遞陣列的reference給函數,大小也可以直接求出 1 2 3 4 正確例子: voidprint1DArraySize(int(&array)[10]){//傳遞reference cout<



請為這篇文章評分?