一、预编译概述
1.1 预编译定义
- 预处理过程扫描源代码,对其进行初步的转换,产生新的源代码提供给编译器。可见预处理过程先于编译器对源代码进行处理。
- 目前绝大多数编译器都包含了预处理程序,但通常认为它们是独立于编译器的。预处理过程读入源码,检查预处理指令,对源代码进行相应转换,并删除程序中的注释和多余空白字符。
- 预处理指令以#号开头,#号必须是该行除了任何空白字符外的第一个字符。#后是指令关键字,在关键字和#号之间允许存在任意个数的空白字符。
1.2 预编译功能
预编译指令通常用于文件包含,宏定义,编译控制(包括:条件编译、编译器警告配置、字节对齐、放置注释、上报消息等等),预编译还有一些更深层次的使用方法,下文将会介绍,综述预编译功能主要为:
- 上述基础功能
- 预编译头机制
- 元编程-代码生成
二、常用预编译指令
指令 |
用途 |
# |
空指令,无任何效果 |
## |
用于把参数链接到一起 |
#include |
包含一个源代码文件 |
#define |
定义宏 |
#undef |
取消已定义的宏 |
#if |
如果给定条件为真,则编译下面代码 |
#ifdef |
如果宏已经定义,则编译下面代码 |
#ifndef |
如果宏没有定义,则编译下面代码 |
#elif |
如果前面的#if给定条件不为真,当前条件为真,则编译下面代码 |
#endif |
结束一个#if……#else条件编译块 |
#line |
重置代码行号和文件名 |
#error |
停止编译并显示错误信息 |
#pragma |
设定编译器的状态或者是指示编译器完成一些特定的动作 |
#pragma warning |
选择性的修改编译器的警告消息行为 |
#pragma pack |
设置结构体等字节长度对齐 |
#pragma comment |
导入lib或dll |
#pragma deprecated |
抑制函数使用 |
#pragma message |
弹出消息 |
#pragma once |
保证文件编译一次 |
2.1 文件包含
#include "xxx.h" // 优先搜索当前目录
#include <yyy.h> // 优先搜索系统目录
PS:使用后定义符号,解决重复包含或者使用#pragma once
2.2 宏定义
2.2.1 标识宏定义
#ifndf _XXX_H_ // 常用于头文件标识定义
#define _XXX_H_
// header file content
……
#endif
#undef _YYY_H_ // 如果需要也可取消一个标识定义
不过头文件中一般时不要要取消定义的,在调试( #define _DEBUG_ )等其他用途中常会使用此功能;
2.2.2 标识或功能宏
还有一些常用的宏定义,用于定义常量或这完成某种功能,但要注意宏定义的形式,不然可能产生意想不到的效果,举例:
#define PI 3.14 // define const variable
#define Cube(n) (n)*(n)*(n) // calculate n's cube
/* may work as above macro, but may cause unexpected result */
#define Cube_2(n) n*n*n
举个栗子,对比使用宏Cube和Cube_2,我们就能发现区别:
int num = 8 + 2;
volume = Cube(num); // 展开结果为:(8 + 2)*(8 + 2)*(8 + 2)
volume2 = Cube_2(num); // 展开结果为:8 + 2*8 + 2*8 + 2
因此计算结果的大不相同;
2.2.3 ##运算符
##运算符用于把参数连接到一起。预处理程序把出现在##两侧的参数合并成一个符号。看下面的例子:
#define NUM(a,b,c) a##b##c
#define STR(a,b,c) a##b##c
main()
{
printf("%d ",NUM(1,2,3));
printf("%s ",STR("aa","bb","cc"));
}
最后程序的输出为:
123
aabbcc
2.3 编译控制
2.3.1 #line改变行号和文件
使用语法,#line number ["filename"],举例:
#line 1000 "123.cpp"
cout << "行号:" <<__LINE__ << " 文件名:" << __FILE__ << endl;
此时输出结果为:
行号:1000
文件名:123.cpp
此编译命令常用与调试,辅助定位代码问题
2.3.2 #error上报编译错误
在某些情况下,控制编译抛出错误,举个栗子:
#ifdef XXX
...
#error "XXX has been defined"
#else
2.3.3 #pragma系列
- 设置告警信息
命令格式:
#pragma warning( warning-specifier : warning-number-list [; warning-specifier : warning-number-list...])
#pragma warning( push[ ,n ] ) // 保存警告信息的现有的警告状态 [,把全局警告等级设定n].
#pragma warning( pop ) // 弹出警告信息,在入栈和出栈之间所作的一切改动取消
举个栗子:
/* 不显示4507和34号警告信息,4385号警告只显示一次,164号警告作为错误 */
#pragma warning( disable : 4507 34; once : 4385; error : 164 )
- 字节对齐
- 数据成员对齐规则:struct或union的数据成员,第一个数据成员offset为0,以后每个数据成员的对齐按照#pragma pack指定的数值和数据成员自身长度中,比较小的那个进行;
- 结构或联合整体对齐规则:在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐,对齐将按照#pragma pack指定的数值和结构(或联合)最大数据成员长度中,比较小的那个进行;
#pragma pack(x) // x is int number
struct A
{
char x;
double y;
int z;
}a;
sizeof(a)==13; // if x = 1
sizeof(a)==14; // if x = 2
sizeof(a)==16; // if x = 4
sizeof(a)==24; // if x = 8,default 相当于#pragma pack()
- 导入文件
#pragma comment( lib, "xxapi" ) // 动态导入 xxapi.lib
- 抑制老代码
#pragma deprecated
When the compiler encounters a deprecated symbol, it issues C4995:
void func1(void) {}
void func2(void) {}
int main()
{
func1();
func2();
#pragma deprecated(func1, func2)
func1(); // C4995
func2(); // C4995
}
- 上报编译消息
#pragma message("This is a test msg.")
三、预编译进阶使用
C++使用“头文件-源文件”的编译模型,每个源文件为一个编译单元,产生一个obj文件,然后所有obj被link到一起,生成exe文件。
在较大的系统软件中,有数以万的源文件,而每个头文件可能会包含数十甚至上百个头文件,
在每一个编译单元,这些头文件都会被从硬盘读进来一遍,然后被解析一遍,无数头文件的重复load与解析以及密集的磁盘操作,严重拖慢了程序的编译速度。
针对这个问题,有多种优化方式 [ref: C++ 预编译解析 ], 这里我们只看预编译头,是如何在这个问题上做出贡献的:
- 将所有稳定代码放入"stdafx.h",VS工程默认放入了"targetver.h" windows平台的,“stdio.h”VC编译器的,“tchar.h”支持宽字符的等代码,根据实际加入自己的;
- stdafx.cpp 包含 stdafx.h, 设置stdafx.cpp文件的属性,预编译头设置为 创建, 其他cpp文件按照实际属性设置为 使用 或者 不使用;
- 工程对预先编译的代码进行编译,会生成一个pch文件(precompiled header),在首次编译生成pch文件之后,就不会再编译stdafx.cpp的内容,从而达到加快编译速度的目的;
四、预编译元编程
此处不探讨此功能,参见: