預處理最大的標志便是大寫,雖然這不是標準,但請你在使用的時候大寫,為了自己,也為了后人。
預處理器在一般看來,用得最多的還是宏,這里總結(jié)一下預處理器的用法。
#include <stdio.h>
#define MACRO_OF_MINE
#ifdef MACRO_OF_MINE
#else
#endif
上述五個預處理是最??匆姷?,第一個代表著包含一個頭文件,可以理解為沒有它很多功能都無法使用,例如C語言并沒有把輸入輸入納入標準當中,而是使用庫函數(shù)來提供,所以只有包含了stdio.h
這個頭文件,我們才能使用那些輸入輸出函數(shù)。
#define
則是使用頻率第二高的預處理機制,廣泛用在常量的定義,只不過它和const
聲明的常量有所區(qū)別:
#define MAR_VA 100
const int Con_va = 100;
...
/*定義兩個數(shù)組*/
...
for(int i = 0;i < 10;++i)
{
mar_arr[i] = MAR_VA;
con_arr[i] = Con_va;
}
MAR_VA
可以用于數(shù)組維數(shù),而Con_va
則不行Con_va
將只有一分真跡,而MAR_VA
則會有n
份真跡(n為使用的次數(shù))
剩下三個則是在保護頭文件中使用頗多。幾個比較實用的用于調(diào)試的宏,由C語言自帶
__LINE__和__FILE__
用于顯示當前行號和當前文件名__DATE__和__TIME__
用于顯示當前的日期和時間__func__
(C99)
用于顯示當前所在外層函數(shù)的名字上述所說的五種宏直接當成值來使用即可。
__STDC__
如果你想檢驗你現(xiàn)在使用的編譯器是否遵循ISO標準,用它,如果是他的值為1。
printf("%d\n", __STDC__);
輸出: 1
如果你想進一步確定編譯器使用的標準版本是C99還是C89可以使用__STDC__VERSION__
,C99(199901)
printf("%d\n", __STDC_VERSION__);
輸出: 199901
可能很多人對這些宏沒什么感觸,實際上一般的確是用不到,但是:
當你在寫一些隱晦的東西時
volatile int x = 10;
你試試把這個代碼用
-std=c99
編譯一下,如果不出意外應該是出錯的在 ISO 標準里,
volatile
是用__volatile__
來實現(xiàn)的,這個對GCC
,Clang
,Visual C++
而言都是如此 除此之外還有許多,有待你們自己發(fā)掘。
#define
預處理器一般只對同一行定義有效,但如果加上反斜杠,也能一直讀取下去
#define err(flag) \
if(flag) \
printf("Correctly")
可以看出來,并沒有在末尾添加;
,并不是因為宏不需要,而是因為,我們總是將宏近似當成函數(shù)在使用,而函數(shù)調(diào)用之后總是需要以;
結(jié)尾,為了不造成混亂,于是在宏定義中我們默認不添加;
,而在代碼源文件中使用,防止定義混亂。
預處理同樣能夠帶來一些便利
#define SWAP1(a, b) (a += b, b = a - b, a -= b)
#define SWAP2(x, y) {x ^= y; y ^= x; x ^= y}
引用之前的例子,交換兩數(shù)的宏寫法可以有效避免函數(shù)開銷,由于其是直接在調(diào)用處展開代碼塊,故其比擬直接嵌入的代碼。但,偶爾還是會出現(xiàn)一些不和諧的錯誤,對于初學者來說:
int v1 = 10;
int v2 = 20;
SWAP1(v1, v2);
SWAP2(v1, v2);//報錯
對于上述代碼塊的情況,為什么SWAP2
報錯?對于一般的初學者來說,經(jīng)常忽略諸如,
goto
do...while
等少見關(guān)鍵字用法,故很少見SWAP1
的寫法,大多集中于SWAP2
的類似錯誤,錯就錯在{}
代表的是一個代碼塊,不需要使用;
來進行結(jié)尾,這便是宏最容易出錯的地方
宏只是簡單的將代碼展開,而不會做任何處理
對于此,即便是老手也常有失足,有一種應用于單片機等地方的C語言寫法可以在此借鑒用于保護代碼:
#define SWAP3(x ,y) do{ \
x ^= y; y ^= x; x ^= y; \
}while(0)
如此便能在代碼中安全使用花括號內(nèi)的代碼了,并且如之前所約定的那樣,讓宏的使用看起來像函數(shù)。
但正所謂,假的總是假的,即使宏多么像函數(shù),它依舊不是函數(shù),如果真的把它當成函數(shù),你會在某些時候錯的摸不著頭腦,還是一個經(jīng)典的例子,比較大小:
#define CMP(x, y) (x > y ? x : y)
...
int x = 100, y = 200;
int result = CMP(x, y++);
printf("x = %d, y = %d, result = %d\n", x, y, result);
執(zhí)行這部分代碼,會輸出什么呢?
答案是,不知道!至少result
的值我們無法確定,我們將代碼展開得到
int result = (x > y++ ? x : y++);
看起來似乎就是y
遞增兩次,最后result
肯定是200
。真是如此?C語言標準對于一個確定的程序語句中,一個對象只能被修改一次,超過一次那么結(jié)果是未定的,由編譯器決定,除了三目操作符?:
外,還有&&
, ||
或是,
之中,或者函數(shù)參數(shù)調(diào)用,switch控制表達式,for里的控制語句
由此可看出,宏的使用也是有風險的,所以雖然宏強大,但是依舊不能濫用。
對于宏而言,前面說過,它只是進行簡單的展開,這有時候也會帶來一些問題:
#define MULTI(x, y) (x * y)
...
int x = 100, y = 200;
int result = MULTI(x+y, y);
看出來問題了吧?展開之后會變成:
int result = x+y * y;
完全違背了當初我們設計時的想法,一個比較好的修改方法是對每個參數(shù)加上括號:
#define MULTI(x, y) ((x) * (y))
如此,展開以后:
int result = ((x+y) * (y));
這樣能在很大程度上解決一部分問題。
如果對自己的宏十分自信,可以嵌套宏,即一個表達式中使用宏作為宏的參數(shù),但是宏只展開這一級的宏,對于多級宏另有辦法展開
int result = MULTI(MULTI(x, y), y);
展開成:int result = ((((x) * (y))) * (y));
實際上,并不要太追求用宏去替換函數(shù),例如這個交換函數(shù),老老實實寫函數(shù),有時候比宏更好
由于我們并不明白,在某些情況下宏是否被定義了,所以我們可以使用一些預處理保護機制來防止錯誤發(fā)生
#ifndef MY_MACRO
#define MY_MACRO 10000
#endif
如果定義了MY_MACRO
那就不執(zhí)行下面的語句,如果沒定義那就執(zhí)行。
在宏的使用中有兩個有用的操作符,姑且叫它操作符#
, ##
對于#
我們可以認為#
操作符的作用是將宏參數(shù)轉(zhuǎn)化為字符串。
#define HCMP(x, y) printf(#x" is equal to" #y" ? %d\n", (x) == (y))
...
int x = 100, y = 200;
HCMP(x, y);
展開以后
printf("x is equal to y ? %d\n", (100) == (200));
注:可以自行添加編譯器選項,來查看宏展開之后的代碼,具體可以查詢GCC
的展開選項,這里不再詳述。特別是在多層宏的嵌套使用情況下,但是我不太推薦,故不做多介紹。
舉一個典型的例子,__LINE__
和 __FILE__
的使用。
/* 下方會說到的 # 預處理指示器,這里先用,實在看不懂,可以自己動手嘗試 */
#define WHERE_AM_I #__LINE__ " lines in " __FILE__
...
fputs(WHERE_AM_I, stderr);
這樣能工作嗎?如果能我還講干嘛。
/* 常理上這應該能工作,但是編譯器非說這錯那錯的 */
/* 好在有前人踏過了坑,為我們留下了解決方案 */
#define DEPAKEGE(X) #X
#define PAKEGE(X) DEPAKEGE(X)
#define WHERE_AM_I PAKEGE(__LINE__) " lines in " __FILE__
...
fputs(WHERE_AM_I, stderr);
不要問我為什么,因為我也不知道C預處理器的真正工作機制是什么。
第一次看見這種解決方案是在 Windows 核心編程 中,這本書現(xiàn)在還能給我許多幫助,雖然已經(jīng)漸漸淡出了書架
總結(jié)起來,即將宏參數(shù)放于#
操作符之后便由預處理器自動轉(zhuǎn)換為字符串常量,轉(zhuǎn)義也由預處理器自動完成,而不需要我們自行添加轉(zhuǎn)義符號。
對于##
它實現(xiàn)的是將本操作符兩邊的參數(shù)合并成為一個完整的標記,但需要注意的是,由于預處理器只負責展開,所以程序員必須自己保證這種標記的合法性,這里涉及到一些寫法問題,都列出來
#define MERGE(x, y) have_define_ ## x + y
#define MERGE(x, y) have_define_##x + y
...
result = MERGE(1, 3);
這里首先說明,上述寫法由于習慣原因,我使用第二種,但是無論哪種都無傷大雅,效果一樣。上述代碼展開以后是什么呢?
result = have_define_1 + 3;
在我看來,這就有點C++
中模版的思想了,雖然十分原始,但是總是有了一個方向,憑借這種方法我們能夠使用宏來進行相似卻不同函數(shù)的調(diào)用,雖然我們可以使用函數(shù)指針數(shù)組來存儲,但需要提前知曉有幾個函數(shù),并且如果要實現(xiàn)動態(tài)增長還需要消耗內(nèi)存分配,但宏則不同。
inline int func_0(int arg_1, int arg_2) { return arg_1 + arg_2; }
inline int func_1(int arg_1, int arg_2) { return arg_1 - arg_2; }
inline int func_2(int arg_1, int arg_2) { return arg_1 * arg_2; }
inline int func_3(int arg_1, int arg_2) { return arg_1 / arg_2; }
#define CALL(x, arg1, arg2) func_##x(arg1, arg2)
...
printf("func_%d return %d\n",0 ,CALL(0, 2, 10));
printf("func_%d return %d\n",1 ,CALL(1, 2, 10));
printf("func_%d return %d\n",2 ,CALL(2, 2, 10));
printf("func_%d return %d\n",3 ,CALL(3, 2, 10));
十分簡便的一種用法,在我們增加減少函數(shù)時我們不必考慮如何找到這些函數(shù)只需要記下每個函數(shù)對應的編號即可,但還是那句話,不可濫用。
#define CAT(temp, i) (cat##i)
//...
for(int i = 0;i < 5;++i)
{
int CAT(x,i) = i*i;
printf("x%d = %d \n",i,CAT(x,i));
}
對于宏,在使用時一定要注意,宏只能展開當前層的宏,如果你嵌套使用宏,即將宏當作宏的參數(shù),那么將導致宏無法完全展開,即作為參數(shù)的宏只能傳遞名字給外部宏
#define WHERE(value_name, line) #value_name #line
...
puts(WHERE(x, __LINE__)); //x = 11
輸出: 11__LINE__
對于其他的預編譯器指令,如:#pragma, #line, #error
和各類條件編譯并不在此涉及,因為使用上并未有陷阱及難點。
C和C++混合編程的情況
extern "C"
這樣的身影,這是做什么的?這是為了混合編程而設計的,常出現(xiàn)在 C++的源代碼中,目的是為了讓 C++能夠成功的調(diào)用 C 的標準或非標準函數(shù)。
#if defined(__cplusplus) || defined(_cplusplus)
extern "C" {
#endif
/**主體代碼**/
#if defined(__cplusplus) || defined(_cplusplus)
}
#endif
這樣就能在C++中調(diào)用C的代碼了。
還有一種可以被稱之為宏的小應用的技巧
其實很簡單
#define TEST_RET(val, continues) ({continues = 19;val = 11;})
...
{
__attribute__((unused)) int oldval = 10;
__attribute__((unused)) int newval = 18;
fprintf (stderr, "%d\n", TEST_RET(oldval, newval));
}
({})
包裹你想要的東西。舉個例子最好說明問題
Linux
內(nèi)核的鏈表原型。所謂的
Linux
內(nèi)核的鏈表原型 就是在內(nèi)核編程中使用的鏈表數(shù)據(jù)結(jié)構(gòu),我以它為例子,自己寫了一個插入操作
#define _list_add_inner(_add_pos, _add_node) \
do {\
(_add_node)->next = (_add_pos)->next;\
(_add_node)->prev = (_add_pos);\
(_add_pos)->next->prev = (_add_node);\
(_add_pos)->next = (_add_node);\
} while(0)
static inline void list_add_after(struct list * add_pos, struct list * add_node) {
_list_add_inner(add_pos, add_node);
}
static inline void list_add_before(struct list * add_pos, struct list * add_node) {
_list_add_inner(add_pos->prev, add_node);
}
list_add_after
,list_add_before
看看是否達到預期目的?有時候代碼真的就是要測試才行
list_add_before
這個函數(shù)的add_pos->prev
參數(shù)上,原因就是宏只是做一個簡單的替換,而不是值代入替換和值代入可是大不相同的
#define _list_add_inner(_add_pos, _add_node) \
do {\
struct list * tmp = _add_pos;\
(_add_node)->next = tmp->next;\
(_add_node)->prev = tmp;\
tmp->next->prev = (_add_node);\
tmp->next = (_add_node);
} while(0)