语言基础,3:C++预编译

一、预编译概述

1.1 预编译定义

  • 预处理过程扫描源代码,对其进行初步的转换,产生新的源代码提供给编译器。可见预处理过程先于编译器对源代码进行处理。
  • 目前绝大多数编译器都包含了预处理程序,但通常认为它们是独立于编译器的。预处理过程读入源码,检查预处理指令,对源代码进行相应转换,并删除程序中的注释和多余空白字符。
  • 预处理指令以#号开头,#号必须是该行除了任何空白字符外的第一个字符。#后是指令关键字,在关键字和#号之间允许存在任意个数的空白字符。

1.2 预编译功能

预编译指令通常用于文件包含,宏定义,编译控制(包括:条件编译、编译器警告配置、字节对齐、放置注释、上报消息等等),预编译还有一些更深层次的使用方法,下文将会介绍,综述预编译功能主要为:

  1. 上述基础功能
  2. 预编译头机制
  3. 元编程-代码生成

二、常用预编译指令

指令

用途

#

空指令,无任何效果

##

用于把参数链接到一起

#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\n",NUM(1,2,3));

printf("%s\n",STR("aa","bb","cc"));

}

最后程序的输出为:

123

aabbcc

2.3 编译控制

2.3.1 #line改变行号和文件

使用语法,#line number ["filename"],举例:

#line 1000 "123.cpp"

cout << "行号:" <<__LINE__ << "\n文件名:" << __FILE__ << endl;

此时输出结果为:

行号:1000

文件名:123.cpp

此编译命令常用与调试,辅助定位代码问题

2.3.2 #error上报编译错误

在某些情况下,控制编译抛出错误,举个栗子:

#ifdef XXX

...

#error "XXX has been defined"

#else

2.3.3 #pragma系列

  1. 设置告警信息

命令格式:

#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 )

  1. 字节对齐
  • 数据成员对齐规则: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()

  1. 导入文件

#pragma comment( lib, "xxapi" ) // 动态导入 xxapi.lib

  1. 抑制老代码

#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

}

  1. 上报编译消息

#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的内容,从而达到加快编译速度的目的;

四、预编译元编程

此处不探讨此功能,参见:

C/C++ 预处理元编程