預處理是編譯過程中單獨執行第一個步驟,主要任務是删除注釋、插入被include包含的檔案的内容、定義和替換#define定義的符号以及根據條件編譯指令進行編譯等。
圖1 C預處理綱要思維導圖
1宏定義
宏定義的形式為:#define名字替換文本。移除則為:# undef名字。
預處理将所有出現名字的地方替換為替換文本。宏定義主要用于替換幻數、替換多處出現的調試資訊及替換多處出現的短小代碼等等。其優點是:宏與類型無關、宏的執行速度快于函數及便于修改維護。缺點是:宏會增加程式的長度、具有副作用的參數可能使宏産生不可預料的結果。
#define identifier token-sequence
#define identifier(identifier-list) token-sequence
1.1 無參數的宏
1.1.1 字面值宏常量
#define PI 3.1415926 #define DEBUG 0 #define DEBUG 1 |
1.1.2 字元串宏常量
#define FILE_PATH “E:\code\preprocess.c”
#define FILE_NAME "test.c"
#define LOG_PATH "/home" "/" FILE_NAME ".log"
1.1.3 表達式宏定義
#define DO_FOREVER for(;;)
#define SEC_A_YEAR (60*60*24*365UL)
1.1.4 程式語句宏定義
宏定義程式語句時,如果替換檔案較長,則可以分為多行,除最後一行外,每行的末尾都要加一個反斜杠\。這利用了相鄰的字元串常量被自動連接配接為一個字元串的特性。
#define DEBUG_PRINT printf(“File %s line %d:”\ “ x = %d, y = %d, z = %d”,\ __FILE__, __LINE__,\ x, y, z) |
1.2 帶參數的宏
帶參數宏的定義形式為:#define NAME(parameter-list) stuff。
其中,parameter-list是一個由逗号分隔的符号清單。參數清單的左括号必須與NAME緊鄰。
1.2.1 表達式語句
帶參數的宏定義時,如果參數用于表達式計算,最好都要加上括号。
#define MAX(x,y) x > y ? x : y
當z = MAX(7,4);z為7。但當z = 4+MAX(4,7);時,展開為z = 4 + 4 >7 ? 4 : 7;則z為4。
是以應修改為:
#define MAX(x,y) ((x) > (y) ? (x) : (y))
當x,y為具有副作用的參數時,如:
int x = 10, y = 20,z = 0;
z = MAX(x++, y++)則z為21
z = MAX(++x, ++y)則z為22
當x,y類型不一樣時,如:
int x = 10, z = 0;
char y = 20;
z = MAX(x++, y++)則z為21
z = MAX(++x, ++y)則z為22
include/linux/kernel.h中,是這樣定義的:
#define min(x,y) ({ \ typeof(x) _x = (x); \ typeof(y) _y = (y); \ (void) (&_x == &_y); \ _x < _y ? _x : _y; }) |
利用新定義的變量_x與_y來比較,避免了x,y參數的副作用。因無法通過typeof(_x)==typeof(_y)做類型相同比較,是以利用void) (&_x == &_y);判斷其位址指針是否相等。顯然兩個位址是不相等的。但如果_x和_y的類型不一樣,其指針類型也會不一樣,兩個不一樣的指針類型進行比較操作時,就會引起編譯器産生一個編譯告警:warning: comparison of distinct pointertypes lacks a cast
1.2.2 程式語句
當定義中的替換文本太長,需分成多行時,可使用do-while-zero結構。
#define DEBUG_PRINT (debug)\ do\ {\ if (1 == debug)\ printf(“File %s line %d”, __FILE__, __LINE__);\ else\ ;\ }while(0) |
1.3 宏與函數的差別
在源檔案多次出現,如簡單計算的代碼,可以用宏實作,也可以用函數實作。如果用函數則存在函數調用/傳回的額外開銷,且函數參數與類型有關。而用宏實作的話,執行速度更快,且宏和類型無關。
#define MALLOC(n,type) \ ((type *)malloc((n) * sizeof(type))) p = MALLOC(10,int); p = ((int *)malloc((25) * sizoef(int))) |
2 條件編譯
2.1 條件為常量表達式
#if常量表達式 statements #endif |
常量表達式由預處理器求值,如果它的值是非零值(真),那麼statements部分就被正常編譯,否則預處理器就會删除它們。
#if常量表達式 statements 1 #else statements2 #endif |
當常量表達式的值是非零值,則處理statements1部分,否則處理#else的statements2部分。
#if常量表達式1 statements1 #elif常量表達式2 statements2 #else statements3 #endif |
當常量表達式1的值是非零值,則處理statements1部分;若常量表達式2的值是非零值,則處理statements2部分;若常量表達式1和2的值都是零值,則處理statements3部分。
#if defined(NAME1) statements1 #endif |
#if !defined(NAME2) statements2 #endif |
2.2 條件為辨別符
辨別符可以是已被定義過的宏,也可以是空宏,也可以是未定義的宏。
#ifdef辨別符 statements #endif |
當辨別符是已被定義過的宏時,則編譯statements部分。
#ifndef辨別符 statements1 #else statements2 #endif |
當辨別符是已被定義過的宏時,則編譯statements1部分,否則編譯statements2部分。
#ifdef NAME1 statements1 #endif |
#ifdnef NAME2 statements2 #endif |
空宏、未定義的宏、空字元串宏
#define EMPTY
#define EMPTY do{}while(0)
#define EMP_STR “”
空宏和未定義的宏都展開為空字元串,但定義為空字元串的宏被視為是在預處理表達式中定義的,一般用在#ifdef/#endif和#ifndef/#endif中。
3 檔案包含
檔案包含指令#include,使另一個檔案的内容被編譯,就像它實際出現在#include指令出現的地方。替換方式:預處理器删除這條指令,并用包含檔案的内容取而代之。
檔案包含有兩種形式,用<>包含的函數庫頭檔案,如:#include<stdio.h>和用””本地頭檔案,如:#include“preprocess.h”。一般,本地頭檔案與本地檔案存在同一個目錄下。用<>包含的頭檔案的常見處理政策是在由編譯器定義的标準位置查找庫函數頭檔案。用””包含的頭檔案的常見處理政策是在源檔案所在的目前目錄進行查找,如果未找到,編譯器就像查找函數庫頭檔案一樣在标準位置查找本地頭檔案。
避免多重包含:
如果a.h和b.h都包含一個嵌套的#include檔案x.h,則x.h被多重包含。
一個頭檔案被多個頭檔案包含,則該頭檔案會被編譯多次。為解決這個問題,可使用條件編譯,即所有的頭檔案才有以下形式:
#ifndef_HEADERNAME_H
#define_HEADERNAME_H
…
#endif
4 其他
4.1 字元串化運算符:#
#argument結構由預處理器轉化為字元串常量“argument”,argument必須是帶參數宏裡的參數。
例1:
#define PRINT(format,value)printf("The value of " #value\
"is"format "\n",value)
PRINT(“%d”, 123);輸出:The value of 123 is 123
例2:
#define PRINT(format,value)printf("The value of " #v \
"is"format "\n",value)
輸出錯誤資訊:expected macro formal parameter(宏定義要求形參名)的錯誤。
例3:
printf("The value of " #value" is %d"\n", 5);
printf(#value “ is %d"\n", 5);
輸出錯誤資訊:preprocessor command must start as first nonwhite space(預處理指令前面隻允許空格)
4.2 連接配接運算符:##
##用來将兩個TOKEN連接配接為一個TOKEN。TOKERN不一定是帶參數宏裡的參數。
#define XNAME(n) x## n
#define LINK_MULTIPLE(a, b, c, d)a##_##b##_##c##_##d
C99編譯器既支援可變參數的函數,也支援可變參數的宏:
int printf(const char *fmt, …);
#define debug(fmt, …) printf(fmt,__VAR_ARGS__)
GCC支援複雜的宏,它使用一種不同的文法進而是你可以給可變參數一個名字,如同其他參數一樣。
#define dbg(format, arg...) printk(KERN_DEBUGPFX ": " format, ## arg)
當可變參數為空時,##操作使預處理器去除掉它前面的逗号。
4.3 #error
形式:#error text oferror message
編譯程式時,隻要遇到#error就會生成一個編譯錯誤提示資訊,并停止編譯。
#if defined(LINUS_HD) #define HIGH_MEMORY (0x800000) #elif defined(LASU_HD) #define HIGH_MEMORY (0x400000) #else #error "must define hd" #endif |
4.4 #line
形式:#line number["filename"]
預處理器将number作為下一行的行号,filename作為目前檔案的名字。這條指令修改__LINE__與 __FILE__符号的值。該指令最常用于把其他語言的代碼轉換為C代碼的程式。
#line 123 “preprocess.c” |
4.5 #pragma
#pragma指令支援因編譯器而異的特性,它的作用是設定編譯器的狀态或訓示編譯器完成一些特定的動作。#pragma是不可移植的,預處理器将忽略它不認識的#pragma指令,兩個不同的編譯器可能以兩種不同的方式解釋同一條#pragma指令。
4.5.1 #pragma comment
格式:#pragma comment(...)
該指令将一個注釋記錄放入一個對象檔案或可執行檔案中。
#pragma comment(lib, ”ws2_32.lib") |
4.5.2 #pragma message
格式:#pragma message(“text message”)
它能夠在編譯資訊輸出視窗中輸出相應的資訊,這對于源代碼資訊的控制是非常重要的。
#pragma message("MACRO activated!") |
4.5.3 #pragma code_seg
格式:#pragma code_seg([ [ { push | pop}, ] [ identifier, ] ] ["section-name"[,"section-class"]] )
它能夠設定程式中函數代碼存放的代碼段,當我們開發驅動程式的時候就會使用到它。
4.5.4 #pragma data_seg
一般用于DLL中。也就是說,在DLL中定義一個共享的,有名字的資料段。最關鍵的是:這個資料段中的全局變量可以被多個程序共享。否則多個程序之間無法共享DLL中的全局變量。
4.5.5 #pragma once
隻要在頭檔案的最開始加入這條指令就能夠保證頭檔案被編譯一次。
4.5.6 #pragma hdrstop
預編譯頭檔案到此為止,後面的頭檔案不進行預編譯。
4.5.7#pragma warning
格式:
#pragma warning(warning-specifier : warning-number-list
[; warning-specifier : warning-number-list...]
#pragma warning(push[ ,n ] )
#pragma warning(pop )
#pragma warning(disable : 4507 34; once : 4385; error : 164 )
等價于:
#pragmawarning(disable:4507 34) //不顯示4507和34号警告資訊
#pragmawarning(once:4385) // 4385号警告資訊僅報告一次
#pragmawarning(error:164) //把164号警告資訊作為一個錯誤。
4.5.8 #pragma resource ".dfm"
#pragma resource "*.dfm"表示把*.dfm檔案中的資源加入工程。*.dfm中包括窗體
外觀的定義。
4.5.9 #pragma pack
格式:#pragma pack(n)
主要用于改變C編譯器的位元組對齊方式。
#pragma pack (n)
#pragma pack (),取消自定義位元組對齊方式。
#pragma pack (8) struct test { char x1; short x2; float x3; char x4; }; #pragma pack () |
GCC編譯器:
__attribute((aligned (n))),讓所作用的結構成員對齊在n位元組自然邊界上。如果結構中有成員的長度大于n,則按照最大成員的長度來對齊。
__attribute__ ((packed)),取消結構在編譯過程中的優化對齊,按照實際占用位元組數進行對齊。
struct test { char x1; short x2; float x3; char x4; }__attribute__ ((packed)); |
參考:
《C和指針》
《C程式設計語言》 第2版
《C語言深度解剖》
及其他