参考几篇网上博文,对C++的预处理命令进行的一个总结和整合,当是学习。
一、预处理的由来
在C++的历史发展中,有很多的语言特征(特别是语言的晦涩之处)来自于C语言,预处理就是其中的一个。C++从C语言那里把C语言预处理器继承过来(C语言预处理器,被Bjarne博士简称为Cpp,不知道是不是C Program Preprocessor的简称)。 【http://www.cnblogs.com/lidabo/archive/2012/08/27/2658914.html】
二、预处理简介
通常我们说C++的Build(这里没用“编译”是怕混淆)可分为4个步骤:预处理、编译、汇编、链接。
NO. | Build步骤 | 处理内容 |
1 | 预处理 | 即本文要详细说的宏替换、头文件包含等; |
2 | 编译 | 指对预处理后的代码进行语法和语义分析,最终得到汇编代码或接近汇编的其他中间代码; |
3 | 汇编 | 指将上一步得到的汇编或中间代码转换为目标机器的二进制指令,一般是每个源文件生成一个二进制文件(VS是.obj,GCC是.o); |
4 | 链接 | 对上一步得到的多个二进制文件“链接”成可执行文件或库文件等。 |
这里说的“预处理”其实并不很严格,在C++标准中对C++的translation分为9个阶段(Phases of translation),其中第4个阶段是Preprocessor,而我们说的通常的“预处理”其实是指所有这4个阶段,下面列出这4个阶段(说的不详细,详见参考文献):
No | 预处理阶段 | 处理内容 |
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)。
【
*****************************************************************************************************************
C++的预处理(Preprocess),又称为预编译,是指在C++程序源代码被编译之前,由预处理器(Preprocessor)对C++程序源代码进行的处理。这个过程并不对程序的源代码进行解析,但它把源代分割或处理成为特定的符号用来支持宏调调用。
【
*****************************************************************************************************************
预处理 ,是做些代码文本的替换工作。处理 # 开头的指令 , 比如拷贝 #include 包含的文件代码, #define 宏定义的替换 , 条件编译等,就是为编译做的预备工作的阶段,主要处理#开始的预编译指令,预编译指令指示了在程序正式编译前就由编译器进行的操作,可以放在程序中的任何位置。
【
*****************************************************************************************************************
三、需要注意的地方
1、预处理是以 translation unit 为单位进行的,一个 translation unit 就是一个源文件连同由#include包含(或间接包含)的所有文本文件的全体(参见C++标准)。一般的,编译器对一个 translation unit 生成一个二进制文件(VS是.obj,GCC是.o)。
2、预处理命令可以放在程序的任何位置;
3、预处理指令中不容许出现空格;
4、一行上只能有一条预处理命令,一个预处理命令可以放在多行,行尾用‘\’表示;
5、宏名最好大写,但是不是硬规定;
6、宏替换不占用程序运行时间,在编译的时候进行;
7、文件包含#include<filename>是通过系统环境变量指定系统库目录进行查找的, #include“filename”是通过实在的用户目录下查找的,如果查找不到再去库文件里查找。库文件可以用<>也可以用“”,但是用户定义头文件只能用“”,所以<>比””查找范围小;
四、常见的预处理功能和预处理指令
1)预处理功能
预处理器的主要作用:把通过预处理的内建功能对一个资源进行等价替换,最常见的预处理有:文件包含,条件编译,布局控制,宏替换
No. | 主要功能 | 常用的预处理指令 | 具体作用 |
1 | 文件包含 | #include | 文件的引用,用于组合源程序正文 |
2 | 条件编译 | #if, #ifndef, #ifdef, #endif, #elif | 编译时进行有选择的挑选,注释掉一些指定的代码,以达到版本控制,防止对文件重复包含的功能。 |
3 | 布局控制 | #pragma | 为编译程序提供非常规的控制流信息 |
4 | 宏替换 | #define #undef | 定义符号常量、函数功能、重新命名、字符串拼接等 |
2)常见的预处理指令
No. | 指令 | 作用 |
1 | #define | 定义宏 |
2 | #undef | 取消宏 |
3 | #include | 文本包含 |
4 | #ifdef | 如果宏被定义就进行编译 |
5 | #ifndef | 如果宏未被定义就进行编译 |
6 | #endif | 结束编译块的控制 |
7 | #if | 表达式非零,就对代码进行编译 |
8 | #elif | 上一表达式为零,此表达式非零,就对代码进行编译 |
9 | #else | 作为其他处理的剩余选项进行编译(与if联用) |
10 | #line | 改变当前行数和名称 |
11 | #error | 输出一个错误信息 |
12 | #pragma | 为编译程序提供非常规的控制流信息 |
五、预处理指令格式
预处理指令的【格式】如下:
# define tokens
【说明】:符号#,应该是这一行的第一个非空字符,一般我们把它放在起始位置。如果指令一行放不下,可以通过反斜杠“/”进行控制,例如:下面两种方式等价
方式1:
#define Error / if(error) exit(1)
方式2:
#define Error if(error) exit(1)
不过我们为了美化起见,一般都不怎么这么用,更常见的方式如下:
# ifdef __BORLANDC__ if_true<(is_convertible::value)>:: template then ::type Make;# else enum { is_named = is_named_parameter ::value }; typedef typename if_true<(is_named)>::template then ::type Make;# endif
六、文件包含指令【#include】
这种预处理使用方式是最为常见的,平时我们编写程序都会用到,最常见的用法是:
指令 | 含义 |
#include <iostream> | file://标准库头文件 |
#include <iostream.h> | file://旧式的标准库头文件 |
#include "IO.h" | file://用户自定义的头文件 |
#include "../file.h" | file://UNIX下的父目录下的头文件 |
#include "/usr/local/file.h" | file://UNIX下的完整路径 |
#include "..//file.h" | file://Dos下的父目录下的头文件 |
#include "//usr//local//file.h" | file://Dos下的完整路径 |
【#include的两个注意】
1、我们用<iostream>还是<iostream.h>?
我们主张使用<iostream>,而不是<iostream.h>,为什么呢?我想你可能还记得我曾经给出过几点理由,这里我大致的说一下:
首先,.h格式的头文件早在98年9月份就被标准委员会抛弃了,我们应该紧跟标准,以适合时代的发展。
其次,iostream.h只支持窄字符集,iostream则支持窄/宽字符集。
还有,标准对iostream作了很多的改动,接口和实现都有了变化。
最后,iostream组件全部放入namespace std中,防止了名字污染。
2、<io.h>和"io.h"的区别?
其实他们唯一的区别就是搜索路径不同:
对于#include <io.h> ,编译器从标准库路径开始搜索
对于#include "io.h" ,编译器从用户的工作路径开始搜索
【#include的三种方式】
文件包含的格式 | 说明 | 举例 |
#include<filename> | 在标准包含目录查找filename [一般C++标准库头文件在此] | #include |
#include"filename" | 先查找当前工作目录,如果没找到再找标准包含目录 | #include "b.cpp" |
#include pp-tokens | pp-tokens须是定义为<filename>或"filename"的宏 | #define CMATH |
七、宏替换指令【#define】
7.1 定义宏常量
#define ,可以被用来生成宏定义常量(defined constants 或 macros),它的形式是:
#define name value
它的作用是定义一个叫做name 的宏定义,在程序代码中遇到name宏时,用value替换
举例 | #define PI 3.1415926 //定义符号PI为3.1415925 #undef PI //取消PI的值 |
解释说明 | PI只是一个符号或标志(不是变量,与变量没有任何关系), 代码编译前,预处理器会遍历代码,符号PI会用它的定义值(3.1415926)来代替 注释或字符串中的PI不进行替换 |
3.14159265 不是一个数值,只是一个字符串,不会进行检查 |
C与C++对比 | C | C++ |
定义常量差异 | 常用 #define 来定义符号常量 | 最好使用const 来定义符号常量 |
举例 | #define PI 3.14159265 | const long double PI=3.14159265 |
说明 | 没有类型的指定容易引起麻烦 | 定义清楚,所以在C++中推荐使用const 定义常量 |
#define 的缺点: 1)不支持类型检查 2)不考虑作用域 3)符号名不能限制在一个明明空间中 |
【注意事项】预编译器在编译的时候按照程序前后顺序就把值一个一个替换进去了,所以它不会考虑运行时候的逻辑关系,例如:
#define a 10void foo(); // 函数声明int main(){ printf("%d..",a); foo(); printf("%d",a);}void foo(){ #undef a #define a 50}//----------------------------//*** 以上程序输出是10..10 ***
对比:
#define a 10void foo(){ #undef a #define a 50}int main(){ printf("%d..",a); foo(); printf("%d",a);}//----------------------------//*** 以上程序输出是50..50 ***
7.2 定义宏函数
【格式】#define Print(Var) cout<<(Var)<<endl
【说明】print与(*)之间没有空格,否则(*)就会被解释为置换字符串的一部分
【注意】使用宏时应注意的易引起错误,所有的情况下都可以使用内联函数来代替宏,这样可以增强类型的检查
【错误1】#define max(x,y) x>y?x:y;+
调用 result = max(myval, 99); 则换成 result = myval>99?myval:99; 这个没有问题是正确的
调用 result = max(myval++, 99); 则换成 result = myval++>99?myval++:99; 这样如果myval>99那么myval就会递增两次,这种情况下()是没什么用的如result=max((x),y)则 result = (myval++)>99?(myval++):99;
【错误2】再如
#define product(m,n) m*n
调用
result = product(5+1,6);则替换为result = 5+1*6; 所以产生了错误的结果,此时应使用()把参数括起
#define product(m,n) (m)*(n)
则result = product(5+1,6);则替换为result = (5+1)*(6);
【结论】一般用内联函数来代替预处理器宏
【宏定义函数的其他用法】
用#define实现求最大值和最小值的宏
宏定义的错误使用
宏参数的连接
用宏得到一个字的高位或低位的字节
用宏定义得到一个数组所含元素的个数
【】
7.3 【小技巧、注意事项】
1) 给替换变量加引号
#define MYSTR "I love you"
cout << MYSTR ; //输出I love you而不是"I love you"
cout << "MYSTR" ; //则会输出"MYSTR"而不是"I love you"
cout << #MYSTR ; //则会输出 "I love you"即cout << "\"I love you\"";
2) 在宏表达式中连接几个参数
如
#define join(a,b) ab 这样不会理解为参数a的值与参数b的值的连接,即如join(10,999)不会理解为10999而是把ab理解为字符串,即输出ab
这时可以
#define join(a,b) a##b
则join(10,999)就会输出10999
3) 逻辑预处理器指令
#if defined CALCAVERAGE 或 #ifdef CALCAVERAGEint count=sizeof(data)/sizeof(data[0])for (int i=0; i
若已经定义符号CALCAVERAGE则把#if与#endif间的语句放在要编译的源代码内
八、编译控制执行,即条件编译【#ifdef、#ifndef、#if、#else】
【主要目的】 进行编译时进行有选择的挑选,注释掉一些指定的代码,以达到版本控制、防止对文件重复包含的功能。
【主要应用】条件编译被大量用于依赖于系统又需要跨平台的代码,这些代码一般会通过检测某些宏定义来识别操作系统、处理器架构、编译器,进而条件编译不同代码,以和系统兼容。但话又说回来,C++标准的最大价值就是让所有版本的C++实现都一致,从这个层面上讲,除非调用系统功能,否则不应该对系统做出任何假设,除了假设它支持C++标准。
No. | 指令格式 | 说明 |
1 | #ifdef identifier your code #endif | 如果【identifier】为一个定义了的符号,【your code】就会被编译, 否则剔除 |
2 | #ifndef identifier your code #endif | 如果【identifier】为一个未定义的符号,【your code】就会被编译, 否则剔除 |
3 | #if expression your code #endif | 如果【expression】非零,【your code】就会被编译, 否则剔除 |
4 | #ifdef identifier your code1 #else your code2 #endif | 如果【identifier】为一个定义了的符号,【your code1】就会被编译, 否则,【your code2】就会被编译 |
5 | #if expression1 your code1 #elif expression2 your code1 #else your code1 #endif | 如果【epression1】非零,编译【your code1】, 如果【expression2】非零,编译【your code2】, 否则,编译【your code3】 |
注:【identifier】是指宏
Eg.
#ifndef MAX_WIDTH#define MAX_WIDTH 100#endifchar str[MAX_WIDTH];
九、其他编译指令【#line、#error、#pragma】
除了上面说的集中常用的编译指令,还有3种不太常见的编译指令:#line、#error、#pragma,接下来简单介绍。
指令 | 语法格式 | 举例 |
#pragma | 【说明1】#pragma是非统一的,他要依靠各个编译器生产者,例如,在SUN C++编译器中: // 把name和val的起始地址调整为8个字节的倍数 #progma align 8 (name, val) char name[9]; double val; file://在程序执行开始,调用函数MyFunction #progma init (MyFunction) 【说明2】专门用于实现预先定义好的选项,其结果在编译器说明文档中进行了详细的解释。编译器未识别出来的#pragma指令都会被忽略 【说明3】这个指令是用来对编译器进行配置的,针对你所使用的平台和编译器而有所不同。要了解更多信息,请参考你的编译器手册。 如果你的编译器不支持某个#pragma的特定参数,这个参数会被忽略,不会产生出错。
| |
#line | #line num filename
| #line 1 "assigning variable" int a?; --------------------------------------------- 这段代码将会产生一个错误,显示为在文件"assigning variable", line 1 |
【作用】 当我们编译一段程序的时候,如果有错误发生,编译器会在错误前面显示出错文件的名称以及文件中的第几行发生的错误。 指令#line 可以使我们对这两点进行控制,也就是说当出错时显示文件中的行数以及我们希望显示的文件名。 ------------------------------------------------------------------- 【格式解读】 number 是将会赋给下一行的新行数。它后面的行数从这一点逐个递增; filename 是一个可选参数,用来替换自此行以后出错时显示的文件名,直到有另外一个#line指令替换它或直到文件的末尾 ------------------------------------------------------------------- 【其他说明】例:#line 30 a.h。这条指令可以改变当前的行号和文件名,本例中,改变当前的行号为30,文件名是a.h。初看起来似乎没有什么用,不过,他还是有点用的,那就是用在编译器的编写中,我们知道编译器对C++源码编译过程中会产生一些中间文件,通过这条指令,可以保证文件名是固定的,不会被这些中间文件代替,有利于进行分析。 | ||
#error | #error info | 【例1】 #ifndef UNIX #error This software requires the UNIX OS. #endif --------------------------------------------- 【例2】 #ifndef __cplusplus #error A C++ compiler is required #endif |
【例1说明】这条指令主要是给出错误信息,本例中,如果没有在UNIX环境下,就会输出This software requires the UNIX OS.然后诱发编译器终止。所以总的来说,这条指令的目的就是在程序崩溃之前能够给出一定的信息。 ------------------------------------------------------------------ 【例2说明】#error 指令,将中断编译过程并返回一个参数中定义的出错信息,本例中如果 __cplusplus 没有被定义就会中断编译过程 |
十、预定义标识符,预定义宏
为了处理一些有用的信息,预处理定义了一些预处理标识符,虽然各种编译器的预处理标识符不尽相同,但是以下宏名称在任何时候都是定义好的:
No. | 预定义宏 | 含义 | 备注 |
1 | __cplusplus | 整数值,所有C++编译器都定义了这个常量为某个值。如果这个编译器是完全遵守C++标准的,它的值 应该等于或大于199711L,具体值取决于它遵守的是哪个版本的标准。 【C++98中定义为199711L,C++11中定义为201103L】 | |
2 | __FILE__ | 字符串字面值,正在编译的文件的名字 | |
3 | __LINE__ | 十进制整数(从1开始),正在编译的文件的行号 | |
4 | __LINE__ | 一个格式为 "Mmm dd yyyy" 的字符串字面值,存储编译开始的日期。【例如: " Dec 25 2000"】 | |
5 | __TIME__ | 一个格式为 "hh:mm:ss" 的字符串字面值,存储编译开始的时间。【例如: "12:30:55"】 | |
6 | __STDC__ | 指示是否符合Standard C,是否定义取决于实现方式,如果编译器选项设置为编译标准的C代码,通 常就定义它,否则就不定义它 | |
7 | __STDC_HOSTED__ | 若是Hosted Implementation,定义为1,否则为0 | |
8 | __STDC_MB_MIGHT_NEQ_WC__ | 见ISO/IEC 14882:2011 | C++11 |
9 | __STDC_VERSION__ | 见ISO/IEC 14882:2011 | C++11 |
10 | __STDC_ISO_10646__ | 见ISO/IEC 14882:2011 | C++11 |
11 | __STDCPP_STRICT_POINTER_SAFETY__ | 见ISO/IEC 14882:2011 | C++11 |
12 | __STDCPP_THREADS__ | 见ISO/IEC 14882:2011 | C++11 |
13 | 【注】其中上面5个宏一定会被定义,下面从__STDC__开始的宏不一定被定义,这些预定义宏不能被 #undef |
① 例如:cout<<"The file is :"<<__FILE__"<<"! The lines is:"<<__LINE__<<endl;
② 使用#line可以修改__FILE__返回的字符串(查看【九、其他编译指令】)
举例
指令 | 含义 |
#line 1000 | 把当前行号设置为1000 |
#line 1000 "the program file" | 修改__FILE__返回的字符串行号改为了1000,文件名改为了"the program file" |
#line __LINE__ "the program file" | 修改__FILE__返回的字符串行号没变,文件名改为了"the program file" |
cout << "program last complied at "<<__TIME__<< " on " << __DATE__<< endl; |
十一、assert() 宏
在标准库头文件中声明
用于在 程序中 测试一个逻辑表达式,如果逻辑表达式为false, 则assert()会终止 程序,并显示诊断 消息
用于在条件不满足就会出现重大错误,所以应确保后面的语句不应再继续执行,所以它的应用非常灵活
注意: assert不是错误处理 机制,逻辑表达式的结果不应产生负面效果,也不应超出 程序员的控制(如找开一个文件是否成功), 程序应提供适当的代码来处理这种情况
assert(expression);assert(expression) && assert(expression2);
可以使用#define NDEBUG来关闭断言 机制
#include#include using std::cout;using std::endl;int main(){ int x=0; int y=0; cout<
【】
主要参考博文:
【
【
【
【】