C/C++编译系统编译程序的过程为预处理、编译、链接。预处理器是在程序源文件被编译之前根据预处理指令对程序源文件进行处理的程序。预处理器指令以#号开头标识,末尾不包含分号,以换行符结束。预处理命令不是C/C++语言本身的组成部分,不能直接对它们进行编译和链接。C/C++语言的一个重要功能是可以使用预处理指令和具有预处理的功能。C/C++提供的预处理功能主要有文件包含、宏替换、条件编译等。
预处理指令#include用于包含头文件,有两种形式:#include
<xxx.h>
,#include "xxx.h"
。
宏定义的作用一般是用一个短的名字代表一个长的代码序列。宏定义包括无参数宏定义和带参数宏定义两类。宏名和宏参数所代表的代码序列可以是任何意义的内容。但要尤其注意的是宏名和宏参数必须是合法的标识符,其所代表的内容及意义在宏展开前后必须一直是独立且保持不变的,不能分开解释和执行。宏定义代码序列中可以引用已经定义的宏名,即宏定义可以嵌套。
无参数宏定义
用一个用户指定的称为宏名的标识符来代表一个代码序列,这种定义的一般形式为#define 标识符 代码序列。其中#define之后的标识符称为宏定义名(简称宏名),在宏定义#define之前可以有若干个空格、制表符,但不允许有其它字符,宏名与代码序列之间用空格符分隔。
带参数宏定义
带参数宏定义进一步扩充了无参数宏定义的能力,这时的宏展开既进行宏名的替换又进行宏参数的替换。带参数的宏定义的一般形式为:
#define 标识符(参数表) 代码序列
其中参数表中的参数之间用逗号分隔,在代码序列中必须要包含参数表中的的参数。在定义带参数的宏时,标识符与右圆括号之间不允许有空白符,应紧接在一起,否则变成了无参数的宏定义。带参数宏调用提供的实在参数个数必须与宏定义中的形式参数个数相同。
作用域
宏定义的有效范围称为宏名的作用域,宏名的作用域从宏定义的结束处开始到其所在的源代码文件末尾。宏名的作用域不受分程序结构的影响。如果需要终止宏名的作用域,可以用预处理指令#undef加上宏名。
命名规则
宏名一般用大写字母,以便与变量名区别。如有必要,宏名可被重复定义,被重复定义后,宏名原先的意义被新意义所代替。宏定义代码序列中必须把”“配对,不能把字符串”“拆开。例如:
#define NAME "vrmozart //不合法
#define NAME "vrmozart" //合法
宏定义在源文件中必须单独另起一行,换行符是宏定义的结束标志。如果一个宏定义中代码序列太长,一行不够时,可采用续行的方法。续行是在键入回车符之前先键入符号 \,注意回车要紧接在符号\之后,中间不能插入其它符号,当然代码序列最后一行结束时不能有 \。注意多行宏在调用时只能单独一行调用,不能用在表达式中或作为函数参数。
预处理器在处理宏定义时,会对宏进行替换。宏替换首先将源文件中在宏定义随后所有出现的宏名均用其所代表的代码序列替换之,如果是带参数宏则接着将代码序列中的宏形参名替换为宏实参名。宏替换只作代码字符序列的替换工作,不作任何语法的检查,也不作任何的中间计算,一切其它操作都要在替换完后才能进行。如果宏定义不当,错误要到预处理之后的编译阶段才能发现。源代码中的宏名和宏定义代码序列中的宏形参名必须是标识符才会被替换,即只替换标识符,不替换别的东西,像注释、字符串常量以及标识符内出现的宏名或宏形参名则不会被替换。例如:
///*NAME*/、"NAME"、my_NAME_blog中的宏名NAME都不会被替换。
#define NAME vrmozart
//宏定义代码序列中的宏形参名name也都不会被替换。
#define BLOG(name) my_name_blog="name"
如果希望宏定义代码序列中标识符内出现的宏形参名能够被替换,可以在宏形参名与标识符之间添加连接符 ##,在宏替换过程中宏形参名和连接符 ## 一起将被替换为宏实参名。## 用于把宏参数名与宏定义代码序列中的标识符连接在一起,形成一个新的标识符。例如:
#define BLOG(name) my_##name,BLOG(vrmozart)表示my_vrmozart
#define BLOG(name) name##_ blog,BLOG(vrmozart)表示vrmozart_ blog
#define BLOG(name) my_##name##_blog,BLOG(vrmozart)表示my_vrmozart_ blog
如果希望宏定义代码序列中的宏形参名被替换为宏实参名的字符串形式(即在宏实参名两端加双引号”),而不是替换为宏实参名,可以在宏定义代码序列中的宏形参名前面添加符号 #。# 用于把宏参数名变为一个字符串形式。例如:
#define STR(name) #vrmozart //STR(vrmozart)表示"vrmozart"
当宏参数是另一个宏的时候,通常先展开参数。但是如果外部宏定义时含有 # 或者 ##,则参数不进行替换。例如:
#define cat(a,b) a ## b
#define f(a) fff a
#define ab AB
cat(cat(1,2),3)
cat(a,b)
f(cat(cat(1,2),3))
输出:
cat(1,2)3
AB
fff cat(1,2)3
在宏定义中说过,宏名和宏形参名所代表的内容及意义在宏展开前后必须一直是独立且保持不变的,不能分开解释和执行。其原因如下,在宏调用时,用宏定义的代码序列替换宏名,用宏实参名替换宏形参名。替换后,宏定义的代码序列就与源文件中相邻的代码自然连接,宏实参名也与代码序列中相邻的代码自然连接,宏定义的代码序列和宏实参名的独立性就不一定依旧存在。例如:
#define SQR(x) x*x //希望实现表达式的平方计算。
p = SQR(y); //宏展开 p = y*y
q = SQR(u+v); //宏展开 q = u+v*u+v
显然,后者的展开结果不是程序设计者所希望的。为能保持宏实参名替换后的独立性,应在宏定义中给形式参数加上括号。SQR 宏定义改写成
#define SQR(x) ((x)*(x))
__DATE__ 字符串常量类型,表示当前所在源文件的编译日期,输出格式为Mmm dd yyyy(如May 27 2006)。
__TIME__ 字符串常量类型,表示当前所在源文件的编译日期,输出格式为hh:mm:ss(如09:11:10)。
__FILE__ 字符串常量类型,表示当前所在源文件名,且包含文件路径。
__LINE__ 整数常量类型,表示当前所在源文件中的行号。
__FUNCTION__ 字符串常量类型,表示当前所在函数名。
这些预定义宏在调试程序时是很有用的,因为你可以很容易的知道程序运行到了那个文件的那一行,是那个函数。
一般情况下,在进行编译时对源程序中的每一行都要编译,但是有时希望程序中某一部分内容只在满足一定条件时才进行编译,如果不满足这个条件,就不编译这部分内容,这就是条件编译。条件编译主要是进行编译时进行有选择的挑选,注释掉一些指定的代码,以达到多个版本控制、防止对文件重复包含的功能。比较常见条件编译预处理指令:
#if
...
#ifndef
#ifdef
...
#elif
...
#else
...
#endif
表达式
预处理器表达式包括的操作符主要涉及到单个数的操作(+、-、~、<<、>>)、多个数的运算(*、/、%、+、-、&、^、|)、关系比较(<、<=、>、>=、==、!=)、宏定义判断(defined)、逻辑操作(!、&&、||),其优先级和行为方式与C++表达式操作符相同。对于预处理器表达式,一定要记住它们是在编译器预处理器上执行的,是在编译前进行的。
除了上面讨论的常用预处理指令外,还有三个不太常见的预处理指令:#line、#error、#pragma,下面分别介绍。
#pragma warning,设置编译器处理编译警告信息的方式,例如
#pragma warning(disable:4507 34;once : 4385;error:164)
//等价于:
#pragma warning(disable:4507 34) //不显示4507和34号警告信息
#pragma warning(once:4385) //4385号警告信息仅报告一
#pragma warning(error:164) //把164号警告信息作为一个错误
#pragma comment(…)
设置一个注释记录到对象文件或者可执行文件中。常用lib注释类型,用来将一个库文件链接到目标文件中,一般形式为 #pragma comment(lib,"*.lib")
,其作用与在项目属性链接器“附加依赖项”中输入库文件的效果相同。