本文在参考ISO/IEC 14882:2003和cppreference.com的C++ Preprocessor的基础上,对C++预处理做一个全面的总结讲解。如果没有特殊说明,所列内容均依据C++98标准,而非特定平台相关(如VC++)的,C++11新增的特性会专门指出。

1. 简介

通常我们说C++的Build(这里没用“编译”是怕混淆)可分为4个步骤:预处理、编译、汇编、链接。预处理就是本文要详细说的宏替换、头文件包含等;编译是指对预处理后的代码进行语法和语义分析,最终得到汇编代码或接近汇编的其他中间代码;汇编是指将上一步得到的汇编或中间代码转换为目标机器的二进制指令,一般是每个源文件生成一个二进制文件(VS是.obj,GCC是.o);链接是对上一步得到的多个二进制文件“链接”成可执行文件或库文件等。

这里说的“预处理”其实并不很严格,在C++标准中对C++的translation分为9个阶段(Phases of translation),其中第4个阶段是Preprocessor,而我们说的通常的“预处理”其实是指所有这4个阶段,下面列出这4个阶段(说的不详细,详见参考文献):

  1. 字符映射(Trigraph replacement):将系统相关的字符映射到C++标准定义的相应字符,但语义不变,如对不同操作系统上的不同的换行符统一换成规定字符(设为newline);
  2. 续行符处理(Line splicing):对于“\”紧跟newline的,删去“\”和newline(我们在#define等中用的续行在Preprocessor之前就处理了),该过程只进行1遍(如果是“\\”后有两个换行只会删去一个“\”);
  3. 字串分割(Tokenization):源代码作为一个串被分为如下串(Token)的连接:注释、whitespace、preprocessing tokens(标示符等这时都是preprocessing tokens,因为此时不知道谁是标示符,经过下一步之后,真正的预处理符会被处理);
  4. 执行Preprocessor:对#include指令做递归进行该1-4步,此步骤时候源代码中不再含有任何预处理语句(#开头的哪些)。

需要强调的是,预处理是在编译前已经完成的,也就是说编译时的输入文件里已经不含有任何预处理语句了, 这包括,条件编译的测试不通过部分被删去宏被替换头文件被插入等。

另外,预处理是以 translation unit 为单位进行的,一个 translation unit 就是一个源文件连同由#include包含(或间接包含)的所有文本文件的全体(参见C++标准)。一般的,编译器对一个 translation unit 生成一个二进制文件(VS是.obj,GCC是.o)。

有了这些知识之后,本文后面对第4步的Preprocessor做详细介绍。

2. 一般格式及概览

Preprocessor指令一般格式如下:

    # preprocessing_instruction [arguments] newline

其中preprocessing_instruction是以下之一:define, undef, include, if, ifdef, ifndef, else, elif, endif, line, error, pragma;arguments是可选的参数,如#include后面的文件名;Preprocessor占一行,可用“\”紧跟newline续行,但续行不是Preprocessor的专利,且续行在Preprocessor前处理。

Preprocessor指令有以下几种:

  1. Null,一个 # 后跟 newline ,不产生任何影响,类似于空语句;
  2. 条件编译,由 #if, #ifdef, #ifndef, #else, #elif, #endif 定义;
  3. 源文件包含,由 #include 定义;
  4. 宏替换,由 #define, #undef, #, ## 定义;
  5. 重定义行号和文件名,由 #line 定义;
  6. 错误信息,由 #error 定义;
  7. 编译器预留指令,由 #pragma 定义。

要指出的是,除了以上所列的Preprocessor指令外,其他指令是不被C++标准支持的,尽管有些编译器实现了自己的预处理指令。很据“可移植性比效率更重要”的原则,应该尽量仅适用C++标准的Preprocessor。

下一节将对以上每个进行详细说明,除了 Null 预处理指令。

3. 详细解释

条件编译                                                                                                                                                   

条件编译由 #if, #ifdef, #ifndef 开始,后跟 0-n 个 #elif ,后跟 0-1 个 #else ,后跟 #endif 。#if, #ifdef, #ifndef, #elif 后面接expression,条件编译的控制逻辑同 if-else if-else 条件语句(每个没配对的 else 和上面最近的没配对 if 配对这条也类似),只不过它是条件的对代码进行编译而不是执行。#if, #elif 的expression为常量表达式,expression非0时测试为真,expression还可以含有 defined(Token) 测试,即Token为宏定义时为真。#ifdef Token 等价于 #if defined(Token ),#ifndef Token 等价于 #if !defined(Token )。请看例子(摘自cppreference.com):

#include <iostream>
#define ABCD 2
int main()
{
#ifdef ABCD
std::cout << "1: yes\n";
#else
std::cout << "1: no\n";
#endif #ifndef ABCD
std::cout << "2: no1\n";
#elif ABCD == 2
std::cout << "2: yes\n";
#else
std::cout << "2: no2\n";
#endif #if !defined(DCBA) && (ABCD < 2*4-3)
std::cout << "3: yes\n";
#endif
std::cin.get();
return ;
}

条件编译被大量用于依赖于系统又需要跨平台的代码,这些代码一般会通过检测某些宏定义来识别操作系统、处理器架构、编译器,进而条件编译不同代码,以和系统兼容。但话又说回来,C++标准的最大价值就是让所有版本的C++实现都一致,从这个层面上将,除非调用系统功能,否则不应该对系统做出任何假设,除了假设它支持C++标准以外。

源文件包含                                                                                                                                                

文件包含指示将某个文件的内容插入到该#include处,这里“某个文件”将被递归预处理(1-4步,见第1节)。文件包含的3种格式为:#include<filename>(1)、#include"filename"(2)、#include pp-tokens(3),其中第1种方式在标准包含目录查找filename(一般C++标准库头文件在此),第二种方式先查找被处理源文件所在目录,如果没找到再找标准包含目录,第3中方式的pp-tokens须是定义为<filename>或"filename"的宏,否则结果未知。注意filename可以是任何文本文件,而不必是.h、.hpp等后缀文件,例如可以是.c或.cpp文本文件(所以标题是“源文件包含”而非“头文件包含”)。例子:

// file: b.cpp
#ifndef _B_CPP_
#define _B_CPP_ int b = ; #endif // #ifndef _B_CPP_
// file: a.cpp
#include <iostream> // 在标准包含目录查找
#include "b.cpp" // 在该源文件所在目录查找,找不到再到标准包含目录查找
#define CMATH <cmath>
#include CMATH
int main()
{
std::cout << b << '\n';
std::cout << std::log10(10.0) << '\n';
std::cin.get();
return ;
}

注意上面例子,将a.cpp和b.cpp放在同一文件夹,只编译a.cpp。

宏替换                                                                                                                                                       

#define 定义宏替换,#define 之后的宏都将被替换为宏的定义,直到用 #undef 解除该宏的定义。宏定义分为不带参数的常量宏(Object-like macros)和带参数的函数宏(Function-like macros)。其格式如下:

  • #define identifier replacement-list                             (1)
  • #define identifier( parameters ) replacement-list         (2)
  • #define identifier( parameters, ... ) replacement-list    (3) (since C++11)
  • #define identifier( ... ) replacement-list                      (4) (since C++11)
  • #undef identifier                                                     (5)

对于有参数的函数宏,在replacement-list中,“#”置于identifier面前表示将identifier变成字符串字面值,“##”连接,下面的例子来自cppreference.com

#include <iostream>

//make function factory and use it
#define FUNCTION(name, a) int fun_##name() { return a;} FUNCTION(abcd, );
FUNCTION(fff, );
FUNCTION(kkk, ); #undef FUNCTION
#define FUNCTION 34
#define OUTPUT(a) std::cout << #a << '\n' int main()
{
std::cout << "abcd: " << fun_abcd() << '\n';
std::cout << "fff: " << fun_fff() << '\n';
std::cout << "kkk: " << fun_kkk() << '\n';
std::cout << FUNCTION << '\n';
OUTPUT(million); //note the lack of quotes
std::cin.get();
return ;
}

可变参数宏是C++11新增部分(来自C99),使用时用__VA_ARGS__指代参数“...”,一个摘自C++标准2011的例子如下(标准举的例子就是不一样啊):

#define debug(...) fprintf(stderr, __VA_ARGS__)
#define showlist(...) puts(#__VA_ARGS__)
#define report(test, ...) ((test) ? puts(#test) : printf(__VA_ARGS__))
debug("Flag");
debug("X = %d\n", x);
showlist(The first, second, and third items.);
report(x>y, "x is %d but y is %d", x, y);

这段代码在预处理后产生如下代码:

fprintf(stderr, "Flag");
fprintf(stderr, "X = %d\n", x);
puts("The first, second, and third items.");
((x>y) ? puts("x>y") : printf("x is %d but y is %d", x, y));

在上面条件编译就讲到,有时用 #ifdef macro_NAME 来识别一些信息,C++标准指定了一些预定义宏,列在下表中(C++11新增宏已标出):

Predefined macros

Meaning

Remark

__cplusplus

在C++98中定义为199711L,C++11中定义为201103L

__LINE__

指示所在的源代码行数(从1开始),十进制常数

__FILE__

指示源文件名,字符串字面值

__DATE__

处理时的日期,字符串字面值,格式“Mmm dd yyyy”

__TIME__

处理时的时刻,字符串字面值,格式“hh:mm:ss”

__STDC__

指示是否符合Standard C,可能不被定义

wikipedia条目

__STDC_HOSTED__

若是Hosted Implementation,定义为1,否则为0

C++11

__STDC_MB_MIGHT_NEQ_WC__

见ISO/IEC 14882:2011

C++11

__STDC_VERSION__

见ISO/IEC 14882:2011

C++11

__STDC_ISO_10646__

见ISO/IEC 14882:2011

C++11

__STDCPP_STRICT_POINTER_SAFETY__

见ISO/IEC 14882:2011

C++11

__STDCPP_THREADS__

见ISO/IEC 14882:2011

C++11

其中上面5个宏一定会被定义,下面从__STDC__开始的宏不一定被定义,这些预定义宏不能被 #undef。使用这些宏的一个例子如下(连续字符串字面值会被自动相连,“ab”“cde” 等价于 “abcde”):

 #include <iostream>
int main()
{
#define PRINT(arg) std::cout << #arg": " << arg << '\n'
PRINT(__cplusplus);
PRINT(__LINE__);
PRINT(__FILE__);
PRINT(__DATE__);
PRINT(__TIME__);
#ifdef __STDC__
PRINT(__STDC__);
#endif
std::cin.get();
return ;
}

这些宏经常用于输出调试信息。预定义宏一般以“__”作为前缀,所以用户自定义宏应该避开“__”开头。

应当指出的是,现代的C++程序设计原则不推荐适用宏定义常量或函数宏,应该尽量少的使用 #define ,如果可能,用 const 变量或 inline 函数代替。

重定义行号和文件名                                                                                                                                  

从 #line number ["filename"] 的下一行源代码开始, __LINE__ 被重定义为从 number 开始,__FILE__ 被重定义"filename"(可选),一个例子如下:

 #include <iostream>
int main()
{
#define PRINT(arg) std::cout << #arg": " << arg << '\n'
#line 999 "WO" PRINT(__LINE__);
PRINT(__FILE__);
std::cin.get();
return ;
}

错误信息                                                                                                                                                    

#error [message] 指示编译器报告错误,一般用于系统相关代码,例如检测操作系统类型,用条件编译里 #error 报告错误。例子如下:

int main()
{
#error "w"
return ;
#error
}

第2个 #error 可能不被执行,因为编译器可能在遇到一个 #error "w" 时就报错停止了。

编译器预留指令                                                                                                                                         

#pragma 预处理指令是C++标准给特定C++实现预留的标准,所以,在不同的编译器上 #pragma 的参数及意义可能不同,例如 VC++2010 提供 #pragma once 来指示源文件只被处理一遍。OpenMP作为一个共享内存并行编程模型,使用 #pragma omp 指导语句,详见:OpenMP共享内存并行编程详解

VC++的 #pragma 指令参见MSDN相关条目

GCC的 #pragma 指令参见GCC文档相关条目

4. 预处理的典型应用

预处理的常见使用有:

  1. Include guard,见wikipedia条目,该技术用来保证头文件仅被同一文件包含一次(准确地说,头文件内容在一个 translation unit 中仅出现一次),以防止违反C++的“一次定义”原则;
  2. 用 #ifdef 和特殊宏识别操作系统、处理器架构、编译器,条件编译,进而实现针对特定平台的功能,多用于可移植性代码;
  3. 定义函数宏,以简化代码,或是方便修改某些配置;
  4. 用 #pragma 设定和实现相关的配置(见上一节最后给出的链接)。

sourceforge.net上有一个项目,是关于用宏检测操作系统处理器架构编译器(请点链接或见参考文献)。下面是一个例子(来自这里):

#ifdef _WIN64
//define something for Windows (64-bit)
#elif _WIN32
//define something for Windows (32-bit)
#elif __APPLE__
#include "TargetConditionals.h"
#if TARGET_OS_IPHONE && TARGET_IPHONE_SIMULATOR
// define something for simulator
#elif TARGET_OS_IPHONE
// define something for iphone
#else
#define TARGET_OS_OSX 1
// define something for OSX
#endif
#elif __linux
// linux
#elif __unix // all unices not caught above
// Unix
#elif __posix
// POSIX
#endif

参考文献

http://en.wikipedia.org/wiki/C_preprocessor

ISO/IEC 14882:2003 2.1[lex.phases] 和 16[cpp]

ISO/IEC 14882:2011 16.3[cpp.replace] 和 16.8[cpp.predefined]

cppreference.com 关于 C++ Preprocessor

Microsoft MSDN 关于 C++ Preprocessor

GCC在线文档 关于 C++ preprocessor

http://sourceforge.net/p/predef/wiki/Home/

C++预处理详解的更多相关文章

  1. C语言之预处理详解

    C语言之预处理详解 纲要: 预定义符号 #define #define定义标识符 #define定义宏 #define的替换规则 #与## 几点注意#undef 带副作用的宏参数 宏和函数的对比 命名 ...

  2. C++头文件,预处理详解

    C++遵循先定义,后使用的原则.就拿函数的使用来举例吧. 我看过有些人喜欢这样写函数. #include<iostream> using namespace std; int add(in ...

  3. gcc/g++等编译器 编译原理: 预处理,编译,汇编,链接各步骤详解

    摘自http://blog.csdn.net/elfprincexu/article/details/45043971 gcc/g++等编译器 编译原理: 预处理,编译,汇编,链接各步骤详解 C和C+ ...

  4. #pragma 预处理指令详解

    源地址:http://blog.csdn.net/jx_kingwei/article/details/367312 #pragma  预处理指令详解              在所有的预处理指令中, ...

  5. mysql中SQL执行过程详解与用于预处理语句的SQL语法

    mysql中SQL执行过程详解 客户端发送一条查询给服务器: 服务器先检查查询缓存,如果命中了缓存,则立刻返回存储在缓存中的结果.否则进入下一阶段. 服务器段进行SQL解析.预处理,在优化器生成对应的 ...

  6. C#中的预处理指令详解

    这篇文章主要介绍了C#中的预处理指令详解,本文讲解了#define 和 #undef.#if.#elif.#else和#endif.#warning和#error.#region和#endregion ...

  7. pragma comment的使用 pragma预处理指令详解

    pragma comment的使用 pragma预处理指令详解   #pragma comment( comment-type [,"commentstring"] ) 该宏放置一 ...

  8. EasyPR--开发详解(7)字符分割

    大家好,好久不见了. 一转眼距离上一篇博客已经是4个月前的事了.要问博主这段时间去干了什么,我只能说:我去“外面看了看”. 图1 我想去看看 在外面跟几家创业公司谈了谈,交流了一些大数据与机器视觉相关 ...

  9. EasyPR--开发详解(6)SVM开发详解

    在前面的几篇文章中,我们介绍了EasyPR中车牌定位模块的相关内容.本文开始分析车牌定位模块后续步骤的车牌判断模块.车牌判断模块是EasyPR中的基于机器学习模型的一个模块,这个模型就是作者前文中从机 ...

随机推荐

  1. JS常用正则表达式和JS控制输入框输入限制(数字、汉字、字符)

    验证数字:^[0-9]*$验证n位的数字:^\d{n}$验证至少n位数字:^\d{n,}$验证m-n位的数字:^\d{m,n}$验证零和非零开头的数字:^(0|[1-9][0-9]*)$验证有两位小数 ...

  2. HDU--1863--畅通工程--并查集

    畅通工程 Time Limit: 1000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)Total Submis ...

  3. (DFS、全排列)POJ-2718 Smallest Difference

    题目地址 简要题意: 给若干组数字,每组数据是递增的在0--9之间的数,且每组数的个数不确定.对于每组数,输出由这些数组成的两个数的差的绝对值最小是多少(每个数出现且只出现一次). 思路分析: 对于n ...

  4. spring IOC装配Bean(注解方式)

    1 Spring的注解装配Bean (1) Spring2.5 引入使用注解去定义Bean @Component 描述Spring框架中Bean (2) Spring的框架中提供了与@Componen ...

  5. UIControl

    //当遇到button上添加图片,不显示图片,而显示蓝色,解决方案 //1.button的类型,改成UIButtonTypeCustom //2.button的set使用setBackgroundIm ...

  6. io端口与io内存详解

    (一)地址的概念 1)物理地址:CPU地址总线传来的地址,由硬件电路控制其具体含义.物理地址中很大一部分是留给内存条中的内存的,但也常被映射到其他存储器上(如显存.BIOS等).在程序指令中的虚拟地址 ...

  7. android studio使用中遇到的问题

    旧版和新版切换会报错(点击更正, 不影响程序使用) 2.debug正常, 打包签名程序时候报错 String index out of range: -82 java.lang.StringIndex ...

  8. 使用WebDriverWait类处理等待(sleep)的问题

    用selenium进行web UI的自动化开发时,经常遇到loading需要等待的时候,或者需要验证一个action之后某个dialog是否呈现或者消失.对于这类情况是不建议用sleep(xx)来死等 ...

  9. Scala编程--基本类型和操作

    如果你熟悉Java,你会很开心地发现Java基本类型和操作符在Scala里有同样的意思.然而即使你是一位资深Java开发者,这里也仍然有一些有趣的差别使得本章值得一读.因为本章提到的一些Scala的方 ...

  10. EXT 环境部署

    1. 准备工作 在开始搭建Ext 开发环境前,你需要安装好下列工具/程序: JDK1.5 MyEclipse 3.  Ext 基类代码 2. 安装JDK1.5 2.1. 确定已安装的JDK版本 点击开 ...