《Head First C 中文版》審讀筆記(一)

黃志斌發表於2013-05-02

不顧一切找 Frank

《Head First C 中文版》第 94 頁:

 1: #include <stdio.h>
 2: #include <string.h>
 3: 
 4: char tracks[][80] = {
 5:   "I left my heart in Harvard Med School",
 6:   "Newark, Newark - a wonderful town",
 7:   "Dacing with a Dork",
 8:   "From here to maternity",
 9:   "The girl from Iwo Jima",
10: };
11: 
12: void find_track(char search_for[])
13: {
14:   int i;
15:   for (i = 0; i < 5; i++) {
16:     if (strstr(tracks[i], search_for))
17:       printf("Track %i: '%s'\n", i, tracks[i]);
18:   }
19: }
20: 
21: int main()
22: {
23:   char search_for[80];
24:   printf("Search for: ");
25:   fgets(search_for, 80, stdin);
26:   find_track(search_for);
27:   return 0;
28: }

試駕

第 95 頁:

下面開啟終端,看看程式碼能否工作:

> gcc text_search.c -o text_search && ./text_search
Search for: town
Track 1: 'Newark, Newark - a wonderful town'
>

好訊息,程式工作了!
到目前為止,這個程式是你寫過最長的一個,但它做了更多的事情。程式建立了一個字元陣列,並利用標準庫中的字串處理函式搜尋陣列中的歌名,最後找到了使用者想要找的歌曲。

程式其實不工作

實際上,在我的機器上,這個程式的執行結果是:

> gcc text_search.c -o text_search && ./text_search
Search for: town
>

並且,不論你輸入什麼,這個程式都沒有找到任何歌曲。為什麼會這樣呢?

C 語言庫函式 fgets()

問題出在這個程式第 25 行的fgets()上面:

char *fgets(char *s, int size, FILE *stream);

實際上,fgets() 函式從輸入流stream讀取最多size - 1個字元到緩衝區s中。如果遇到EOF或者換行符則停止。如果讀到換行符,則換行符也儲存到s中。然後在緩衝區s中附加一個null位元組('\0')。

這就是程式不工作的原因了,我們的search_for中包含了換行符,所以find_track()函式無法搜尋到任何歌曲。

修正方案一

簡單地使用以下語句代替第 25 行的語句:

gets(search_for);

這個程式就可以正常工作了。因為gets()函式從標準輸入讀取字元到緩衝區中,也是遇到EOF或者換行符就停止。如果讀取換行符,就扔掉它,並不儲存到search_for中。這正符合我們的要求。

且慢,gets()函式是個十分危險的傢伙,非常容易造成緩衝區溢位。實際上,gcc 編譯器在編譯時會給出以下警告:

text_search.c: 在函式‘main’中:
text_search.c:25:3: 警告:不建議使用‘gets’(宣告於 /usr/include/stdio.h:638) [-Wdeprecated-declarations]
   gets(search_for);
   ^

C 語言庫函式 gets()

《Head First 中文版》第 67 頁:

fgets() 函式其實是從一個更古老的函式演變而來的,它叫 gets() 。

儘管我們說 fgets() 比 scanf() 更安全,但它的祖先 gets() 才是最危險的傢伙。為什麼?因為 gets() 函式沒有任何限制:

char dangerous[10];
gets(dangerous);  // 別!我是認真的,千萬別用它。

雖然 gets() 函式已經行走江湖很多年了,但真的不應該用它。

修正方案二

將第 25 行替換為以下語句:

scanf("%79s", search_for);

這也可以正確工作。注意,一定要用%79s格式串,不要使用%s格式串,否則也非常容易造成緩衝區溢位。

修正方案三

但是,修正方案二也有個小缺點,如果我們輸入的字串中包含空格,scanf()函式只會讀取空格之前的字元。所以就有以下修正方案三:

  1. 在第 25 行之後增加一句:

    remove_tail_newline(search_for);
    
  2. 在第 21 行之前增加一個函式remove_tail_newline,用於刪除字串尾部的換行符:

    void remove_tail_newline(char str[])
    {
      size_t len = strlen(str);
      if (len > 0 && str[len - 1] == '\n') str[len - 1] = '\0';
    }
    

這樣,就完美解決這個問題。注意,remove_tail_newline函式不能寫成以下樣子:

str[strlen(str) - 1] = '\0';

這是因為:

  1. 字串str的尾部可能不包含換行符,比如標準輸入被重定向到一個不包括換行符的檔案中。
  2. 字串str的長度還有可能為零。

修正方案四

將第 25 行替換為以下語句:

scanf("%79[^\n]\n", search_for);

這和修正方案二很像,只不過是把格式符 %79s 替換為 %79[^\n]\n 。這能夠讀入空格,避免修正方案二的缺點。但 scanf() 函式的這個格式符在 K&R 的經典著作中沒有提到,應該是後來增加的。我想現代的 C 編譯器應該都會支援。(老古董的 C 編譯器就難說了)

附註:根據 @It 的評論,這個修正方案有個問題:輸入完回車後不會馬上響應,需要再輸入點什麼後才返回結果。所以這個方案不太靠譜,使用者體驗不好。看來這種格式符更適用於從檔案中讀取輸入等不與使用者發生互動的場合。

再次附註:把格式符從%79[^\n]\n改為%79[^\n]可以解決上述問題。

其他影響

《Head First C 中文版》第 91 頁、第 93 頁也涉及到上述text_search.cmain()函式,也要進行相應修改。

參考資料

  1. 《Head First 中文版》
  2. The GNU C Library: Line Input
  3. POSIX specification: fscanf, scanf, sscanf
  4. Linux manual page: gets(3)
  5. Linux manual page: scanf(3)
  6. cppreference: gets, gets_s
  7. Brian W. Kernighan, Dennis M. Ritchie: The C Programming Language, Second Edition

相關文章