[重要] 發文前務必閱讀:常見問題十三誡

看板C_and_CPP作者 (沒有存在感的人)時間8年前 (2016/04/18 00:15), 8年前編輯推噓5(5012)
留言17則, 7人參與, 最新討論串1/1
C 語言新手十三誡(The Thirteen Commandments for Newbie C Programmers) by Khoguan Phuann 請注意: (1) 本篇旨在提醒新手,避免初學常犯的錯誤(其實老手也常犯:-Q)。 但不能取代完整的學習,請自己好好研讀一兩本 C 語言的好書, 並多多實作練習。 (2) 強烈建議新手先看過此文再發問,你的問題極可能此文已經提出並解答了。 (3) 以下所舉的錯誤例子如果在你的電腦上印出和正確例子相同的結果, 那只是不足為恃的一時僥倖。 (4) 不守十三誡者,輕則執行結果的輸出數據錯誤,或是程式當掉,重則 引爆核彈、毀滅地球(如果你的 C 程式是用來控制核彈發射器的話)。 ============================================================================ 目錄: 2 01. 不可以使用尚未給予適當初值的變數 3 02. 不能存取超過陣列既定範圍的空間 4 03. 不可以提取不知指向何方的指標 5 04. 不要試圖用 char* 去更改一個"字串常數" 8 05. 不能在函式中回傳一個指向區域性自動變數的指標 10 06. 不可以只做 malloc(), 而不做相應的 free() 13 07. 在數值運算、賦值或比較中不可以隨意混用不同型別的數值 14 08. 在一個運算式中,不能對一個基本型態的變數修改其值超過一次以上 16 09. 在 Macro 定義中, 務必為它的參數個別加上括號 19 10. 不可以在 stack 設置過大的變數 21 11. 使用浮點數精確度造成的誤差問題 22 12. 不要猜想二維陣列可以用 pointer to pointer 來傳遞 23 13. 函式內 new 出來的空間記得要讓主程式的指標接住 27 直接輸入數字可跳至該頁碼 01. 你不可以使用尚未給予適當初值的變數 錯誤例子: int accumulate(int max) /* 從 1 累加到 max,傳回結果 */ { int sum; /* 未給予初值的區域變數,其內容值是垃圾 */ int num; for (num = 1; num <= max; num++) { sum += num; } return sum; } 正確例子: int accumulate(int max) { int sum = 0; /* 正確的賦予適當的初值 */ int num; for (num = 1; num <= max; num++) { sum += num; } return sum; } 02. 你不可以存取超過陣列既定範圍的空間 錯誤例子: int str[5]; int i; for (i = 0 ; i <= 5 ; i++) str[i] = i; 正確例子: int str[5]; int i; for (i = 0; i < 5; i++) str[i] = i; 說明:宣告陣列時,所給的陣列元素個數值如果是 N, 那麼我們在後面 透過 [索引值] 存取其元素時,所能使用的索引值範圍是從 0 到 N-1 C/C++ 為了執行效率,並不會自動檢查陣列索引值是否超過陣列邊界, 我們要自己來確保不會越界。一旦越界,操作的不再是合法的空間, 將導致無法預期的後果。 03. 你不可以提取(dereference)不知指向何方的指標(包含 null 指標)。 錯誤例子: char *pc1; /* 未給予初值,不知指向何方 */ char *pc2 = NULL; /* pc2 起始化為 null pointer */ *pc1 = 'a'; /* 將 'a' 寫到不知何方,錯誤 */ *pc2 = 'b'; /* 將 'b' 寫到「位址0」,錯誤 */ 正確例子: char c; /* 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, 礙於篇幅本文假定使用上皆合法,也有正確歸還記憶體 ) 錯誤例子: char *name; /* name 尚未指向有效的空間 */ printf("Your name, please: "); fgets(name,20,stdin); /* 您確定要寫入的那塊空間合法嗎??? */ printf("Hello, %s\n", name); 正確例子: /* 如果編譯期就能決定字串的最大空間,那就不要宣告成 char* 改用 char[] */ char name[21]; /* 可讀入字串最長 20 個字元,保留一格空間放 '\0' */ printf("Your name, please: "); fgets(name,20,stdin); printf("Hello, %s\n", name); 正確例子(2): /* 若是在執行時期才能決定字串的最大空間,則需利用 malloc() 函式來動態 分配空間 */ size_t length; char *name; printf("請輸入字串的最大長度(含null字元): "); scanf("%u", &length); name = (char *)malloc(length); printf("Your name, please: "); scanf("%s", name); printf("Hello, %s\n", name); /* 最後記得 free() 掉 malloc() 所分配的空間 */ free(name); name = NULL; 04. 你不可以試圖用 char* 去更改一個"字串常數" 錯誤例子: char* pc = "john"; /* pc 現在指著一個字串常數 */ *pc = 'J'; /* 但是 pc 沒有權利去更改這個常數! */ 正確例子: char pc[] = "john"; /* pc 現在是個合法的陣列,裡面住著字串 john */ /* 也就是 pc[0]='j', pc[1]='o', pc[2]='h', pc[3]='n', pc[4]='\0' */ *pc = 'J'; pc[2] = 'H'; 說明:字串常數的內容是"唯讀"的。您有使用權,但是沒有更改的權利。 若您希望使用可以更改的字串,那您應該將其放在合法空間 錯誤例子: char *s1 = "Hello, "; char *s2 = "world!"; /* strcat() 不會另行配置空間,只會將資料附加到 s1 所指唯讀字串的後面, 造成寫入到程式無權碰觸的記憶體空間 */ strcat(s1, s2); 正確例子(2): /* s1 宣告成陣列,並保留足夠空間存放後續要附加的內容 */ char s1[20] = "Hello, "; char *s2 = "world!"; /* 因為 strcat() 的返回值等於第一個參數值,所以 s3 就不需要了 */ strcat(s1, s2); 05. 你不可以在函式中回傳一個指向區域性自動變數的指標。否則,會得到垃圾值 [感謝 gocpp 網友提供程式例子] 錯誤例子: char *getstr(char *name) { char buf[30] = "hello, "; /*將字串常數"hello, "的內容複製到buf陣列*/ strcat(buf, name); return buf; } 說明:區域性自動變數,將會在離開該區域時(本例中就是從getstr函式返回時) 被消滅,因此呼叫端得到的指標所指的字串內容就失效了。 正確例子: void getstr(char buf[], int buflen, char const *name) { char const s[] = "hello, "; strcpy(buf, s); strcat(buf, name); } 正確例子: int* foo() { int* pInteger = (int*) malloc( 10*sizeof(int) ); return pInteger; } int main() { int* pFromfoo = foo(); } 說明:上例雖然回傳了函式中的指標,但由於指標內容所指的位址並非區域變數, 而是用動態的方式抓取而得,換句話說這塊空間是長在 heap 而非 stack, 又因 heap 空間並不會自動回收,因此這塊空間在離開函式後,依然有效 (但是這個例子可能會因為 programmer 的疏忽,忘記 free 而造成 memory leak) [針對字串操作,C++提供了更方便安全更直觀的 string class, 能用就盡量用] 正確例子: #include <string> /* 並非 #include <cstring> */ using std::string; string getstr(string const &name) { return string("hello, ") += name; } 06. 你不可以只做 malloc(), 而不做相應的 free(). 否則會造成記憶體漏失 但若不是用 malloc() 所得到的記憶體,則不可以 free()。已經 free()了 所指記憶體的指標,在它指向另一塊有效的動態分配得來的空間之前,不可 以再被 free(),也不可以提取(dereference)這個指標。 [C++] 你不可以只做 new, 而不做相應的 delete 注:new 與 delete 對應,new[] 與 delete[] 對應,不可混用 切記,做了幾次 new,就必須做幾次 delete 小技巧: 可在 delete 之後將指標指到 NULL,由於 delete 本身會先做檢查, 因此可以避免掉多次 delete 的錯誤 正確例子: int *ptr = new int(99); delete ptr; ptr = NULL; delete ptr; /* delete 只會處理指向非 NULL 的指標 */ 07. 你不可以在數值運算、賦值或比較中隨意混用不同型別的數值,而不謹慎考 慮數值型別轉換可能帶來的「意外驚喜」(錯愕)。必須隨時注意數值運算 的結果,其範圍是否會超出變數的型別 錯誤例子: unsigned int sum = 2000000000 + 2000000000; /* 超出 int 存放範圍 */ unsigned int sum = (unsigned int) (2000000000 + 2000000000); double f = 10 / 3; 正確例子: /* 全部都用 unsigned int, 注意數字後面的 u, 大寫 U 也成 */ unsigned int sum = 2000000000u + 2000000000u; /* 或是用顯式的轉型 */ unsigned int sum = (unsigned int) 2000000000 + 2000000000; double f = 10.0 / 3.0; 錯誤例子: unsigned int a = 0; int b[10]; for(int i = 9 ; i >= a ; i--) { b[i] = 0; } 說明:由於 int 與 unsigned 共同運算的時候,會轉換 int 為 unsigned, 因此迴圈條件永遠滿足,與預期行為不符 錯誤例子: (感謝 sekya 網友提供) unsigned char a = 0x80; /* no problem */ char b = 0x80; /* implementation-defined result */ if( b == 0x80 ) { /* 不一定恒真 */ printf( "b ok\n" ); } 說明:語言並未規定 char 天生為 unsigned 或 signed,因此將 0x80 放入 char 型態的變數,將會視各家編譯器不同作法而有不同結果 08. 你不可以在一個運算式(expression)中,對一個基本型態的變數修改其值 超過一次以上。否則,將導致未定義的行為(undefined behavior) 錯誤例子: int i = 7; int j = ++i + i++; 正確例子: int i = 7; int j = ++i; j += i++; 你也不可以在一個運算式(expression)中,對一個基本型態的變數修改其值, 而且還在同一個式子的其他地方為了其他目的而存取該變數的值。(其他目的, 是指不是為了計算這個變數的新值的目的)。否則,將導致未定義的行為。 錯誤例子: x = x++; 錯誤例子: int arr[5]; int i = 0; arr[i] = i++; 正確例子: int arr[5]; int i = 0; arr[i] = i; i++; 錯誤例子: int i = 10; cout << i << "==" << i++; 正確例子: int i = 10; cout << i << "=="; cout << i++; 錯誤例子: int Integer=10; printf( "%d %d %d", Integer++, Integer++, Integer++ ); 錯誤例子: void foo(int a, int b) { ... } int main() { int i=0; foo(i++, i++); } 說明: C/C++ 並沒有強制規定參數會由哪個方向開始處理(不像Java是由左到右), 因此可能會造成與預期不符的情況 09. 在 Macro 定義中, 務必為它的參數個別加上括號 錯誤例子: #include <stdio.h> #define SQUARE(x) (x * x) int main() { printf("%d\n", SQUARE(10-5)); return 0; } 正確例子: #include <stdio.h> #define SQUARE(x) ((x) * (x)) int main() { printf("%d\n", SQUARE(10-5)); return 0; } 說明:如果是用 C++, 請多多利用 inline function 來取代上述的 macro, 以免除 macro 定義的種種危險性。如: inline int square(int x) { return x * x; } macro 定義出的「偽函式」至少缺乏下列數項函式本有的能力: (1) 無法進行參數型別的檢查。 (2) 無法遞迴呼叫。 (3) 無法用 & 加在 macro name 之前,取得函式位址。 (4) 呼叫時往往不能使用具有 side effect 的引數。例如: 錯誤例子: (感謝 yaca 網友提供) #define MACRO(x) (((x) * (x)) - ((x) * (x))) int main() { int x = 3; printf("%d\n", MACRO(++x)); return 0; } 10. 不可在 stack 設置過大的變數,否則會造成 stack overflow (感謝 VictorTom 版友幫忙) 錯誤例子: int array[10000000]; // 僅舉例說明 說明:由於編譯器會自行決定 stack 的上限,某些預設是數 KB 或數十 KB, 當變數所需的空間過大時,很容易造成 stack overflow,程式亦隨之 當掉,若真正需要如此大的空間,那麼建議配置在 heap 上,或是採用 static / global variable,亦或是改變編譯器的設定 使用 heap 時,雖然整個 process 可用的空間是有限的,但採用動態抓取 的方式,new 無法配置時會丟出 std::bad_alloc 例外,malloc 無法配置 時會回傳 null,不會影響到正常使用下的程式功能 正確例子: int *array = (int*) malloc( 10000000*sizeof(int) ); 說明:由於此時 stack 上只需配置一個 int* 的空間,可避免 stack overflow 更多說明請參考精華區 z-10-13 11. 使用浮點數千萬要注意精確度所造成的誤差問題 根據 IEEE 754 的規範,又電腦中是用有限的二進位儲存數字,因此常有可 能因為精確度而造成誤差,例如加減乘除,等號大小判斷,分配律等數學上 常用到的操作,很有可能因此而出錯(不成立) 更詳細的說明可以參考精華區 z-8-11 或參考冼鏡光老師所發表的一文 "使用浮點數最最基本的觀念" http://blog.dcview.com/article.php?a=VmhQNVY%2BCzo%3D 12. 不要猜想二維陣列可以用 pointer to pointer 來傳遞 (感謝 loveme00835 legnaleurc 版友的幫忙) 首先必須有個觀念,C 語言中陣列是無法直接拿來傳遞的! 不過這時候會有人跳出來反駁: void pass1DArray( int array[] ); int a[10]; pass1DArray( a ); /* 可以合法編譯,而且執行結果正確!! */ 事實上,編譯器會這麼看待 void pass1DArray( int *array ); int a[10]; pass1DArray( &a[0] ); 我們可以順便看出來,array 變數本身可以 decay 成記憶體起頭的位置 因此我們可以 int *p = a; 這種方式,拿指標去接陣列。 也因為上述的例子,許多人以為那二維陣列是不是也可以改成 int ** 錯誤例子: void pass2DArray( int **array ); int a[5][10]; pass2DArray( a ); /* 這時候編譯器就會報錯啦 */ /* expected ‘int **’ but argument is of type ‘int (*)[10]’*/ 在一維陣列中,指標的移動操作,會剛好覆蓋到陣列的範圍 例如,宣告了一個 a[10],那我可以把 a 當成指標來操作 *a 至 *(a+9) 因此我們可以得到一個概念,在操作的時候,可以 decay 成指標來使用 也就是我可以把一個陣列當成一個指標來使用 (again, 陣列!=指標) 但是多維陣列中,無法如此使用,事實上這也很直觀,試圖拿一個 pointer to pointer to int 來操作一個 int 二維陣列,這是不合理的! 儘管我們無法將二維陣列直接 decay 成兩個指標,但是我們可以換個角度想, 二維陣列可以看成 "外層大的一維陣列,每一維內層各又包含著一維陣列" 如果想通了這一點,我們可以仿造之前的規則, 把外層大的一維陣列 decay 成指標,該指標指向內層的一維陣列 void pass2DArray( int (*array) [10] ); // array 是個指標,指向 int [10] int a[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 來求得陣列元素個數,那是不可行的 錯誤例子: void print1DArraySize( int* arr ) { printf("%u", sizeof(arr)/sizeof(arr[0])); /* sizeof(arr) 只是 */ } /* 一個指標的大小 */ 受此限制,我們必須手動傳入大小 void print1DArraySize( int* arr, size_t arrSize ); C++ 提供 reference 的機制,使得我們不需再這麼麻煩, 可以直接傳遞陣列的 reference 給函數,大小也可以直接求出 正確例子: void print1DArraySize( int (&array)[10] ) { // 傳遞 reference cout << sizeof(array) / sizeof(int); // 正確取得陣列元素個數 } 13. 函式內 new 出來的空間記得要讓主程式的指標接住 對指標不熟悉的使用者會以為以下的程式碼是符合預期的 void newArray(int* local, int size) { local = (int*) malloc( size * sizeof(int) ); } int main() { int* ptr; newArray(ptr, 10); } 接著就會找了很久的 bug,最後仍然搞不懂為什麼 ptr 沒有指向剛剛拿到的合法空間 讓我們再回顧一次,並且用圖表示 ______________ 1. int* ptr; ptr -> |__未知的空間__| ______________ 2. 呼叫函式 newArray ptr -> |__未知的空間__| <- local ______________ 3. malloc 取得合法空間 ptr -> |__未知的空間__| ______________ |___合法空間___| <- local ______________ 4. 離開函式 ptr -> |__未知的空間__| 用圖看應該一切就都明白了,我也不需冗言解釋 也許有人會想問,指標不是傳址嗎? 精確來講,指標也是傳值,只不過該值是一個位址 (ex: 0xfefefefe) local 接到了 ptr 指向的那個位置,接著函式內 local 要到了新的位置 但是 ptr 指向的位置還是沒變的,因此離開函式後就好像事什麼都沒發生 ( 嚴格說起來還發生了 memory leak ) 以下是一種解決辦法 int* createNewArray(int size) { return (int*) malloc( size * sizeof(int) ); } int main() { int* ptr; ptr = createNewArray(10); } 改成這樣亦可 ( 為何用 int** 就可以?想想他會傳什麼過去給local ) void createNewArray(int** local, int size) { *local = (int*) malloc( size * sizeof(int) ); } int main() { int *ptr; createNewArray(&ptr, 10); } 如果是 C++,別忘了可以善用 Reference void newArray(int*& local, int size) { local = new int[size]; } 後記:從「古時候」流傳下來一篇文章 "The Ten Commandments for C Programmers"(Annotated Edition) by Henry Spencer http://www.lysator.liu.se/c/ten-commandments.html 一方面它不是針對 C 的初學者,一方面它特意模仿中古英文 聖經的用語,寫得文謅謅。所以我現在另外寫了這篇,希望 能涵蓋最重要的觀念以及初學甚至老手最易犯的錯誤。 作者:潘科元(Khoguan Phuann) (c)2005. 感謝 ptt.cc BBS 的 C_and_CPP 看板眾多網友提供寶貴意見及程式實例。 nowar100 多次加以修改整理,擴充至 13 項,並且製作成動畫版。 wtchen 應板友要求移除動畫 如發現 Bug 請推文回報,謝謝您 -- ※ 發信站: 批踢踢實業坊(ptt.cc)

09/22 11:13,
推一個唷~ 變動畫了XDD
09/22 11:13

09/22 11:32,
昨天晚上就看到變成動畫了 只是沒人推就不敢推下去XD
09/22 11:32

09/22 14:16,
推....:)
09/22 14:16

09/24 20:21,
改版了XD
09/24 20:21

09/26 11:00,
第8戒 int i = 7; int j = ++i + i++; 蠻多公司有這考題
09/26 11:00

09/26 11:00,
printf("i=%d, j=%d \n", i, j); => i=9, j=16
09/26 11:00

09/26 11:01,
一般推論答案無誤, 但就與第8戒違反了@@ 該如何解....
09/26 11:01

09/26 13:06,
問題不存在 不應該出現的就是不應該 就算大家約定成俗
09/26 13:06

09/26 13:07,
討論它也是沒什麼意義的
09/26 13:07

09/28 20:33,
推~真的很常犯...話說其實我比較希望有下載版XD
09/28 20:33
※ 編輯: nowar100 來自: 140.112.30.82 (10/08 13:53)

10/10 19:47,
有時間不是應該要專精自己的領域嗎?
10/10 19:47

11/16 22:22,
有些看不太懂....
11/16 22:22

11/16 22:25,
樓上哪裡看不懂 可以發文問 版上很熱心的 :)
11/16 22:25

03/07 11:24,
選擇畫面的排版很糟糕, 請全部靠左吧...
03/07 11:24

03/19 08:35,
全部靠左的話,就會有空白換行不均的排版問題了 :(
03/19 08:35

04/02 22:33,
可以不要用動畫嗎 =.=
04/02 22:33

06/26 15:09,
可以不要用動話嗎?
06/26 15:09

01/01 14:49,
可以不要用動畫嗎?
01/01 14:49

03/03 22:35,
動畫好難用 By 初學者... = =a
03/03 22:35

01/10 20:46,
看來我以前的動畫排版造成不少人的困擾 真抱歉 QQ
01/10 20:46

04/10 02:33,
推動畫,雖然主選單有點亂
04/10 02:33

05/01 22:33,
推,太有用了!
05/01 22:33

05/09 16:06,
剛開始寫程式就連犯5誡以上 根本悲劇
05/09 16:06

06/22 00:14,
第13點的最後一個例子是不是錯了? int (&array)[10]??
06/22 00:14

12/10 18:46,
大推!! 神文!!
12/10 18:46

12/21 08:01,
受益匪淺
12/21 08:01

06/11 13:41,
好一個當頭棒喝=一個完美的開始
06/11 13:41

10/03 08:54,
十誡~~~ 引以為戒
10/03 08:54

11/10 18:52,
什麼時候變13誡了!?
11/10 18:52
-- ※ 發信站: 批踢踢實業坊(ptt.cc), 來自: 90.41.60.207

04/15 15:55, , 1F
目錄那邊要多空行翻頁才會對
04/15 15:55, 1F
※ 編輯: wtchen (90.41.60.207), 04/15/2016 15:56:27 ※ 編輯: wtchen (90.41.60.207), 04/15/2016 15:57:03

04/16 01:19, , 2F
拿掉動畫之後我覺得可以改成在後面標頁數 ' -')
04/16 01:19, 2F

04/16 01:20, , 3F
畢竟 PTT 有支援在文章頁面輸入數字跳至第幾頁 ' -')
04/16 01:20, 3F

04/16 01:46, , 4F
....板工不會用,有強者可以代勞嗎?
04/16 01:46, 4F
感謝 red0210 代為嵌入頁數 ※ 編輯: wtchen (90.41.60.207), 04/18/2016 00:17:45 ※ 編輯: wtchen (90.41.60.207), 04/18/2016 00:19:41 ※ 編輯: wtchen (90.41.60.207), 04/19/2016 00:29:59

04/19 07:44, , 5F
有版主 有推倒
04/19 07:44, 5F

04/21 22:44, , 6F
我每頁不是24行時, 頁數就會錯誤... 可否加上行數? (###.)
04/21 22:44, 6F
※ 編輯: wtchen (90.41.32.22), 04/21/2016 22:50:04

04/21 22:51, , 7F
可能要等下次改版(加延伸閱讀部份),不用急不會太久
04/21 22:51, 7F

04/29 12:59, , 8F
終於把動畫改掉了~可喜可賀
04/29 12:59, 8F

04/30 13:28, , 9F
頁數錯誤是什麼情況?可以說得更清楚一點嗎
04/30 13:28, 9F
※ 編輯: wtchen (86.209.161.140), 04/30/2016 15:56:40

05/07 01:13, , 10F
比方我目前視窗最大化, Rows 42 Columns 168;
05/07 01:13, 10F

05/07 01:16, , 11F
在這篇文章的第一頁, 下方的瀏覽狀態即顯示:
05/07 01:16, 11F

05/07 01:16, , 12F
瀏覽 第 1/19 頁 ( 8%) 目前顯示: 第 01~40 行
05/07 01:16, 12F

05/07 01:18, , 13F
按頁數 12 就已跳到 第10誡 了.
05/07 01:18, 13F

05/10 19:21, , 14F
原來如此,不過為何會每頁不是24行…
05/10 19:21, 14F

05/10 20:33, , 15F
PuTTY Configuration => Window => When window is resized:
05/10 20:33, 15F

05/10 20:33, , 16F
Change the number of rows and columns
05/10 20:33, 16F

05/10 20:34, , 17F
用習慣很多列以後, 就回不去了 :P
05/10 20:34, 17F
※ 編輯: wtchen (86.209.153.222), 05/13/2016 20:19:48
文章代碼(AID): #1N4xQszL (C_and_CPP)