1. 宏(考察很多)-要求輕松實現宏,很容易出錯
#define 機制包括了一個規定,允許把參數替換到文本中,這種實現通常稱為宏或定義宏。
下面是宏的聲明方式:
#define name(參數列表) 內容
參數列表的左括號必須與name緊鄰,如果兩者間存在空白,參數列表就會被解釋成內容的一部分。
#define SQUARE(x) x*x
這個宏接收一個參數x,如果寫SQUARE(5),預處理器就會用5*5這個表達式替換SQUARE(5)。
但是這個宏存在一個問題:
#define SQUARE(x) x*xint main()
{int a = 5;printf("%d\n", SQUARE(a+1));return 0;
}
我們想象中應該是6×6=36,但是實際結果居然是11,為什么呢?
SQUARE(a+1) 實際上被替換成了 a+1*a+1,并不是(a+1)*(a+1),所以結果是11。
應該在宏定義上加上兩個括號:
#define SQUARE(x) (x)*(x)
如果是這樣一個宏,我們吸取經驗在每個x上加上括號:
#define DOUBLE(x) (x)+(x)int main()
{int a = 5;// printf("%d\n", SQUARE(a+1));printf("%d\n", 10*DOUBLE(a));return 0;
}
我們想象中是10×10=100,但是實際上是55,我們展開替換DOUBLE,實際上是10*(5)+(5)
先算10*5 = 50, 最后再+5,等于55。
所以我們要在外面也加上括號。
#define DOUBLE(x) ((x)+(x))
所以用于對數值表達式進行求值的宏定義都應該用這種方式加上括號,避免在使用宏時由于參數中的操作符或鄰近操作符之間不可預料的相互作用。
當宏參數在宏的定義中出現超過一次時,如果參數帶有副作用,那么在使用這個宏的時候就可能出現危險,導致不可預測的后果。副作用就是表達式求值的時候出現永久性的效果。
x++就是帶有副作用
#define MAX(x, y) ((x)>(y)?(x):(y))int x = 5;int y = 8;int z = MAX(x++, y++);printf("%d %d %d\n", x, y, z);
預處理器處理后的結果是:
z = ((x++) > (y++) ? (x++) : (y++))
?(x++) > (y++) 都會走,走完x=6, y=9,然后走y++,z=9,y=10,最后的結果就是6,10,9。
某筆試題:
寫一個宏,計算結構體中某變量相對于首地址的便宜,并給出說明
#define OFFSET_OF(type, member) ((size_t)&(((type *)0)->member))
說明:
-
type
: 結構體的類型。 -
member
: 結構體中的成員變量。 -
((type *)0)
: 將地址0
強制轉換為指向type
類型的指針。這相當于假設結構體的首地址是0
。 -
&(((type *)0)->member)
: 獲取成員變量member
的地址。由于結構體的首地址是0
,這個地址就是成員變量相對于結構體首地址的偏移量。 -
(size_t)
: 將偏移量轉換為size_t
類型,通常用于表示內存地址或偏移量的大小。
struct example
{int a;char b;double c;
};int main()
{printf("Offset of 'a': %zu\n", OFFSET_OF(struct example, a));printf("Offset of 'b': %zu\n", OFFSET_OF(struct example, b));printf("Offset of 'c': %zu\n", OFFSET_OF(struct example, c));return 0;
}
Offset of 'a': 0
Offset of 'b': 4
Offset of 'c': 8
2. 編譯鏈接的過程(考的不多)
1. 預處理(Preprocessing)
預處理是編譯過程的第一步,主要處理源代碼中的預處理指令(以 #
開頭的指令)。
主要任務:
-
宏展開:將所有的宏定義展開。
-
頭文件包含:將
#include
指定的頭文件內容插入到源文件中。 -
條件編譯:根據
#if
、#ifdef
等條件編譯指令,選擇性地包含或排除代碼。 -
刪除注釋:刪除源代碼中的注釋。
輸入輸出:
-
輸入:
.c
源文件。 -
輸出:
.i
預處理后的文件。
gcc -E main.c -o main.i
2. 編譯(Compilation)
編譯階段將預處理后的代碼轉換為匯編代碼。
主要任務:
-
詞法分析:將源代碼分解為 token(如關鍵字、標識符、運算符等)。
-
語法分析:根據語法規則構建抽象語法樹(AST)。
-
語義分析:檢查語義是否正確(如類型檢查)。
-
代碼優化:對代碼進行優化。
-
生成匯編代碼:將高級語言代碼轉換為目標機器的匯編代碼。
輸入輸出:
-
輸入:
.i
預處理后的文件。 -
輸出:
.s
匯編文件。
gcc -S main.i -o main.s
3. 匯編(Assembly)
匯編階段將匯編代碼轉換為機器代碼(目標文件)。
主要任務:
-
將匯編代碼翻譯為機器指令。
-
生成目標文件(通常是
.o
或.obj
文件),包含機器代碼和符號表。
輸入輸出:
-
輸入:
.s
匯編文件。 -
輸出:
.o
目標文件。
gcc -c main.s -o main.o
4. 鏈接(Linking)
鏈接階段將多個目標文件和庫文件合并為一個可執行文件。
主要任務:
-
符號解析:解析目標文件中的未定義符號(如函數和變量)。
-
地址分配:為代碼和數據分配最終的內存地址。
-
重定位:根據最終的內存地址調整代碼中的引用。
-
合并目標文件:將多個目標文件合并為一個可執行文件。
-
鏈接庫文件:將靜態庫或動態庫鏈接到可執行文件中。
輸入輸出:
-
輸入:
.o
目標文件和庫文件。 -
輸出:可執行文件(如
a.out
或main.exe
)。
gcc main.o -o main