预处理是编译过程中单独执行第一个步骤,主要任务是删除注释、插入被include包含的文件的内容、定义和替换#define定义的符号以及根据条件编译指令进行编译等。
图1 C预处理纲要思维导图
1宏定义
宏定义的形式为:#define名字替换文本。移除则为:# undef名字。
预处理将所有出现名字的地方替换为替换文本。宏定义主要用于替换幻数、替换多处出现的调试信息及替换多处出现的短小代码等等。其优点是:宏与类型无关、宏的执行速度快于函数及便于修改维护。缺点是:宏会增加程序的长度、具有副作用的参数可能使宏产生不可预料的结果。
#define identifier token-sequence /*不带参数的宏,identifier后面的空格都会被丢弃*/
#define identifier(identifier-list) token-sequence /*带参数的宏,identifier和identifier-list之间的(不能有空格,否则会被解释为不带参数的宏*/
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" /*空格会被丢弃,所以LOG_PATH为 “/home/test.c.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\ ;/*do nothing*/\ }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 /*XNAME(1) 输出:x1*/
#define LINK_MULTIPLE(a, b, c, d)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") /*将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) /* C编译器将按照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语言深度解剖》
及其他