C語言小結--前處理器
1 什麼是預處理
編譯一個C程式涉及很多步驟,其中第一個步驟稱為預處理(preprocessing)階段。C前處理器在原始碼編譯之前對其進行一些文字性質的操作。它的主要任務包括刪除註釋、插入被#include指令包含的檔案的內容、定義和替換#define指令定義的符號以及確認程式碼部分內容是否應該根據一些條件編譯指令進行編譯。
1.2 預定義符號
預定義符號是由前處理器定義的符號,他們的值或者是字串常量、或者是十進位制數字常量。__FILE__
和__LINE__
在確認除錯輸出來源方面很有用處。__DATA__
和__TIME
常常用來在編譯程式中加入版本資訊。__STDC__
下面我們介紹一下,如何善用__FILE__
和__LINE__
來除錯程式。
#include <stdio.h>
#define __DEBUG
#ifdef __DEBUG
#define DEBUG(format,...) printf("[File: "__FILE__", Line: %05d] "format"\n", __LINE__, ##__VA_ARGS__)
#else
#define DEBUG(fmt, ...)
#endif
int main()
{
DEBUG("%s, %d " , "hello world!", 100);
return 0;
}
執行結果如下:
root@ubuntu:/mymnt# gcc test2.c
root@ubuntu:/mymnt# ./a.out
[File: test2.c, Line: 00013] hello world!, 100
我們簡單的分析一下這句:#define DEBUG(format,...) printf("[File: "__FILE__", Line: %05d] "format"\n", __LINE__, ##__VA_ARGS__)
這句定義是用後面的printf來替換DEBUG,首先"[File: "
__FILE__
在前處理器中被替換為當前的檔案資訊,也是一個字串,", Line: %05d] "
當做一個字串輸出,format
替換DEBUG中的format
,"\n"
做為一個字串輸出。到此相當於printf中前半部分完成了。__LINE__
替換前面的格式化引數%d
,##__VA_ARGS__
替換...
。
1.3 預處理指令
在C中,#define
指令把一個符號名與一個任意的字元序列聯絡在一起。例如,這些字元可能是一個字元值常量、表示式或者程式語句。這些序列到該行的末尾結束。如果序列過長,可以把他們分開數行,但是需要在除最後一行之外的其他行末尾加一個反斜槓,表示連線。巨集就是一個被定義的序列,他的引數值將被替換。當一個巨集被呼叫時,他的每一個引數值都被具體的值替換,為了防止可能出現於表示式中與巨集有關的錯誤,在巨集完整定義的兩邊應該加上括號。如:#define NUM (300)
。同樣,在巨集定義的每個引數兩邊也要加上括號。如:#define SQUARE(n) ((n)*(n))
。 #define
指令可以__重寫__C語言,使其看上起更像是其他語言,但是不建議讀者進行這種操作。
#argument
結構由前處理器轉換為字串常量“argument” 、 ##
操作符用於把兩邊的文字貼上成同一個識別符號。
有些任務使用巨集也可以實現使用函式也同樣可以實現。但是,巨集與型別無關,這是一個優點。巨集的執行速度快於函式,因為他不存在函式呼叫/返回的開銷。但是,使用巨集通常會增加程式的長度,而函式卻不會。同樣,具有副作用的引數可以能在巨集的使用過程中產生不可預知的結果,而函式的錯誤更容易預測。由於這些區別,使用一種命名約定,讓程式設計師更容易判斷一個識別符號是函式還是巨集是非常重要的。所以一般情況下,巨集定義都使用大寫字元表示。
在許多編譯器中,符號可以從命令列定義。所以在命令列中輸入不同的符號表示不同的編譯過程。#undef
指令將導致一個名字的原來定義被忽略。
使用條件編譯,你可以從一組單一的原始檔建立程式的不同版本,#if
指令根據編譯時的測試結果,包含或者忽略一個序列的程式碼。當同時使用#elif 和 #else
指令時,你可以從幾個序列的程式碼中選擇其中之一進行編譯。除了測試常量表達式之外,這些指令還可以測試某幾個符號是否已經被定義。#ifdef 和 #ifndef
也可以執行這個任務。
#include
指令用於實現檔案包含。它具有兩種形式。如果檔名位於一對見括號中,如:#include<stdio.h>
,編譯器將在編譯器定義的標準位置查詢這個檔案。這種形式通常用於包含函式庫的標頭檔案時。另一種方式,檔名出現在一對雙引號中。如#include "touch.h"
,不同的編譯器可以使用不同的方式處理這種形式。但是,如果用於處理本地標頭檔案的任務特殊處理方法無法找到這個標頭檔案時,編譯器會使用標準查詢過程來尋找他。這種形式通常用於包含你自己編寫的標頭檔案。檔案包含可以巢狀,但是很少需要進行超過一層或者兩層的檔案包含巢狀。巢狀的包含檔案將會增加多次包含同一檔案的危險,而且我們更難以確定某個特定的原始檔依賴的究竟是哪個標頭檔案。
#error
指令在編譯時產生一條錯誤資訊,資訊中包含的是你選擇的文字。#line
指令允許你告訴編譯器下一行輸入的行號,如果他加上了可選內容,它將告訴編譯器輸入原始檔的名字。因編譯器而異的#progma
指令允許編譯器提供不標準的處理過程。比如向函式插入內聯的彙編程式碼(AVR的中斷處理函式前就使用的#progma
指令)。
1.4 總結
1 不要在一個巨集定義的末尾加上分號,使其成為一條完整的語句。
2 在巨集定義中使用引數,不要忘了在他們周圍加上括號,防止歧義。
3 整個巨集定義的兩邊不要忘了加上括號
4 避免使用#define
指令定義可以使用函式實現的很長序列的程式碼
5 #define
巨集定義的字元全部都用大寫字元來實現,防止很函式名發生歧義
6 標頭檔案只應該包含一組函式和資料的宣告
7 不同集合的宣告分離到不同的標頭檔案中可以改善資訊隱藏
8 避免使用巢狀的#include
檔案,否則我們很難判斷原始檔之間的依賴關係