C++程序预处理

2018-07-31作者:池剑锋, 编著编辑:Solomon

程序代码在编译前通常会对程序进行预处理工作,这些工作往往包括宏替换、引入头文件等工作。C++的预处理(Preprocess),是指在C++程序源代码被编译之前,由预处理器(Preprocessor)对 C++程序源代码进行的处理。这个过程并不对程序的源代码进行解析,它仅把源代码分隔或处理为特定的符号,用来支持宏调用。

在本章主要的知识点有:

1、程序预处理,理解什么是程序预处理,掌握预处理指令。 

2、条件编译,学会在编写代码时,使用条件编译语句。


预处理简介


在 C++的历史发展中,有很多的语言特征来自于 C 语言,预处理就是其中的一个。C++ 的程序预处理功能由预处理器完成。对程序进行预处理的目的,通常是为了能有助于执行编译过程。预处理器的主要作用就是通过预处理的内建功能对一个资源进行等价替换,最常见的预处理有:文件包含,条件编译、布局控制和宏替换等 4 种。

(1)文件包含 文件包含使用命令#include,是一种最为常见的预处理,主要是作为文件的引用组合源程序正文。

(2)条件编译

条件编译是程序代价常用的预处理,在 C++库中大量使用到了条件编译。条件编译需要使用#if,#ifndef,#ifdef,#endif,#undef 等命令。条件编译主要是在进行编译时进行有选择的挑选,注释掉一些指定的代码,以达到版本控制、防止对文件重复包含的功能。

(3)布局控制。

布局控制使用#progma 命令,这也是预处理的一个重要方面,主要功能是为编译程序提供非常规的控制流信息。 

(4)宏替换 

宏是 C++中最常用的预处理,它使用#define 命令,可以定义符号常量、函数功能、重新命名、字符串的拼接等各种功能。

在 C++程序代码中,预处理命令具有以下特点:  

    1、在左边加#号,作为标志。

   2、一般独占一行。

   3、预处理命令不是编程语句,因此句末不加分号。

   4、在正常编译过程之前作为预备动作执行,编译过程结束后不占用存储空间。


C++预处理程序

预处理指令的格式如下:

    # directive tokens

#符号应该是这一行的第一个非空字符,一般我们把它放在起始位置。directive 是预处理的命令,tokens 是预处理的内容。如果指令一行放不下,可以通过“/”进行控制,例如下列代码:

    #define Error if(error) exit(1)

等价于:

#define Error /

if(error) exit(1)

当然,对于有条件判断语句的预处理,通常不会采用上述方式。为了美观起见,更常见的方式如下:

# ifdef __BORLANDC__

    if_true<(is_convertible<Value,named_template_param_base>::value)>::

    template then<make_named_arg, make_key_value>::type Make;

# else

    enum { is_named = is_named_parameter<Value>::value };

    typedef typename if_true<(is_named)>::template

    then<make_named_arg, make_key_value>::type Make;

# endif

一些常用的预处理指令及其含义如表:

asda.jpg


include(包含)文件


文件包含(#include)这种预处理使用方式是最为常见的,平时在编写程序时基本上都会用到。头文件通常以“.h”结尾,其内容可使用#include 预处理器指令包含到程序中。在头文件中一般包含了函数(类)的原型与全局变量等信息。当使用#include 命令将一个头文件引入某个代码中时,在这个代码中就可以调用头文件中的函数了。

文件包含#include 指令通常有下面两种形式:

#include <iostream> //方式1 

#include "myheader.h" //方式2

即用尖括号和双引号将待引入的代码括起来。前者<>用来引用标准库头文件,后者"" 常用来引用自定义的头文件。在使用尖括号<>时,编译器只搜索包含标准库头文件的默认目录,而使用双引号""则会首先搜索正在编译的源文件所在的目录,找不到时再搜索包含标准库头文件的默认目录。如果把头文件放在其他目录下,为了查找到它,必须在双引号中指定从源文件到头文件的完整路径。

常见的文件包含方式,如下:

#include <iostream>

#include <iostream.h>

#include "IO.h"

#include "../file.h"

#include "/usr/local/file.h"

#include "..file.h"

在 C 语言中,引入的头文件通常是带有“.h”后缀的文本文件,然而在 C++中却不一 定。例如:

#include <iostream>

在这里的文件包含,尖括号中的内容为“iostream”而不是“iostream.h”。这是由于 C++在标准化的过程中,对原来的 C 语言的部分做了相应改动。其中包括两个方面:


(1)增加命名空间 

C++增加了名称空间概念,借以将原来声明在全局空间下的标识符声明在了 namespacestd 下。因此在前面章节编写代码使用#include <iostream> 的时候,使用函数前要用 using namespace std; 导入命名空间,否则要使用“iostream”中定义的函数或变量,就需要在每次调用加“std::”。例如代码:

#include<iostream>

using namespace std;

void main()

{

 cout<<"hello world";

}

如果不使用语句“using namespace std”,则在调用 cout 时,需要在前面添加“std::”。 代码如下所示。

#include<iostream>

void main()

{

    std::cout<<"hello world";

}

C++之所以采用这样一种方式,是为了解决代码编程过程中的重名问题。众所周知,随着时间的发展标准库变得非常庞大,程序员在选择类的名称或函数名时就很有可能和标 准库中的某个名字相同。所以为了避免这种情况所造成的名字冲突,就把标准库中的一切都放在名字空间 std 中。



(2)统一后缀名

在早期的 C 语言或 C++代码头文件都不全是“.h”开头的。有些程序员使用如“.h”“.hpp”“.hxx”这样的后缀名。标准化之前的头文件就是带后缀名的文件,标准化后的头文件就是不带后缀名的文件。C++ 98 规定用户应使用新版头文件,对旧版本头文件不再进行强制规范,但大多数编译器厂商依然提供旧版本头文件,以求向下兼容。


换句话说,在当今的编译环境下进行开发时,使用“.h”的头文件是旧标准的,如果想用新的标准的头文件,就不要带 .h。


另外,为了和 C 语言兼容,C++标准化过程中,原有 C 语言头文件标准化后,头文件名前带个 c 字母,如 cstdio、cstring、ctime、ctype 等。也就是说,如果要用 C++标准化了的 C 语言头文件,文件包含代码就得做如下的转换:

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

预处理指令#define


宏是 C++程序中非常有用的预处理,它实现了代码在编译时的常量替换功能。在 C++标准库提供的头文件中,包含了大量的宏定义。宏包括带参数和不带参数的宏。


预定义符号常量

每个预定义符号常量在常量的开始和结 尾都有一个下划线。这些符号常量不能用于指令#define 或#undef 中。

_LINE_   ------------  当前源代码的行号(整数)

_FILE_   ------------  假设的源文件名(字符串)

_DATE_  ------------  源文件的日期编译(字符串)

_TIME_   ------------  源文件的编译时间(字符串,格式为:“hh:mm:ss”)


带参数的宏定义和不带参数的宏定义

宏通过预处理指令#define 实现。在 C++中,宏分为两种:带参数的宏和不带参数的宏。 

    1.不带参数的宏

在一般程序设计中,经常会遇到多次书写相同的内容,如果程序需要修改,就必须修改全部这些内容。如果使用宏,仅需要修改一处宏即可,这样可以大大减少修改的量。宏定义中需要使用define 关键字,其中不带参数宏的一般定义格式为:

#define 宏名 宏体

宏名可以在代码中使用,在编译时宏名出现的地方均会被宏体的内容替代。宏名与宏体之间用空格分隔,所以宏名中间不能有空格。宏体是对宏的具体实现过程,可以用任意 字符串,中间也可以有空格,宏体以回车结尾。例如:

#define PI 3.14 

#define TRUE 1 

#define FALSE 0

上述代码中定义了 3 个宏,分别为 PI、TRUE 和 FALSE。定义了宏之后,在程序中就可以使用宏名来取代需要宏体出现的地方。例如,计算圆周长时需要用到圆周率 PI,判断真假值时用到 TRUE 和 FALSE。


在程序编译时,凡出现宏名的地方,都可以被宏体字符串替换。例如,如果在程序代码中,多次出现了 PI,编译时每个 PI 都会被替换为 3.14。如果程序需要修改 PI 的值,比如需要增加 PI 的精度,则仅需要修改宏体即可。

【例子】 实现了使用宏计算圆的周长和面积。并将输出结果打印出来。

#include<iostream>

using namespace std;

 #define PI 3.14 //宏圆周率

 void main()

 {

    int r = 10; //半径

    double area,length; //面积和周长

    area = PI*r*r;    //求面积

    length = PI*r*2;  //求周长

    cout << "area is : "<< area << endl;

    cout << "length is : " << length << endl;

 }

编译、运行上述程序得到如下结果:

 area is : 314

length is : 62.8

程序定义了宏 PI,在代码 08~09 行分别使用了宏名 PI。在程序编译时,这两行语句会被解释为:

 area = 3.14*r*r;

length = 3.14*r*2;

需要注意的是,编译时编译器宏替换仅会进行字符串替换,而不会像函数参数那样会先将表达式进行计算。例如,将 PI 定义为 3+0.14,则替换后 08~09 行会被解释为:

 area = 3+0.14*r*r;

length = 3+0.14*r*2;

这样根据运算符优先级顺序,程序执行结果将与原来的预计不符而导致错误的发生。 如果在编写时不确定宏使用时的优先级,可以将宏体用括号括起来。


由于宏在计算时容易因为优先级等原因引起错误,因此对于常量的定义通常不建议使用宏定义。在 C 语言中常以#define 来定义符号常量,但在 C++中最好使用 const 来定义常 量。例如:

#define PI 3.14159265

可以使用常量来代替:

 const long double PI=3.14159265;

两者比较下,前者没有类型的指定容易引起不必要的麻烦,而后者定义清楚,所以在 C++中推荐使用 const 来定义常量。但需要注意的是,后者定义方式为变量指定了数据类型,在内存中会为其分配相应类型大小的内存空间。而宏则不会为其分配内存空间。


宏的优点在于它能无条件地将一段程序代码替换成一个简短的宏名。对于经常使用到的程序代码段来说,这非常有用。但相应地引入宏也有不少缺点:

 1、不支持类型检查

 2、不考虑作用域

 3、符号名不能限制在一个命名空间中


    2.带参数的宏

带参数的宏有点类似于函数的使用,它同样也可以带参数。其替换的一般形式为:

#define 宏名(参数表) 宏体 

在使用带参数的宏时,不是进行简单的字符串替换,还需要补充完参数。

【例子】 实现了使用宏替换圆周长和面积的计算,并将输出结果打印出来。

 #include<iostream>

 using namespace std;

 #define PI 3.14 //宏 圆周率

 #define AREA(R) (PI*(R)*(R))    //宏 求圆面积 

 #define LENGTH(R) (PI*(R)*2)  //宏 求圆周长

 void main() 07 {

 int r = 10;   //半径

 double area,length;   //面积和周长

 area = AREA(r);       //求面积

 length = LENGTH(r);   //求周长

 cout << "area is : "<< area << endl;

 cout << "length is : " << length << endl;

 getchar();

 }

编译、运行上述程序得到如下结果:

area is : 314

length is : 62.8


和程序代码 17.1 的区别不大,仅将程序中求面积和周长的表达式改为宏实现。从例子中可以发现,带参数的宏替换与带参数的函数还是有本质区别的,其区如下:


asdasdad.jpg


宏定义取消


已定义的宏可以撤销,在撤销之后,它不再发挥作用。宏撤销的一般格式为:

#undef 宏名

这里的宏名是之前定义过的宏名。例如:

#define PI 3.14

...

#undef PI


条件编译


条件编译的主要目的是在编译时进行有选择的挑选,注释掉一些指定的代码,以达到版本控制、防止对文件重复包含的功能。通常条件编译会和宏结合使用。条件编译的指令有:#if、#else、#elif、#endif、#ifdef、#ifndef 等。


#if-#else-#endif 

#if-#else-#endif 语句是条件编译中的条件判断语句,其使用方法与 C++的条件判断语句 if-else 语句类似。#if-#else-#endif 语句的使用方法如下:

#if expression

    code1

#else code2

#endif

该条件判断语句的意思是,如果表达式 expression 为非 0,则对 code1 进行编译,否则对 code2 进行编译。如果没有 code2 部分,即不满足 expresssion 条件就不编译的情况,那么可以省略#else。此时预编译指令变化为:

 #if expression

    code

#endif


【例子】 实现了使用#if-#else-#endif 语句在不同时期产生不同的字符串,并将输出结果打印出来。

#include<iostream>

 using namespace std;


 #define DEBUG 1

 #if DEBUG        //定义常数

 const char* str = "there is in debug!";     //定义字符串常量

 #else

 const char* str = "there is not in debug!";

 #endif


 void main()

 {

 cout << str<< endl;

 }

编译、运行上述程序得到如下结果:

there is in debug!"

上述代码中,定义了一个 DEBUG 的宏,用来表示开发的不同时期。当 DEBUG 为 1 时,则应该编译 05 行的字符串语句;如果为 0 则应该编译第 07 行的语句。上述代码如果是在非 DEBUG 时期,则将 DEBUG 的宏修改为 0 即可。这样程序的输出结果为:

there is not in debug!"


#if - #elif - #endif 指令

#if-#elif-#endif 语句与程序控制语句 if-else if 类似,即当前一个条件不满足后,可以再判断其他条件是否满足。#if-#elif-#endif 语句的使用方法如下:

#if expression1

    code1

#elif expression2

    coed2

#endif

如果 expression1 为非 0 值,则编译 code1 代码。如果不满足,则判断 expression2 是否为非 0。若 expression2 满足,则编译 code2 部分。#if-#elif-#endif 语句可以进行无限的拓展, 即#elif 部分可以根据需要进行拓展。例如:

#if expression1

    code1

#elif expression2

    coed2

#elif expression3

    coed3

......

#elif expressionN

coedN #endif

这种形式下,会依次判断 expression1、expression2...expressionN,直到有一个满足条件止。


#if-#else-#endif 语句用于两个分支的判断,而#if-#elif-#endif 语句用于多个分支的判断。#if-#elif-#endif 语句还可以和#else 进行组合,但一条#if 语句中最多出现一次#else,且必须出现在最后一个#elif 后,如:

#if expression1

    code1

#elif expression2

    coed2

#else code3

#endif


#ifdef - #endif 指令


#ifdef-#endif 指令用于判断程序代码中某符号是否已经定义。该语句的使用方法如下:

#ifdef identifier

    code

#endif

如果 identifier 为一个已定义的符号,则 code 就会被编译,否则剔除。identifier 通常是一个宏,该宏不一定需要宏体。它仅用于#ifdef-#endif 判断该宏代表的逻辑部分是否已经定义。例如,如下代码:

#ifdef CALC_AVERAGE

    int count=sizeof(data)/sizeof(data[0]);

    for(int i=0; i<count; i++)

    average += data;

    average /= count;

#endif

如果已经定义符号 CALCAVERAGE,则把#if 与#endif 间的语句放在要编译的源代码内,否则该段代码不会被编译。


#ifndef - #endif 指令

#ifndef-#endif 指令与#ifdef-#endif 类似,但#ifndef-#endif 用于判断程序代码中某符号

是否还未定义。#ifndef-#endif 的使用方法如下:

 #ifndef identifier

    code

#endif

如果 identifier 为一个未定义的符号,则 code 就会被编译,否则剔除。该语句的特点常用于解决不同文件中对同一头文件进行包含而导致的错误。该指令常常用于解决 C++程序中的文件重复包含问题。


例如,在 code.h 中包含了 another.h 头文件,如果在其他文件 code.cpp 中既包含了 code.h 又包含了 another.h,那么将会导致错误的发生。


文件重复包含的问题是难以避免的。为了解决这个问题,可以使用#ifndef-#endif 指令来实现。当出现包含语句时,可以使用该语句来判断文件是否已经包含。在 C++标准库中就大量用到了该语句,例如在 iostream 中有这样的代码:

#ifndef _IOSTREAM_ //判断是否已经定义_IOSTREAM_

#define _IOSTREAM_ //定义_IOSTREAM_

#include <istream> //包含istream #endif

在代码中,会对_IOSTREAM_这个宏进行判断。如果该宏不存在,则不定义宏 _IOSTREAM_,并且包含 istream。这样做的好处是,当程序按这种方法进行设计后,就不 用担心在其他代码中对该文件的重复包含。因此,不论其他代码中对 iostream 包含了多少次,仅在第一次包含时会对其进行编译,其后的包含由于_IOSTREAM_已经存在,则编译 器不会对其进行编译。


关注微信公号“书问”,快去免费领取符合你目标的图书吧!


内容来源:书问

作者池剑锋 等
出版清华大学出版社
定价59.8元
书籍比价

分享到

扫描二维码 ×

参与讨论

电子纸书

Boost程序库探秘——深度解析C++准标准库(第2版)

罗剑锋
清华大学出版社[2014] ¥62

Boost程序库探秘——深度解析C++准标准库

罗剑锋
清华大学出版社[2012] ¥32

工业设计程序与方法(高等院校工业设计专业“十二五”创新规划教材)

田野,王妮娜编著
辽宁科学技术出版社[2013] ¥14

微信小程序:分享微信创业2.0时代千亿红利

张翔
清华大学出版社[2017] ¥29

程序员考试全程指导

王鹏、祝宁
清华大学出版社[2010] ¥15

微信小程序:产品+运营+推广实战

牛建兵
清华大学出版社[2017] ¥29

Python程序设计开发宝典

董付国
清华大学出版社[2017] ¥45

话说程序调试

葛芝宾
清华大学出版社[2011] ¥12

出版业领先的TMT平台

使用社交账号直接登陆

Copyright © 2018 BookAsk 书问   |   京ICP证160134号


注册书问

一键登录

Copyright © 2018 BookAsk 书问   |   京ICP证160134号