為什麼「hello world」值得一寫
整個本階你一直在建立一張心智地圖:電腦是一台儲存程式機器,會擷取並執行指令;高階與低階語言座落在抽象層疊的不同高度;而編譯式與直譯式語言則從你的文字到執行中行程,走出兩條不同的路。現在我們要把這些都兌現。第一支程式的重點不在於它做了什麼了不起的事——它幾乎什麼都沒做——而在於執行它會走過從原始碼到矽晶片的整條路,讓你讀過的每一塊都變成看得見的東西。
C 的第一支程式按慣例是一支 hello world 程式:印出一行字,然後停下。它是一個語言能做、卻仍會碰觸外在世界的最小而誠實的事。為了印字,你的程式必須把一個字串交給 C 標準函式庫,函式庫最終再請核心把位元組寫到你的終端機——所以即使這支小程式,也悄悄地從你的程式碼、經過函式庫、往下穿到作業系統,再折返回來。我們不會假裝這趟穿越很瑣碎,但現在你只需要看著它發生。
這七行,以及每一行的用途
整支程式如下。把它一字不差地打進一個叫 hello.c 的檔案——C 對細節錙銖必較。注意它真的很短,但這裡沒有任何裝飾:每一行都名副其實,我們會逐行走過。
#include <stdio.h>
int main(void)
{
printf("Hello, world!\n");
return 0;
}第一行 #include <stdio.h> 是一道前置處理器指令,它把標準輸入/輸出的宣告(包含 printf())貼進來;沒有它,編譯器就不知道 printf() 是什麼。int main(void) 這一行定義了程式的進入點——系統會呼叫這唯一一個叫 main 的函式來啟動你;int 表示它會交回一個整數,void 表示這裡它不接收任何引數(之後你會遇到接收命令列引數的 argc 與 argv 版本)。在裡頭,printf() 請函式庫印出文字,其中 \n 是單一個換行字元,而不是反斜線後面跟著一個 n。
最後一行 return 0 是最被低估的。從 main 返回時,會把一個小整數交回給啟動這支程式的人;依長久以來的慣例,0 代表成功,任何非零值代表某種失敗。那個整數會成為程式的結束狀態,命令列殼層會記住它,像 make 這樣的工具也會讀它來決定是否繼續。所以這支程式做了兩件看得見的事:印出一行字,並回報「一切順利」。
編譯:從文字到可執行檔
你的 hello.c 只是文字——CPU 無法執行文字。因為 C 是編譯式語言,你必須先用編譯器把它翻譯成機器碼,而編譯器正是工具鏈的主角。在多數系統上指令是 gcc 或 clang。在跟你檔案同一個資料夾裡,執行「gcc -Wall -o hello hello.c」來建置,再用「./hello」執行它;你應該會看到 Hello, world! 這一行出現。最後執行「echo $?」,殼層會印出 0。
把那行指令拆開來看。旗標 -o hello 把輸出檔命名為 hello;-Wall 請編譯器打開常見的警告,這是你永遠該要的。結果是一個可執行檔:一個塞滿機器指令的真實檔案,作業系統可以直接載入並執行它。你用 ./hello 執行它——其中 ./ 表示「就在這個目錄裡的這支程式」——它便印出那一行。指令 echo $? 會顯示上一支程式的結束狀態,而那裡正是你 return 0 的那個 0,它撐過了整趟旅程,從程式裡一路傳到了殼層。
編譯器悄悄做了什麼
那單單一行 gcc 指令,藏起了工具鏈好幾個各自分明的階段,每個都在做真實的工作。即使在你深入研究它們之前,知道它們存在也值得,因為當建置失敗時,錯誤訊息通常會指出是哪個階段噎住了。
- 前置處理器處理你的 #include 那一行,把 stdio.h 的內容貼進來,好讓 printf() 在你呼叫它之前就已被宣告。
- 真正的編譯器把你的 C 翻譯成對應你這顆特定 CPU 的低階指令,這是把原始碼轉成機器碼的第一步。
- 組合器把那些指令變成原始的二進位機器碼,並打包成一個目的檔。
- 連結器把你的程式碼、printf() 的函式庫程式碼,以及一小段啟動常式縫在一起,產出最終那個叫 hello 的可執行檔。
當你執行 ./hello 時,那些工具一個都不再參與——工作早已完成,凍結在檔案裡。作業系統把可執行檔載入記憶體,跳到它的啟動程式碼,由啟動程式碼安排好一切,然後呼叫你的 main()。這正是你讀過的那條編譯之路:翻譯的成本只在前頭付一次,之後每一次執行都只是 CPU 在跑寫好的指令,中間沒有任何直譯器擋著。
你將長住其中的那個循環
你剛剛做的這件事有個名字:編輯—編譯—執行循環。你編輯原始碼、編譯它、執行結果、看看發生了什麼,然後再繞一圈。身為系統程式設計師,你幾乎所有時間都待在這個循環裡,不斷收緊它,讓「我有了個想法」到「我看見它跑起來」之間的間隔盡可能短。現在就試試:改掉引號裡的文字,用同一行 gcc 重新編譯,再執行一次。新的字會出現,因為你改了原始碼並重新翻譯了它——舊的可執行檔自己永遠不會變。
在你慶祝之前,先說一句誠實的提醒。C 會很樂意編譯那些以編譯器看不見的方式出錯的程式,而某些錯誤是未定義行為——語言標準對接下來會發生什麼完全不作任何要求。這不代表「它會挑一個隨機而合理的答案」;它代表最佳化器被允許假設那件壞事永遠不會發生,於是一支壞掉的程式今天看似正常、明天卻可能毀損資料。我們的 hello world 是安全的,但請保有你日後會養成的習慣——檢查每一個可能失敗的呼叫的回傳值——你就能一直站在誠實的那一邊。