預處理是C語言的一個重要知識點,它能改善程序設計的環境,有助于編寫易移植、易調試的程序。因此,我們有必要掌握好預處理命令,在自己編程的時候靈活的使用它,使得編寫的程序結構優良,更加易于調試和閱讀。接下來我盡可能的把預處理中重要知識點向讀者講解清楚,使讀者能夠在自己以后編程的過程中熟練的使用預處理命令。 C語言的預處理主要有三個方面: 1、文件的包含 2、宏定義 3、條件編譯 一、文件包含的形式有下面兩種 1、#include "文件名" 2、#include 它們之間的區別在于:系統到頭文件目錄查找文件, "文件名"則先在當前目錄查找,如果沒有才到頭文件目錄查找;當然我們也可以使用在命令行來指定頭文件路徑方法。還要注意就是如果在源文件包含的頭文件之間出現調用的情況,那么被調用的頭文件要出現在調用頭文件的前面。 二、宏定義 宏定義的使用有兩種形式,一種不帶參數,而另外一種帶參數。 1、不帶參數 格式: #define 標識符 字符串 相信上面這個格式大家并不陌生,下面還是來看看如何使用吧。當然在講解之前我們的看看使用過程中的如下幾個注意要點: (1)預處理不做語法檢查,所以我們選用的時候要尤其小心 (2)宏定義寫在函數的花括號外邊,作用域為其后的程序,通常在文件開頭部分,直到用#undef命令終止宏定義的作用域 (3)不要在字符串中使用宏,如果宏名出現在字符串中那么將按照字符串進行處理 下面來看段代碼的使用。 [html] view plaincopy#include #define N 9 int main () { int i,a[N]; for(i=0;i { a[ i]=i; printf("%d\t",a[ i]); if((i+1)%3==0) printf("\n"); } //#undef N printf("%d\n",N); } 運行結果為: [html] view plaincopy0 1 2 3 4 5 6 7 8 9 Press any key to continue 我們在此主要是介紹下宏的作用域問題,當在以上代碼中注釋掉#undef N時,接下來的打印語句能夠正常的打印出;但是當我們沒有注釋掉#undef N的時候就會出現error C2065: 'N' : undeclared identifier錯誤,提示N沒有定義。接下來看看帶參數的宏的使用。 2、帶參數 #define 宏名(參數表) 字符串 注意要點: (1)宏名和參數的括號間不能有空格 (2)宏替換只作替換,不做計算,不做表達式求解,這點要尤其注意 (3)函數調用在編譯后程序運行時進行,并且分配內存。宏替換在編譯前進行,不分配內存 (4)宏的啞實結合(所謂的啞實結合類似于函數調用過程中實參替代形參的過程)不存在類型,也沒有類型轉換。 (5)宏展開使源程序變長,函數調用不會 下面來看看linux下一個典型的應用: #define min(x,y) ({ typeof(x) _x = (x); typeof(y) _y = (y); (void) (&_x == &_y); _x _y ? _x : _y; }) 在上面的兩個宏中我們發現有這么一句代碼(void) (&_x == &_y);可能不少讀者有點發懵的感覺,這啥意思呢?!其實我們細細分析就知道,首先看看“==”,這是一個邏輯表達式,它要求兩邊的比較類型必須一致,如果我們的&x和&y類型不一致,如一個為char*,另一個為int*,不是同一個類型,當我們使用gcc編譯的時候就會出現警告信息,vc6則會報錯error C2446: '==' : no conversion from 'char *' to 'int *'。這句代碼(void) (&_x == &_y); 在此的功能就相當于執行一個簡單的判斷操作,我們用來判斷x和y的類型是否一致。別小看了這句代碼,如果學會了使用它會給你的代碼帶來不少的便捷。下面給出一個小小的事例: [cpp] view plaincopy#include void print() { printf("hello world!!!\n"); return ; } void main(int argc,char*argv) { print(); return ; } 運行結果為: [cpp] view plaincopyhello world!!! Press any key to continue 現在我們來修改下代碼后看看運行結果: [cpp] view plaincopy#include void print() { printf("hello world!!!\n"); return ; } void main(int argc,char*argv) { #define print() ((void)(3)) print(); return ; } 運行結果為: [cpp] view plaincopyPress any key to continue 這兒的結果沒有了我們之前的那句hello world!!!,可以看出這個時候函數并沒有被調用,這是因為我們使用了#define print() ((void)(3)),使得之后調用函數print()轉換為了一個空操作,所以這個函數在接下來的代碼中都不會被調用了,就像被“沖刷掉”了一樣?吹竭@兒你是不是想起我們之前的那篇《C語言的那些小秘密之斷言》了呢,我們同樣可以使用這種方法來實現斷言的關閉,方法與之類似,在此就不再講解了,有興趣的讀者可以自己試試。講到這兒似乎應該結束了,但是細心的讀者會有另外一個疑惑?在#define min(x,y) ({ typeof(x) _x = (x); typeof(y) _y = (y); (void) (&_x == &_y); _x #define print(...) printf(__VA_ARGS__) int main(int argc,char*argv) { print("hello world----%d\n",1111); return 0; } 運行結果為: [cpp] view plaincopyroot@ubuntu:/home/shiyan# ./arg hello world----1111 接著往下看。 #define printf (tem, ...) fprintf (stdout, tem, ## __VA_ARGS__) 如有對fprintf不熟悉的讀者可以自己查查函數手冊,在此不再講解。 [cpp] view plaincopy#include #define print(temp, ...) fprintf(stdout, temp, ##__VA_ARGS__) int main(int argc,char*argv) { print("hello world----%d\n",1111); return 0; } 運行結果為: [cpp] view plaincopyroot@ubuntu:/home/shiyan# ./arg hello world----1111 temp在此的作用為設定輸出字符串的格式,后邊“...”為可變參數,F在問題來了,我們在宏定義中為什么要使用“##”呢?如果我們沒有使用##會怎么樣呢?看看下面的代碼: [cpp] view plaincopy#include #define print(temp, ...) fprintf(stdout, temp, __VA_ARGS__) int main(int argc,char*argv) { print("hello world\n"); return 0; } 編譯時發生了如下錯誤: [cpp] view plaincopyroot@ubuntu:/home/shiyan# gcc arg.c -o arg arg.c: In function ‘main’: arg.c:7:2: error: expected expression before ‘)’ token 為什么會出現上面的錯誤呢,現在我們來分析下,我們進行下宏替換,print("hello world\n")就變為了fprintf(stdout, "hello world\n",)這樣我們就發現了后面出現了一個逗號,所以導致了錯誤,如果有“##”就不會出現這樣的錯誤了,這是因為如果可變參數被忽略或為空的時候,“##”操作將使預處理器去除掉它前面的那個逗號。如果存在可變參數時候,它也能正常工作。講了“##”,我們當然也要講講“#”。先來看看下面一段代碼: [cpp] view plaincopy#include #define return_exam(p) if(!(p)) \ {printf("error: "#p" file_name:%s\tfunction_name:%s\tline:%d .\n",\ __FILE__, __func__, __LINE__); return 0;} int print() { return_exam(0); } int main(int argc,char*argv) { print(); printf("hello world!!!\n"); return 0; } 運行結果為: [cpp] view plaincopyroot@ubuntu:/home/shiyan# ./arg error: 0 file_name:arg.c function_name:print line:9 . hello world!!! 我們發現在運行結果中打印出了出錯的文件名、函數名、以及行號。采用宏定義來檢測函數的返回值是否正確,僅僅是為了體現出我們要講解的宏,所以代碼做了最大的簡化工作,讀者在自己編寫代碼時候要學會這樣的檢測方式!#”的作用就是將其后面的宏參數進行字符串化操作,就是在宏變量進行替換之后在其左右各加上一個上雙引號,這就使得"#p"變味了""p""我們發現這樣的話剛好兩邊的“""”就消失了。下面來看看最后一個知識點條件編譯。 三、條件編譯 條件編譯命令#if、#else、#elif、#endif、#ifdef、#ifndef,條件編譯指令的意思很簡單,跟我們學習的if語句類似。 一般格式 #if 常量表達式 程序段1; [#else 程序段2;] #endif 功能:當表達式為非0(“邏輯真”)時,編譯程序段1,否則編譯程序段2。 一般格式 #ifdef 標識符 程序段1; [#else 程序段2;] #endif 功能:當“標識符”已經被#define命令定義過,則編譯程序段1,否則編譯程序段2。 #ifndef 標識符 程序段1; [#else 程序段2;] #endif 功能:當“標識符”未被#define命令定義過,則編譯程序段1,否則編譯程序段2。 學習了條件編譯指令之后,我們在調試代碼的時候,就不要再隨心所欲的刪減代碼了,如果我們不想某段代碼被編譯就可以使用條件編譯指令來將其注釋掉。如: #if (0) 注釋代碼段; #endif 就可以實現代碼的注釋了,需要的時候也可以將其啟用,而不會為需要重新編輯代碼時,發現已被刪除而頭疼了。 其中值得注意的地方為,常量表達式在編譯時求值,所以表達式只能是常量或者已經定義過的標識符,不能為變量,也不可以為那些在編譯時候求值的操作符,如sizeof。 下面來看段代碼: [cpp] view plaincopy#include #define N 1 int main(int argc,char*argv) { int a=3; #if(a) printf("#if后面的表達式為變量\n"); #endif #if(N) printf("#if后面的表達式已定義,且不為0---success\n"); #else printf("#if后面的表達式已定義,且不為0---fail\n"); #endif return 0; } 運行結果為: [cpp] view plaincopy#if后面的表達式已定義,且不為0---success Press any key to continue 看看上面的代碼我們的表達式為變量a時并沒有打印出來,所以我們不能在其后的表示中使用變量。如果我們使用sizeof操作符會怎么樣呢?為了加深印象看看下面的代碼后結果吧。 [cpp] view plaincopy#include int main(int argc,char*argv) { int a=9; #if(sizeof(a)) printf("#if后面的表達式含有sizeof操作符\n"); #endif return 0; } 編譯出現了如下錯誤: [cpp] view plaincopyfatal error C1017: invalid integer constant expression 所以我們在使用條件編譯的時候要牢記這兩點,常量表達式不能為變量和含有sizeof等在編譯時求值的操作符。 接下來看看這里要講的最后一個#pragma指令。 一般格式為: #pragma 參數 下面給出幾種經常使用的形式 1、#pragma message("消息") 看看下面一段代碼。 [cpp] view plaincopy#include #define FDSA int main(int argc,char*argv) { #ifdef FDSA #pragma message("FDSA 已經定義過了") #endif return 0; } 編譯的時候我們可以在編譯輸出窗口中看到了輸出“FDSA 已經定義過了”,通過這種方式我們可以在一些我們想要的地方輸出很多我們需要的信息。 2、#pragma once 如果我們在頭文件的開頭部分加入這條指令,那么就能保證我們的頭文件僅僅被編譯一次。 3、#pragma hdrstop 該指令表示編譯頭文件到此為止,后面的無需在進行編譯了。 4、#pragma pack() 設定字節的對齊長度,這個指令我們在《C語言的那些小秘密之字節對齊》中已經講解了,在此不再復述。 5、#pragma warning(disable:M N;once:H;error:K) 表示不顯示M和N號的警告信息,H號警告信息只報告一次,把K號警告信息作為一個錯誤來處理。 到此關于預處理的講解就結束了。由于本人水平有限,博客中的不妥或錯誤之處在所難免,殷切希望讀者批評指正。同時也歡迎讀者共同探討相關的內容,如果樂意交流的話請留下你寶貴的意見。 |