C語言-標頭檔與前置處理器

簡介

這一個章節要介紹如何撰寫標頭檔,以及如何使用前置處理器

前置處理是在程式編譯之前所做的事,包括引入標頭檔、定義符號常數以及巨集

所有前置處理都是由井字號’#’開始

標頭檔與實作檔

首先我們要先了解程式從撰寫到執行的過程,請看下圖

程式執行過程

程式原始碼在經過編譯器(Compiler)編譯之後會成為目的檔(Object file)

然後多個目的檔透過連結器(Linker)連結之後會形成可執行檔(Executable file)

之後可執行檔經過載入器(Loader)載入到記憶體中執行

當程式越來越龐大的時候,我們會為了保持彈性將單一功能切割為標頭檔跟實作檔

實作檔編譯成目的檔,要使用時再連結起來就行

這樣以後只要有錯誤只要處理小部分的程式

而標頭檔則提供介面,讓使用這段功能的人不用了解實作也能使用程式

以後實作檔改變了,只要重新編譯實作檔然後再連結一次就可以更改程式了

提高程式撰寫的彈性,而連結又分為靜態跟動態連結

這時候就要進入工商時間(推坑時間)

可以找程式設計師的自我修養:連結、載入、程式庫這本書來看

可以更了解程式如何連結與執行

詳細可以參考這個連結

以下會寫一個標頭檔,一個實作檔,一個執行檔當作範例

標頭檔 calc.h

1
2
3
//宣告函數原型
int add(int a,int b);
int sub(int a,int b);

實作檔 calc.c

1
2
3
4
5
6
7
8
9
10
#include "calc.h"
//實作函數
int add(int a,int b){
return a+b;
}
int sub(int a,int b){
return a-b;
}

執行檔 main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
#include "calc.h"
int main() {
int a = 5,b = 3;
printf("a = %d\n",a);
printf("b = %d\n",b);
printf("add(a,b) = %d\n",add(a,b));
printf("sub(a,b) = %d\n",sub(a,b));
return 0;
}

執行結果

a = 5
b = 3
add(a,b) = 8
sub(a,b) = 2

這邊要注意的是,如果是用箭頭<>括起來的標頭檔

會從系統預設的路徑去找標頭檔

而用雙引號””括起來的標頭檔,系統會到被編譯的檔案所在的目錄去找

#define

#define的語法如下

1
#define 識別字 代換文字

#define語法會在編譯過程中,將所有識別字都替換為代換文字,例如

1
#define PI 3.14159

之後整個程式只要看到’PI’就會自動被替換成3.14159,例如

1
2
3
4
double area(double radius){
return radius * radius * PI;
//會被代換成 radius * radius * 3.14159;
}

注意這不是宣告常數,所以不要在代換文字前面加上等號’=’

以下是範例程式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#define PI 3.14159
double area(double);
int main() {
double r;
printf("請輸入半徑:");
scanf("%lf",&r);
printf("圓面積:%f\n",area(r));
return 0;
}
double area(double radius){
return radius * radius * PI;
//會被代換成 radius * radius * 3.14159;
}

執行結果

請輸入半徑:4
圓面積:50.265440

巨集是由#define所定義的運算,跟符號常數一樣都是在編譯的時候代換文字

不同的是巨集可以有引數,以下是計算圓面積的巨集

1
2
#define PI 3.14159
#define AREA(r) ((PI * (x) * (x)))

之後只要寫到AREA()就會自動展開為以下算式

1
2
double area = AREA(4)
// 會被替換成 double area = (3.14159 * (4) * (4))

這邊要小心的是括號問題

如果沒有寫好括號,寫成((PI x x)),會展開成以下結果

1
2
double area = AREA(4 + 2)
// 會被替換成 double area = (3.14159*4 + 2*4 + 2)

所以將引數用括號括起來是非常重要的

如果要代換的文字太長,可以使用反斜線\表示下一行還有代換文字

以下是兩整數交換寫成巨集的範例

1
2
3
#define SWAP(a,b) int c = a; \
a = b; \
b = c;

這邊要強調的是,巨集並不是函數,只是在編譯過程中,將文字取代成代換文字

不使用巨集跟常數符號的時候,可以使用#undef來解除定義

條件編譯

條件編譯可以讓設計者控制哪段程式要怎麼編譯

例如不同作業系統就編譯不同的程式碼,這樣就不用分開成兩個檔案去編譯了

以下是常看得的NULL的條件編譯

1
2
3
#if !defined(NULL)
#define NULL 0
#endif

以上判斷NULL是否曾經定義為符號常數,若沒有就定義NULL為0

也可以簡寫成

1
2
3
#ifndef NULL
#define NULL 0
#endif

只要是條件編譯都需要用#endif做結尾

另外還有指令#if defined(名稱),可以簡寫成#ifdef

而如果有多個條件可以使用#elif跟#else,就像if-else一樣

條件編譯常用的用途可以在標頭檔前面看到

這邊再改寫上面標頭檔的實作當作範例

1
2
3
4
5
6
7
8
#ifndef _CALC_H_
#define _CALC_H_
//宣告函數原型
int add(int a,int b);
int sub(int a,int b);
#endif

實作檔 calc.c

1
2
3
4
5
6
7
8
9
10
#include "calc.h"
//實作函數
int add(int a,int b){
return a+b;
}
int sub(int a,int b){
return a-b;
}

執行檔 main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
#include "calc.h"
int main() {
int a = 5,b = 3;
printf("a = %d\n",a);
printf("b = %d\n",b);
printf("add(a,b) = %d\n",add(a,b));
printf("sub(a,b) = %d\n",sub(a,b));
return 0;
}

為什麼這樣寫是為了避免重複引入,例如A標頭檔有引入stdio

B標頭檔也有引入stdio,如果沒有使用條件編譯

這樣會在編譯過程中產生重複宣告的錯誤

所以為了避免重複宣告,利用條件編譯,只要有定義過就不用再定義

以避免錯誤

參考

  1. C語言
  2. C 語言快速導覽 - 標頭檔
  3. C 語言初學教材 - 第六章 設計自己的標頭檔
  4. Re: [問題] 環境設定
  5. Re: [問題] C語言—要怎麼寫標頭檔阿??