C++/C学习笔记(八)
——C++编译预处理
1.文件包含
(1)基本定义和方式
#include伪指令用于包含一个头文件,头文件中存放的一般是模块接口,编译预处理器在扫描到该伪指令后就用对应的文本内容替换它。文件包含有两种语法形式:
① #include <头文件名称> ——一般用来包含开发环境提供的库头文件,它指示编译预处理器在开发环境设定的搜索路径中查找所需的头文件。
② #include "头文件名称" ——一般用来包含自己编写的头文件,它指示编译预处理器首先在当前工作目录下搜索头文件,如果找不到的话再到开发环境设定的路径中去查找。
使用该伪指令时,头文件可以加相对路径或绝对路径:
#include ".\myinclude\abc.h"
#include "E:\myproject\test1\source\include\abc.inl"
(2)内部包含卫哨和外部包含卫哨
为了避免同一个编译单元包含同一个头文件的内容超过一次(会导致重复定义错误),我们需要在头文件里面使用内部包含卫哨。内部包含卫哨实际上是使用预处理器的一种标志宏。
//stddef.h
#ifndef _STDDEF_H_INCLUDE_
#define _STDDEF_H_INCLUDE_
... //头文件的内容
#endif //!_STDDEF_H_INCLUDE_
//xxx.cpp
#include "stddef.h"
#include "stddef.h" //No problem!
当包含一个头文件时,如果能始终如一地使用外部包含卫哨,可以显著地提高编译速度,因为当一个头文件被一个源文件反复包含多次时(常常是因为递归的#include指令展开后所致),可以避免多次查找和打开头文件的操作。例如:
#if !defined(_INCLUDE_STDDEF_H)
#include <stddef,h>
#define _STDDEF_H_INCLUDE_
#endif //!_STDDEF_H_INCLUDE_
建议外部包含卫哨和内部包含卫哨使用同一个标志宏,这样可以少定义一个宏。例如:
#if !defined(_STDDEF_H_INCLUDE_)
#include <stddef,h>
#endif //!_STDDEF_H_INCLUDE_
可以仅在头文件包含其他头文件时使用外部包含卫哨,源文件中可以不使用,基本不影响编译速度。
(3)头文件包含的合理顺序
如果包含顺序不当,有可能出现包含顺序依赖问题,甚至引起编译时错误,推荐顺序如下:
在头文件中:
① 包含当前工程中所需要的自定义头文件(顺序自定);
② 包含第三方程序库的头文件;
③ 包含标准头文件。
在源文件中:
① 包含该源文件对应的头文件(如果存在);
② 包含当前工程中所需要的自定义头文件;
③ 包含第三方程序库的头文件;
④ 包含标准头文件。
2.宏定义
(1)基本定义
使用#define伪指令来定义一个宏。宏分为不带参数的宏和带参数的宏。宏定义以#define关键字后面出现的第一个连续字符序列作为宏名,剩下的部分作为宏体。宏定义具有文件作用域,不论宏定义出现在文件中的哪个地方,在它后面的任何地方都可以引用宏,
(2)特点和注意事项
① 宏定义不是C++语句,不需要使用语句结束符“;”,否则它也被看做宏体的一部分,例如:
#define OUTPUT(word) cout<<#word<<endl
使用方法: OUTPUT(I like C++.);
替换结果为:cout<<"I like C++."<<endl;
② 任何宏在编译预处理阶段都只是进行简单的文本替换,不会做类型检查和语法检查,这个工作留给编译器进行。参数替换发生在宏扩展之前。
③ 宏定义可以嵌套。例如:
#define PI 3.14
#define PI_2 (2*PI)
④ 宏不可以调试,因为宏不会进入符号表(符号表示编译器创建的,在编译时宏已经消失了),即使宏替换后出现了语法错误,编译器也会将错误定位到源程序中而不是某个具体的宏定义中。
⑤ 程序中使用双引号括起来的字符串中即使出现了与宏同名的子串,预处理过程也不进行替换。
⑥ 定义带参数的宏时,宏名和左括号之间不能出现空格,否则使用时会出现问题,但是编译器不会检查出这种错误。例如,宏TEXT如果定义为:
#define TEXT (str)#str
则宏引用语句:cout<<TEXT(hello world);
将扩展为:(str)#str(hello world); 是错误的
⑦ 带参数的宏和各个形参应该分别用括号括起来,以免造成意想不到的错误。例如:
#define SQUARE(x) ((x)*(x))
如果写成 #define SQUARE(x) x*x
那么语句a=SQUARE(3+5) 将被扩展为a=3+5*5+3;
⑧ 不要在引用宏定义的参数列表中使用增量和减量运算符,否则可能导致变量多次求值,且结果可能与预期不符,因为复合表达式中子表达式的求值顺序可能因具体编译器的不同而不同。
int n=5;
int x=SQUARE(n++);
其结果可能是30,也可能是25.
⑨ 带参数的宏定义不是函数,因此没有函数调用的开销,但是每一次扩展都会生成重复的代码,结果使可执行代码的体积增大。
⑩ Inline函数不可能完全取代宏,用宏来构造一些重复的、数据和函数混合的、功能较特殊的代码段的时候,其优点就显示出来。
11 当我们不再使用一个宏定义时,可以用#undef来取消定义:#undef TEXT
12 不要使用宏定义来定义新类型名,应用typedef。
13 给宏定义添加注释时请使用块注释(/* */),而不要使用行注释。因为有些编译器可能会把宏后面的行注释理解为宏体的一部分。
14 尽量使用const取代宏来定义符号常量。
15 对于较长的使用频率较高的重复代码片段,建议使用函数或模板而不要使用带参数的宏定义;而对于较短的重复代码片段,可以使用带参数的宏定义。
16 尽量避免在局部范围内(函数内、类型定义内)定义宏,除非它只在该局部范围内使用,否则会损害程序的清晰性。
3.条件编译
使用条件编译可以控制预处理器选择不同的代码段作为编译器的输入,从而使得源程序在不同的编译条件下产生不同的目标代码。条件编译伪指令就如同程序控制结构中的选择结构,不同的是前者只在编译预处理阶段即正式编译前发挥作用(不生成运行时代码),而后者则是在运行时起作用的。条件编译伪指令包括#if、#ifdef、#ifndef、#elif、#else、#endif、defined。每一个条件编译块都必须以#if开始,以#endif结束,defined必须结合#if或#elif使用。条件编译块可以出现在程序代码的任何地方。
(1)#if、#elif、#else
用条件编译伪指令if来屏蔽一段代码如下,如果要使这段代码生效,只需把0改为任意非0值即可。
#if 0
... /*...*./ //希望禁止编译的代码段
... /*...*./ //希望禁止编译的代码段
#endif
由于条件编译伪指令有编译预处理器来处理,显然预编译伪指令无法计算有变量参与的表达式或sizeof表达式,因此只能用常量表达式。如果常量表达式的值为非0,则条件为真,否则条件为假。例如:
#define FLAG_DOS 2
#define FLAG_UNIX 1
#define FLAG_WIN 0
#define OS 1
#if OS==FLAG_DOS
cout<<"DOS platform"<<endl;
#elif OS==FLAG_UNIX
cout<<"UNXI platform"<<endl;
#elif OS==FLAG_WIN
cout<<"Windows platform"<<endl;
#else
cout<<"Unknown platform"<<endl;
#endif
(2)#ifdef和#ifndef
预编译伪指令#ifdef XYZ等价于#if defined(XYZ),此处XYZ称为调试宏。如果前面曾经用#define定义过宏XYZ,那么#ifdef XYZ表示条件为真,否则条件为假,例如:
#define XYZ
...
#ifdef XYZ
Dosomething();
#endif
如果不想让Dosomething();语句被编译,那么删除#define XYZ,或者在其后用#undef XYZ取消该宏即可。
预编译伪指令#ifndef XYZ等价于#if !defined(XYZ)。
4.#error
编译伪指令#error用于输出与平台、环境等有关的信息。例如:
#if !defined(WIN32)
#error ERROR:Only Win32 platform supported!
#endif
#ifndef _cplusplus
#error MFC requires C++compilation(use a .cpp suffix)
#endif
当预处理器发现应用程序中没有定义宏WIN32或者_cplusplus时,那么 把#error后面的字符序列输出到屏幕后终止,程序不会进入编译阶段。
5.#pragma
编译伪指令#pragma用于执行语言实现所定义的动作。
#pragma pack(push,8) /*对象成员对齐字节数*/
#pragma pack(pop)
#pragma warning(disable:4069) /*不要产生第C4069号编译警告*/
#pragma comment(lib,"kernel32.lib")
#pragma comment(lib,"user32.lib")
#pragma comment(lib,"gdi32.lib")
6.#和##运算符
构串操作符#只能修饰带参数的宏的形参,它将实参的字符序列(而不是实参代表的值)转换成字符串常量。例如:
#define STRING(x) #x #x #x
#define TEXT(x) "class" #x "Info"
那么宏引用:
Int abc=100;
STRING(abc)
TEXT(abc)
展开后的结果分别为:
"abcabcabc"
"classabcInfo"
合并操作符##将出现在其左右的字符序列合并成一个新的标识符(注意,不是字符串)。例如:
#define ClASS_NAME(name) class##name
#define MERGE(x,y) x##y##x
则宏引用:
CLASS_NAME(SysTimer)
MERGE(me,To)
将分别扩展为如下两个标识符:
classSysTimer
meTome
使用合并操作符时,产生的标识符必须预先有定义,否则编译器会报“标识符未定义”的编译错误。
7.预定义符号常量
以下这些符号不能用#define重新定义,也不能用#undef取消。
符号常量 |
解释 |
_LINE_ |
引用该符号的语句的代码行号 |
_FILE_ |
引用该符号的语句的源文件名称 |
_DATE_ |
引用该符号的语句所在源文件被编译的日期(字符串) |
_TIME_ |
引用该符号的语句所在源文件被编译的时间(字符串) |
_TIMESTAMP_ |
引用该符号的语句所在源文件被编译的日期和时间(字符串) |
_STDC_ |
标准C语言环境都会定义该宏以标识当前环境 |
上表中的预定义符号常量可以被直接引用,常用来输出调试信息和定位异常发生的文件及代码行。