學習目標

  • 了解 C 語言的歷史背景與設計哲學,知道 C 為何至今仍是系統程式設計的核心語言。
  • 理解完整的編譯流程:前置處理(preprocessing)→ 編譯(compilation)→ 組譯(assembly)→ 連結(linking)。
  • 能逐行解釋經典的 hello world 程式,理解 #include、main、printf 各自的角色。
  • 學會使用編譯器選項 -Wall -Wextra -std=c11,養成「零警告」的開發習慣。
  • 能在 macOS / Linux 終端機獨立完成「編寫 → 編譯 → 執行」的完整循環。

對照 K&R 章節

  • K&R《The C Programming Language, 2nd Edition》§1.1 Getting Started
  • 補充:附錄 A(參考手冊)中關於翻譯階段(translation phases)的描述

觀念講解

1. C 語言簡史與設計哲學

C 語言由 Dennis Ritchie 於 1972 年前後在貝爾實驗室(Bell Labs)開發,最初的目的是重寫 UNIX 作業系統。 它的前身是 Ken Thompson 的 B 語言,而 B 又源自 BCPL。1978 年 Kernighan 與 Ritchie 出版了第一版 《The C Programming Language》(俗稱 K&R),1988 年的第二版對應 ANSI C(C89)標準,也就是本教程的主軸。

C 的設計哲學可以濃縮為幾句話:

  • 信任程式設計師:語言不會擋你做危險的事,效率優先。
  • 貼近硬體:C 的型別與運算幾乎直接對應到機器指令,因此常被稱為「可攜的組合語言」。
  • 小而精的語言核心:語言本身很小,功能(如輸入輸出)由「函式庫」提供。

之後的標準演進:C89/C90 → C99 → C11(本教程使用)→ C17 → C23。

2. 第一支程式:hello world 逐行解析

#include <stdio.h>              /* 引入標準輸入輸出函式庫的宣告 */

int main(void)                  /* 程式進入點:作業系統從 main 開始執行 */
{
    printf("hello, world\n");   /* 呼叫函式庫函式 printf 印出字串 */
    return 0;                   /* 回傳 0 給作業系統,代表正常結束 */
}

逐行說明:

程式碼 說明
#include <stdio.h> 前置處理指令。把標頭檔 stdio.h 的內容「貼」進來,讓編譯器知道 printf 的原型(prototype)。
int main(void) 每支 C 程式都必須有一個 main 函式。int 表示它回傳整數給作業系統;void 表示不接受參數。
{ ... } 大括號圍出函式本體(block),內含一連串敘述(statements)。
printf("hello, world\n"); printf 是「格式化列印」函式;\n 是跳脫序列,代表換行字元。敘述以分號 ; 結尾。
return 0; 將結束狀態(exit status)回傳給作業系統:0 代表成功,非零代表錯誤。

注意:main 的正確寫法是 int main(void) 或 int main(int argc, char *argv[])。 初學者常見的 void main() 並不是標準寫法,請避免。

3. 編譯流程四階段

當你執行 cc hello.c 時,背後其實經歷了四個階段:

階段 工具 輸入 → 輸出 做的事
1. 前置處理(Preprocessing) 前置處理器 cpp .c → 展開後的原始碼 處理 #include、#define、#ifdef,移除註解
2. 編譯(Compilation) 編譯器本體 原始碼 → 組合語言 .s 語法/語意分析、最佳化、產生組合語言
3. 組譯(Assembly) 組譯器 as .s → 目的檔 .o 把組合語言翻成機器碼(object file)
4. 連結(Linking) 連結器 ld .o + 函式庫 → 可執行檔 把 printf 等函式庫程式碼與你的程式接在一起

你可以用選項觀察各階段的中間產物:

cc -E hello.c -o hello.i    # 只做前置處理(看看 stdio.h 展開後有多大!)
cc -S hello.c -o hello.s    # 產生組合語言
cc -c hello.c -o hello.o    # 產生目的檔(不連結)
cc hello.o -o hello         # 連結成可執行檔

4. 編譯器選項:養成零警告的習慣

本教程所有程式一律使用以下指令編譯:

cc -std=c11 -Wall -Wextra -o 程式名 原始碼.c
選項 意義
-std=c11 指定使用 C11 標準,避免不同編譯器的預設標準不一致
-Wall 開啟「常見且幾乎都是 bug」的警告(名字叫 all 其實不是全部)
-Wextra 再開啟一批額外警告,例如「未使用的參數」
-o 名稱 指定輸出檔名;不加的話預設輸出 a.out

警告不是錯誤,但幾乎每個警告都在指出潛在的 bug。 本教程要求所有範例 「零錯誤、零警告」通過編譯,請把這當成基本紀律。

在 macOS 上,cc 實際上是 Apple clang;在多數 Linux 上,cc 是 gcc。 兩者對本教程的程式行為一致。

5. 執行程式與結束狀態

./hello          # 執行目前目錄下的 hello
echo $?          # 印出上一個程式的結束狀態(return 0 → 印出 0)

./ 表示「目前目錄」,因為安全考量,shell 預設不會在目前目錄找執行檔。 main 的 return 0 就是 echo $? 看到的數字,這是程式與作業系統溝通成敗的管道。

範例程式

檔案 內容 編譯與執行
01_hello.c 經典 hello world 與變化版:多行輸出、跳脫序列示範 cc -std=c11 -Wall -Wextra -o 01_hello 01_hello.c && ./01_hello
02_compile_stages.c 用程式本身示範前置處理(#define、__FILE__、__LINE__ 等預定義巨集)與編譯期資訊 cc -std=c11 -Wall -Wextra -o 02_compile_stages 02_compile_stages.c && ./02_compile_stages
03_exit_status.c 示範 return 與結束狀態、main 的正確簽名、簡單的算術輸出 cc -std=c11 -Wall -Wextra -o 03_exit_status 03_exit_status.c && ./03_exit_status; echo $?

常見錯誤與陷阱

陷阱 1:忘記分號

printf("hello, world\n")    /* 錯誤:少了分號 */
return 0;

編譯器會報 expected ';'。注意:錯誤訊息指到的行號常常是「下一行」, 因為編譯器讀到下一行才發現分號不見了。修正:每個敘述以 ; 結尾。

陷阱 2:void main() 或省略回傳型別

void main()          /* 不標準:main 必須回傳 int */
main()               /* C89 允許省略回傳型別,C11 不允許 */

修正:一律寫 int main(void)。這不只是風格問題——作業系統確實會讀取 main 的回傳值作為程式的結束狀態。

陷阱 3:字串跨行直接換行

printf("hello,
world\n");           /* 錯誤:字串常值不能直接跨行 */

修正方式有兩種:

printf("hello,\nworld\n");          /* 用 \n 表達換行 */
printf("hello, "
       "world\n");                  /* 相鄰字串常值會自動串接 */

陷阱 4:把警告當耳邊風

int main(void)
{
    int x;                   /* -Wall 會警告:x 未初始化就使用 */
    printf("%d\n", x);
    return 0;
}

未初始化變數的值是垃圾值(indeterminate),每次執行可能不同。 看到警告請立刻修正,而不是等程式行為異常才回頭找。

練習題

  1. 基本題:修改 hello world,讓它印出三行文字:你的名字、就讀學校/公司、一句座右銘。每行結尾都要換行。
  2. 跳脫序列:寫一支程式,只用一個 printf,印出包含雙引號與反斜線的句子:She said: "C:\ is a path."(提示:\" 與 \\)。
  3. 觀察編譯階段:對你的 hello world 依序執行 cc -E、cc -S、cc -c,觀察 .i、.s、.o 檔案。回答:hello.i 大約有幾行?為什麼比原始碼大這麼多?
  4. 結束狀態:寫一支程式 return 42;,執行後用 echo $? 驗證。再試試 return 256;,觀察並解釋看到的數字(提示:結束狀態只有 8 位元)。
  5. 故意製造錯誤:分別移除 #include <stdio.h>、刪掉一個大括號、拼錯 printf 為 pritnf,各編譯一次,記下每種錯誤訊息。學會「讀懂錯誤訊息」是除錯的第一步。
  6. 挑戰題:查詢 man 3 printf(或 man printf),找出 %s 與 %d 的意義,寫一支程式印出 My favorite number is 7.,其中 7 必須用 %d 帶入。

延伸閱讀

  • K&R 2/e,§1.1;Introduction(了解本書寫作背景)
  • Dennis Ritchie,〈The Development of the C Language〉(HOPL II, 1993)—— C 語言起源的第一手資料
  • man cc、man clang:查閱編譯器選項完整清單
  • GCC/Clang 文件中 -Wall 實際包含的警告清單——理解每個警告在防什麼
  • 下一單元預告:變數與基本型別,程式終於要開始「記住」資料了