1 C 程式的四階段生命週期(從原始碼到執行檔)
當我們在電腦上寫好一段 C 語言原始碼(如 main.c)並按下「執行」或「編譯」按鈕時,電腦底層並不是直接看懂英文程式碼,而是必須經過嚴謹的四個轉換步驟:
前處理器負責處理所有以 # 開頭的指令(如展開 #include 標頭檔、進行 #define 純文字替換、過濾條件編譯 #ifdef,並刪除所有註解)。
編譯器檢查 C 語言的文法(語法分析),若有拼錯字或少分號會在此時報出 Syntax Error;若完全正確,則轉為「組合語言(Assembly)」。
組譯器將組合語言翻譯為 CPU 能理解的機器二進位指令碼,產生目的檔(Object File)。
連結器把多個目的檔與 C 語言標準函式庫(如 printf、scanf)綁定結合在一起,最終產出可在作業系統執行的 .exe 執行檔。
想像你要參加一場廚藝競賽:
1. 前處理:助手先把食譜(原始碼)裡的縮寫代號替換成完整食材清單,並撕掉便條紙註記(展開 #include、替換 #define、移除註解)。
2. 編譯:主廚檢查食譜步驟是否合乎料理文法與邏輯(檢查 Syntax Error),並轉化成切菜下鍋的標準動作指令(組合語言)。
3. 組譯:把動作指令變成廚師肌肉記憶的自動化動作(機器碼目的檔 .obj)。
4. 連結:將外叫的現成醬汁包(標準函式庫 stdio.h)與你的料理裝盤打包,產出完整的一道菜(.exe 執行檔)!
#include <stdio.h> // 前處理:把 stdio.h 內容整份貼到這裡
#define MSG "Hello TVE" // 前處理:將所有 MSG 文字替換為 "Hello TVE"
int main(void) {
// 這是單行註解,前處理階段會被乾淨移除
printf("%s\n", MSG); // 展開後等同於 printf("%s\n", "Hello TVE");
return 0;
}
2 前處理器(Preprocessor)核心機制與三大鐵律
前處理器不是編譯器,它只是一個「非常聽話但完全不懂 C 語言語法的文字替換小幫手」。牢記以下三大鐵律,統測前處理器相關考題就能秒殺:
| 三大鐵律 | 正確示範與原理 | 高職生最常踩雷的錯誤寫法 |
|---|---|---|
| 鐵律一: 必以 # 開頭 |
#include <stdio.h>#define MAX 100前處理器只認井字號開頭的指令。 |
❌ define MAX 100少了 #,編譯器會以為是變數宣告而直接報錯! |
| 鐵律二: 結尾「不可加分號」 |
#define PI 3.14#include <math.h>前處理指令不是 C 敘述句,結尾不加分號。 |
❌ #define PI 3.14;若加分號,展開時會連分號一起貼上,導致 2 * PI * r 變成 2 * 3.14; * r 造成編譯崩潰! |
| 鐵律三: 不用等號 = |
#define RATE 0.05巨集名稱與替換值之間以「空白鍵」隔開即可。 |
❌ #define RATE = 0.05若加了等號,展開時會把 = 0.05 整個貼過去,引發語法錯誤! |
請仔細看以下這段震撼示範程式碼:
#include <stdio.h>
#define PI_BAD 3.14; // ❌ 致命錯誤:結尾多打了分號
int main(void) {
double area = 10 * 10 * PI_BAD;
// 經過前處理器文字替換後,編譯器看到的真實程式碼居然是:
// double area = 10 * 10 * 3.14; ;
// 看似沒事?但如果是以下情況:
// double radius = 2 * PI_BAD * 5;
// 展開後變成:double radius = 2 * 3.14; * 5;
// 💥 編譯器看到第二個星號「* 5」前面居然被分號截斷,當場噴出 Syntax Error!
return 0;
}
3 #include 標頭檔引入與防重複引用機制
#include 指令的作用極為單純:「把我指定的檔案內容,完完整整、一字不漏地複製貼上到本檔案中」。
📌 尖括號 < > vs 雙引號 " " 的決定性差異
| 標頭檔語法 | 搜尋路徑順序 | 典型適用時機 |
|---|---|---|
#include <stdio.h>(尖括號) |
直接前往系統標準函式庫目錄(System Include Paths)搜尋。 | 使用 C 語言內建的標準函式庫(如 stdio.h, stdlib.h, math.h, string.h)。 |
#include "my_math.h"(雙引號) |
先從當前專案所在的原始碼目錄(Current Directory)開始找;若找不到,再去系統標準目錄搜尋。 | 引入你自己或專案組員寫的自訂標頭檔(Custom Header Files)。 |
🛡️ 標頭檔防重複引用(Include Guard)
在大型專案中,如果多個 .c 檔都引用了同一個 .h 檔,可能會造成變數或結構體重複定義(Redefinition Error)。標準解決方法是使用條件編譯防護罩:
#ifndef MY_HEADER_H // 如果還沒定義過 MY_HEADER_H
#define MY_HEADER_H // 那麼現在立即定義它
// 這裡放置函式原型宣告、結構體定義等
void sayHello(void);
#endif // MY_HEADER_H // 結束防護區塊
// 現代編譯器也可以在標頭檔第一行寫:
// #pragma once (非標準但被各大主流編譯器普遍支援)
4 #define 巨集常數與常考三大陷阱
#define 建立的不是變數,而是「文字替換巨集(Macro)」。在進入編譯階段前,前處理器會像文書軟體的「全部取代」功能一樣,把所有巨集名稱換成對應的值。
變數在記憶體中有一個專屬的「門牌號碼(記憶體位址)」,所以可以被賦值(如 x = 20;)。
但巨集常數只是純數字字面常數(Literal Constant),沒有記憶體位址!
#include <stdio.h>
#define PI 3.14 // 定義巨集常數 PI
int main(void) {
printf("PI is %f\n", PI); // 展開成 printf("PI is %f\n", 3.14);
PI = 3.14159; // ❌ 致命編譯錯誤!
// 經過前處理器文字替換後,這行程式碼會變成:
// 3.14 = 3.14159;
// 請問:你能把 3.14159 存進數字 3.14 裡面嗎?當然不行!
// 編譯器會噴出:error: lvalue required as left operand of assignment
return 0;
}
5 帶參數巨集(Macro Function)與括號安全防護原則
巨集也可以接收參數,外表長得很像函式,但本質依然是「純文字代入」,絕對不會先幫你計算參數的值!
#include <stdio.h>
// ❌ 危險巨集:參數與整體都沒有括號保護
#define SQUARE_BAD(x) x * x
// ✅ 安全巨集:每個參數與整個運算式都用小括號保護
#define SQUARE_SAFE(x) ((x) * (x))
int main(void) {
int a = 3;
// 我們希望計算 (3 + 1)^2 = 16
int res1 = SQUARE_BAD(a + 1);
// 文字替換展開過程:
// a + 1 * a + 1
// = 3 + 1 * 3 + 1
// = 3 + 3 + 1 = 7 (💥 天差地遠!先乘除後加減導致嚴重邏輯錯誤!)
int res2 = SQUARE_SAFE(a + 1);
// 文字替換展開過程:
// ((a + 1) * (a + 1))
// = ((3 + 1) * (3 + 1))
// = 4 * 4 = 16 (✅ 完全符合預期!)
printf("BAD: %d, SAFE: %d\n", res1, res2);
return 0;
}
「參數加括號,整體加括號;少一個括號,分數全跑掉!」
例如:#define MULTIPLY(a, b) ((a) * (b)),所有出現參數的地方都要包緊!
6 const 唯讀變數 vs #define 巨集大比拼
在 C 語言中要建立「不能修改的常數」,有兩種常見方式:#define 與 const。高職統測非常喜歡考兩者的區別與修復編譯錯誤:
| 比較項目 | #define PI 3.14 (前處理器巨集) |
const double PI = 3.14; (唯讀常數) |
|---|---|---|
| 處理階段 | 前處理階段(編譯前完成文字置換)。 | 編譯階段(由編譯器進行型態分析與記憶體配置)。 |
| 型態安全檢查 | 無型態檢查(純文字,容易引發隱晦錯誤)。 | 具有嚴格的資料型態檢查(如 int, double)。 |
| 記憶體配置 | 不佔用記憶體(只存在程式碼的即值常數中)。 | 佔用記憶體空間(擁有實體記憶體位址)。 |
| 作用域 (Scope) | 全域有效(從定義處開始直到檔尾或 #undef)。 |
遵循變數作用域規則(如在函式內部宣告則只在該區塊有效)。 |
| 能否被修改? | 絕對不能被當作變數賦值(無左值)。 | 被宣告為唯讀,若嘗試賦值(PI = 3.0;)會引發編譯錯誤。 |
#include <stdio.h>
int main(void) {
const int MAX_SCORE = 100; // 宣告為唯讀常數
// MAX_SCORE = 90;
// ❌ 編譯錯誤!error: assignment of read-only variable 'MAX_SCORE'
// 解法一:若數值需要變動,請刪除 const,改為一般變數 int MAX_SCORE = 100;
// 解法二:在宣告時就給定正確初始值。
printf("Max score: %d\n", MAX_SCORE);
return 0;
}
7 條件式編譯(Conditional Compilation)
條件式編譯允許我們在前處理階段,依據巨集是否被定義,決定「哪些程式碼要送給編譯器編譯,哪些程式碼要直接當作垃圾丟掉(不編譯)」。
#include <stdio.h>
#define DEBUG_MODE 1 // 設定 1 開啟除錯訊息,設定 0 關閉
int main(void) {
int x = 50, y = 100;
#if DEBUG_MODE == 1
printf("[除錯日誌] 變數 x 初始化為 %d, y 初始化為 %d\n", x, y);
#endif
int sum = x + y;
#ifdef DEBUG_MODE
printf("[除錯日誌] 正在執行加法運算...\n");
#else
// 若 DEBUG_MODE 未定義時編譯這段
#endif
printf("計算總和為: %d\n", sum);
return 0;
}
[除錯日誌] 正在執行加法運算...
計算總和為: 150
8 程式除錯四大錯誤型態全解構(統測每屆必考!)
寫程式發生錯誤很常見,但在統測中,題目非常喜歡考你:「這個錯誤到底是在哪一個階段被抓出來的?」請務必釐清以下四大分類:
| 錯誤類型名稱 | 發生時間點 | 經典代表案例 | 能否產生執行檔? |
|---|---|---|---|
| 語法錯誤 (Syntax Error / Compile-Time Error) |
編譯階段(由編譯器檢查語法時發現)。 |
• 敘述結尾漏打分號 ;• 大小括號不對稱 { ... )• 拼錯關鍵字(如寫成 prntf、intt)• 嘗試對常數賦值(如 3.14 = x;)
|
❌ 否,編譯失敗中斷 |
| 連結錯誤 (Link-Time Error) |
連結階段(由連結器找不到實體定義時發現)。 |
• 找不到 main() 進入點函式• 呼叫了某個函式卻沒有提供其實作本體 • 同一個變數或函式名稱在不同檔案重複定義 |
❌ 否,無法產出 .exe |
| 執行期錯誤 (Run-Time Error) |
執行階段(程式能成功編譯並執行,但跑一跑當機崩潰)。 |
• 除以 0(Division by zero) • 指標未初始化就存取(野指標存取) • 陣列索引超出界線(Array out of bounds) • 記憶體不足(Out of memory / Stack Overflow) |
✅ 能產出執行檔,但執行時崩潰當機 |
| 邏輯錯誤 (Logic Error / Bug) |
執行完畢後(程式順利執行結束,但算出來的答案是錯的!)。 |
• 計算圓面積公式誤寫成 2 * PI * r• 迴圈終止條件寫錯(如 <= 10 誤寫成 < 10)• 巨集沒加括號導致運算優先順序錯誤 |
✅ 能正常執行,但結果不正確 |
9 111~115 歷屆統測真題全選項深度剖析
以下收錄統測專業科目(二)近年全部 6 題前處理器與編譯相關真題。每題均提供高解析度原始考題掃描圖、互動選項按鈕與所有選項逐項深入解析:
🔍 四個選項逐項深度剖析(為什麼對?為什麼錯?)
.c 原始程式碼必須先經由編譯器轉譯為目的檔(.obj 或 .o),再由連結器(Linker)將目的檔與標準函式庫結合產生可執行檔(如 Windows 下的 .exe),作業系統才能載入執行。因此本敘述完全正確。
#include、#define)是在編譯前由前處理器執行文字替換與檔案引入,它不是 C 語言的敘述句(Statement),結尾「絕對不需要(也不可以)」加上分號。如果寫成 #include <stdio.h>;,前處理器會將分號遺留在原始碼中,引發非預期的語法錯誤(Syntax Error)。因此本選項敘述錯誤,符合題意所求。
main() 是所有標準 C 語言程式的程式進入點(Entry Point)。作業系統啟動程式時,必定最先呼叫並執行 main() 函式中的第一行敘述,直到 main() 執行完畢或遇到 return 為止。因此本敘述完全正確。
// 開頭代表單行註解,編譯器會自動忽略該行後續所有內容;多行註解則使用 /* ... */ 包夾。因此本敘述完全正確。
🔍 四個選項逐項深度剖析
#;② 誤用等號 =(巨集是以空白隔開名稱與置換內容);③ 結尾誤加分號 ;。
# 字號,且結尾多加了分號 ;。若無 #,編譯器會把 define 當作未宣告的變數或型態而報錯。
# 且沒加分號,但誤用了等號 =!若這樣定義,前處理器會把程式碼中所有的 PI 置換為 =3.14,例如 double r = PI; 會被展開成 double r = =3.14;,在編譯時造成嚴重的語法錯誤。
#、常數名稱 PI 與替換值 3.14 之間以空白隔開、不使用等號 =、結尾不加分號 ;。
🔍 四個選項逐項深度剖析
int a = 10 / b;(當執行時變數 b 為 0)在語法結構上完全符合「變數 = 運算式」的規則,能順利通過編譯;但在程式「執行時」,CPU 遇到除數為 0 會觸發硬體例外(Floating-point exception)導致當機,因此「除以 0」屬於典型的執行期錯誤(Run-time error),而非語法錯誤。
int *ptr; 後立即寫 *ptr = 100;,程式在文法上完全合法,編譯器能夠編譯通過;但執行時由於 ptr 指向隨機未知的記憶體區段,寫入會引發 Segmentation Fault(記憶體區段錯誤)導致程式強制終止,這是執行期錯誤(Run-time error),非語法錯誤。
;。若漏打分號,編譯器在編譯期間進行語法剖析時就會直接判定語法違規報錯(Syntax error),根本無法產出可執行檔,因此是編譯期的語法錯誤,而非執行期錯誤。
1: #define Value1 100
2: #define Value2 (Value1 - 1)
3: const int Value3;
4: int CheckValue = 0;
5: Value3 = Value2;
6: CheckValue = Value1 + Value3;
🔍 四個選項逐項深度剖析
(Value1 - 1),行號 2 會變成 #define Value2(空巨集),導致行號 5 展開為 Value3 = ;,反而引發更嚴重的語法錯誤。
Value3 賦值,但 Value3 將維持未初始化的垃圾值,導致行號 6 的加法計算失去意義(邏輯錯誤);而且題目的核心是考 const 關鍵字的唯讀特性。
const 修飾詞將 Value3 宣告為唯讀常數。在 C 語言中,const 變數一旦宣告即不可再被賦值,行號 5 嘗試執行 Value3 = Value2; 必然引發編譯錯誤。只要刪除 const,Value3 就變回一般的 int 變數,行號 5 的賦值動作完全合法,整段程式即可順利編譯通過!
Value2 會變成未宣告的識別字(undeclared identifier),編譯器同樣會報錯。
🔍 程式碼逐步追蹤(Trace)計算過程
| 行號與敘述 | 前處理器文字替換與執行說明 | 各變數數值狀態 |
|---|---|---|
1: #define Value1 100 |
定義巨集 Value1 為 100。 |
Value1 = 100 |
2: #define Value2 (Value1 - 1) |
定義巨集 Value2,展開為 (100 - 1) = 99。 |
Value2 = 99 |
3: int Value3; |
移除 const 後宣告整數變數 Value3。 |
Value3 = 未知 |
4: int CheckValue = 0; |
宣告並初始化 CheckValue 為 0。 |
CheckValue = 0 |
5: Value3 = Value2; |
文字展開為 Value3 = (100 - 1);,計算後存入 99。 |
Value3 = 99 |
6: CheckValue = Value1 + Value3; |
文字展開為 CheckValue = 100 + 99;,計算結果為 199。 |
CheckValue = 199 |
🔍 四個選項誘答力分析
Value2 等於 Value1(即 100 + 100 = 200)而忽略了減 1。Value3 沒加上去,只拿到 Value1 的值。1: #include <stdio.h>
2: #define PI 3.14
3: void main(void){
4: double area1, area2;
5: printf("圓面積area1為 %lf 平方公分.\n", 100*100*PI);
6: PI = 3.14159265358979323846;
7: printf("圓面積area2為 %lf 平方公分.\n", 100*100*PI);
8: }
🔍 四個選項逐項深度剖析
#define PI 3.14 符合前處理指令標準語法(有 #、有空白隔開、無等號、無分號),前處理器會順利記錄巨集符號,此行不會出錯。
area1 與 area2,語法完全正確。
#define PI 3.14 是文字替換巨集,在前處理階段,程式碼中所有的 PI 都會被原封不動置換為數值字面常數 3.14。因此行號 6 在前處理完後,實際上送進編譯器的程式碼是:
3.14 = 3.14159265358979323846;在 C 語言中,賦值運算子
= 的左邊必須是「具有記憶體位址的可修改左值(Modifiable lvalue)」(例如變數)。純數字 3.14 是右值(Rvalue),不能被賦值!編譯器在編譯行號 6 時會立刻噴出錯誤:error: lvalue required as left operand of assignment,故行號 6 發生錯誤。
100*100*3.14,屬於合法的算術運算與輸出,語法本身毫無錯誤。
10 高職段考與模擬精選實戰題(5 題全選項解析)
透過以下 5 題高擬真度題型自我檢測,看看自己是否已經完全掌握本章的所有常考陷阱:
#define CUBE(x) x*x*x,若在 main() 函式中執行 int ans = CUBE(1 + 2);,則 ans 的值為何?
🔍 四個選項逐項深度剖析
1 + 2 * 1 + 2 * 1 + 2依據「先乘除後加減」優先順序:
=
1 + (2 * 1) + (2 * 1) + 2=
1 + 2 + 2 + 2 = 7!
#ifndef 與 #define 搭配,其主要目的為何?
🔍 四個選項逐項深度剖析
malloc/free 等函式負責,與前處理指令無關。🔍 四個選項逐項深度剖析
main(Undefined reference),會產生 Link-Time Error。#include <stdio.h>
#define LEVEL 2
int main(void) {
int score = 100;
#if LEVEL == 1
score += 10;
#elif LEVEL == 2
score += 20;
#else
score += 30;
#endif
printf("%d\n", score);
return 0;
}
🔍 追蹤說明
LEVEL == 2 成立,因此只會將 score += 20; 這行程式碼保留給編譯器,其他分支程式碼全部在前處理階段被剔除。故
score = 100 + 20 = 120。
const int x = 10; 與 #define x 10,下列敘述何者正確?
🔍 四個選項逐項深度剖析
const 是編譯器處理的唯讀變數,具型態安全性與記憶體位址;#define 則是前處理器的文字置換,無型態檢查。const 變數唯讀;#define 沒有左值)。#define 在前處理階段執行,不做型態檢查。const 由編譯器處理,不會在前處理階段被替換。11 考前衝刺 10 秒速記口訣 & 核心知識檢核清單
以
# 開頭、不用等號 =、結尾不加分號 ;。
巨集只是純文字代入,不是變數,沒有記憶體位址,不能被賦值(無左值)。
#define SQR(x) ((x)*(x)),參數加括號、整體加括號!
前處理 (.c→.i) → 編譯 (.i→.s) → 組譯 (.s→.obj) → 連結 (.obj→.exe)。
漏分號/拼錯字 = 語法錯誤(編譯期);除以 0 / 未初始化指標 = 執行期錯誤。
<stdio.h> 找系統庫;"my.h" 先找當前專案目錄。