小語言,大工具箱
你現在已經認識了 C 的整個核心:型別與變數、運算子、控制流程、函式與遞迴、陣列與字串,還有結構與列舉。令人吃驚的是它的份量竟然這麼少。C 語言本身沒有內建的印出指令、沒有內建的讀檔方式、沒有內建的開平方根。這不是疏漏——這就是設計。語言被刻意保持得很小,好讓它能在任何地方跑,其餘的一切都住在 C 標準函式庫裡,那是隨每一套 C 編譯器一起出貨的一堆現成函式。
把語言想成一間只有工作檯和虎鉗的空工坊,而標準函式庫是你每天都打開的那個工具抽屜:印東西的鎚子、切字串的鋸子、量大小的捲尺。你藉由在檔案最上方「引入」工具的標頭檔來取用它。像 `#include <stdio.h>` 這樣一行,等於告訴編譯器「我要用標準輸入/輸出的工具」,從此 printf 之類的名字就可以使用了。每個抽屜都有一個標頭檔:stdio.h 管輸入輸出、string.h 管字串、stdlib.h 是一般雜項工具、math.h 是數學。
stdio:和外界對話
你早就在用的那個抽屜是 stdio,標準輸入/輸出。它的當家明星是 printf,負責印出格式化的文字。printf 的關鍵在於格式字串:一個帶有小佔位符的範本,每個佔位符以百分號開頭,並由你接在後面傳入的某個值填進去。所以 `printf("x is %d\n", x)` 會印出「x is 」這幾個字,接著在 %d 的位置印出 x 裡的整數,最後印一個換行。%d 表示「這裡放一個 int」;%s 表示「這裡放一個字串」;%f 表示「一個浮點數」。
反過來,scanf 把打進來的輸入讀進你的變數,getchar 則一次讀一個字元。這三者背後都坐著一個概念:串流——一條流動的位元組序列,而不是一整塊。你的程式啟動時就已開好三條串流——標準輸入(你打的字)、標準輸出(printf 去的地方)、標準錯誤(抱怨去的地方)。把正常輸出和錯誤訊息放在不同串流上,正是讓你日後能把結果送進檔案、而錯誤仍留在螢幕上的原因。
string 抽屜,以及藏在裡頭的安全教訓
回想上一篇:C 字串只是一個在第一個零位元組處結束的字元陣列——也就是以空字元結尾的字串。因為字串不是內建型別,對它的每個常見操作都是 string.h 裡的一個函式庫函式。strlen 數出到結尾符為止的字元數;strcmp 告訴你兩個字串是否相等;strcpy 把一個字串複製進另一個;strcat 把一個接到另一個的尾巴上。這些都不是魔法——每一個都是一個沿著陣列走、直到遇上零位元組才停的小迴圈,正是你自己也寫得出來的那種迴圈。
函式庫在這裡教了它最難的一課,所以我們就老實說吧。strcpy 從來源把字元複製進目的地,直到撞上結尾符為止——但它根本不知道你的目的地陣列有多大。如果來源比你預留的空間還長,strcpy 會一路寫過陣列尾端,把緊鄰其後的記憶體塗掉。這就是緩衝區溢位,全 C 語言裡最常見也最危險的錯誤之一:輕則程式當掉,重則攻擊者操縱那些多寫的位元組來接管你的程式。
stdlib、math,以及向系統要記憶體
stdlib.h 這個抽屜是裝著一堆真正好用零碎工具的雜物抽屜。atoi 把字串 "42" 變成整數 42;strtol 做同樣的事但更小心,還會告訴你它是否失敗;rand 給你一個偽隨機數;qsort 排序一個陣列;exit 立刻結束程式,並把一個結束狀態交回給執行它的人。隔壁的 math.h 裝著數值工具——sqrt、pow、sin、floor——它們作用在你先前認識的浮點數上。在某些系統上,數學函式庫要另外用「gcc main.c -lm」連結。
stdlib 也藏著通往整個函式庫裡最舉足輕重的概念的那道門:在程式執行期間向系統要記憶體。到目前為止,你做的每個陣列大小都是寫程式時就定死的。但如果你要等到程式跑起來才知道需要幾個項目呢——比方說,讀進使用者打的一個數字之後?答案是動態記憶體配置:你呼叫 malloc 並給它一個位元組數,系統就交回一塊剛好那麼大、供你使用的區域。calloc 做同樣的事但會先把記憶體清成零;realloc 則調整你已經有的一塊的大小。
int *scores = malloc(n * sizeof(int)); /* ask for n ints */
if (scores == NULL) { /* ALWAYS check */
perror("malloc"); /* report the error */
return 1; /* fail honestly */
}
/* ... use scores[0] .. scores[n-1] ... */
free(scores); /* give it back */兩條規則伴著 malloc 而來,少做任何一條日後都會反咬你。第一,malloc 會失敗——如果系統沒有記憶體可給,它就回傳一個特殊的「這裡什麼都沒有」的值,使用它會讓程式當掉,所以在你碰它之前,必須先檢查回傳結果,就像上面那段程式那樣。第二,你用 malloc 拿來的記憶體不會被自動清理;用完之後你必須用 free 把它還回去,否則你的程式會慢慢囤積它再也用不到的記憶體。更深層的機制——malloc 怎麼找到那塊記憶體、你 free 時到底發生了什麼、一次記憶體洩漏的代價是什麼——是另外一級的主題。現在,先學會這個節奏:拿、檢查、用、還。
那唯一的習慣:檢查函式庫告訴你的事
如果你從這一整級只帶走一個習慣,就帶走這個:函式庫函式會回報失敗,而你必須去聽。C 沒有那種會自己跳出來、替你把程式停下的例外機制;取而代之,函式透過它的回傳值示意出了問題。malloc 在沒記憶體時回傳一個「什麼都沒有」的值。fopen 在開不了檔時回傳同樣的值。strtol 回傳零並設一個旗標。「呼叫一定會成功」這個客氣的假象,正是初學者的程式悄悄弄壞資料、然後在離真正起因很遠的地方才當掉的原因。
當一個函式需要說的不只是「失敗了」、而要說「為什麼失敗」時,許多函式庫與系統呼叫會把一個叫 errno 的全域變數設成一個代碼,指明具體問題,例如「找不到檔案」或「權限不足」。你在那個失敗的呼叫之後立刻讀它,再用 perror 或 strerror 把它變成一句訊息。這就是經典的回傳碼加上 errno樣式:回傳值說是或否,errno 說到底哪裡出錯。
- 呼叫函式並把它的回傳值存起來——不要在同一行就把它丟掉。
- 把那個值和說明文件指明的失敗訊號比對(malloc 和 fopen 是 NULL,許多系統呼叫是一個負數)。
- 失敗時就回報它——若那個呼叫會設 errno,就用 perror 讀出來——並誠實地決定要復原還是停下,而不是抱著一個壞值繼續橫衝直撞。