1.7 预处理
C++提供的预处理功能主要有以下4种:宏定义、文件包含、条件编译和布局控制。文件包含在前面已描述过,下面重点描述宏定义、条件编译和布局控制,其中又着重讲述常用宏定义命令、do…while(0)的妙用、条件编译及extern"C"块的应用知识。
1.?常用宏定义命令
#def?ine命令是一个宏定义命令,它用来将一个标识符定义为一个字符串,该标识符被称为宏名,被定义的字符串称为替换文本。该命令有两种格式:一种是简单的宏定义,另一种是带参数的宏定义。
简单的宏定义的声明格式如下所示:
#define 宏名 字符串
例:#def?ine PI 3.1415926
带参数的宏定义的声明格式如下所示:
#define 宏 (参数表列) 宏
例:#def?ine A(x) x
使用宏定义中,要注意以下问题。
(1)在简单宏定义的使用中,当替换文本所表示的字符串是一个表达式时,需要加上括号,否则容易引起误解和误用。
【例1.17】 简单宏定义不加括号容易引起误用。
#include<iostream>
#define N 2+9
using namespace
std;
int main(){
int a=N*N;
cout<<a<<endl;
return 0;
}
程序的执行结果是:
29
例1.17中就出现了问题:在此程序中存在着宏定义命令,宏N代表的字符串是2+9,在程序中有对宏N的使用,一般同学在读该程序时,容易产生的问题是先求解N为2+9=11,然后在程序中计算a时使用乘法,即N*N=11*11=121,其实该题的结果为29,为什么结果有这么大的偏差?因为宏展开是在预处理阶段完成的,这个阶段把替换文本只是看作一个字符串,并不会有任何的计算发生,在展开时是在宏N出现的地方只是简单地使用串2+9来代替N,并不会增添任何的符号,所以对该程序展开后的结果是a=2+9*2+9,计算后结果为29。要程序如之前想要的结果,只需要写成#def?ine
N (2+9),即加上括号就行。
(2)类似地,在带参数的宏定义的使用中,也容易引起误解。例如当需要使用宏替换来求任何数的平方,这时就需要使用参数,以便在程序中用实际参数来替换宏定义中的参数。初学者容易写成如例1.18中的形式。
【例1.18】 带参数的宏定义不加括号容易引起误用。
#include<iostream>
using namespace
std;
#define area(x)
x*x
int main()
{
int y = area(2+2);
cout<<y<<endl;
return 0;
}
程序的执行结果是:
8
表面上看,给的参数是2+2,所得的结果应该为4*4=16,但该程序的实际结果为8。宏定义中要遵循先替换后计算的原则,在上面的程序里,2+2即为宏area中的参数,应该由它来替换宏定义中的x,即替换成2+2*2+2=8了。那如果遵循(1)中的解决办法,把2+2括起来,即把宏体中的x括起来,是否可以解决呢?#def?ine area(x) (x)*(x),对于area(2+2),替换为(2+2)*(2+2)=16,可以解决,但是对于area(2+2)/area(2+2)又会怎么样呢,有人一看到这道题马上给出结果1,因为分子分母一样,那么这样就又错了。遵循先替换再计算的规则,这道题替换后会变为(2+2)*(2+2)/(2+2)*(2+2)即4*4/4*4按照乘除运算规则,结果为16/4*4=4*4=16。解决这类问题的方法是在整个宏体上再加一个括号,即#def?ine area(x) ((x)*(x)),不要觉得这没必要,没有它是不行的。
要想能够真正使用好宏定义,在读别人的程序时,一定要记住先将程序中对宏的使用全部替换成它所代表的字符串,不要自作主张地添加任何其他符号,完全展开后再进行相应的计算,就不会求错运行结果。
如果是自己在编程时使用宏替换,则在使用简单宏定义时,当字符串中不只一个符号时,加上括号表现出优先级,如果是带参数的宏定义,则要给宏体中的每个参数加上括号,并在整个宏体上再加一个括号。
2.?do...while(0)的妙用
大家都知道,do{…}while(condition)可以表示循环,但你有没有遇到在一些宏定义中可以不用循环的地方,也用到了do{…}while,比如有这样的宏:
#define Foo(x)
do{\
statement one;\
statement two;\
}while(0) // 这里没有分号
粗看会觉得很奇怪,既然循环里面只执行了一次,那要这个看似多余的do...while(0)有什么意义呢?再来看这样的宏:
#define Foo(x)
{\
statement one;\
statement two;\
}
这两个看似一样的宏,其实是不一样的。前者定义的宏是一个非复合语句,而后者却是一个复合语句。假如有这样的使用场景:
if(conditon)
Foo(x);
else
...;
因为宏在预处理的时候会直接被展开,采用第2种写法,会变成:
if(condition)
statement one;
statement two;
else
...///
这样会导致else语句孤立而出现编译错误。加了do{...}while(0),就使得宏展开后,仍然保留初始的语义,从而保证程序的正确性。
3.?条件编译
一般情况下,源程序中所有行的语句都参加编译。但是有时程序员希望其中一部分内容只在满足一定条件时才进行编译,也就是对一部分内容指定编译的条件,这就用到了“条件编译”。
条件编译命令最常见的形式为:
#ifdef 标识符
程序段1
#else
程序段2
#endif
它的作用是:当标识符已经被定义过(一般是用#def?ine命令定义),则对程序段1进行编译,否则编译程序段2。其中#else部分也可以没有,即:
#ifdef 标识符
程序段1
#endif
下面这样的形式则是当指定的表达式值为真(非零)时就编译程序段1,否则编译程序段2。可以事先给定一定条件,使程序在不同的条件下执行不同的功能。
#if 表达式
程序段1
#else
程序段2
#endif
这里的“程序段”可以是语句组,也可以是命令行。
有时候程序中的某些调试代码,只需要在调试的时候被编译,而不希望在程序的正式发行版中被编译,你可能会看到类似例1.19这样的代码段。
【例1.19】 调试代码巧用条件编译。
#include<iostream>
using namespace
std;
#define _DEBUG_
int main(){
int x=10;
#ifdef _DEBUG_
cout<<"File:"<<
__FILE__<<",Line:"<<
__LINE__<<",x:"<<x<<endl;
#else
printf("x = %d\n", x);
cout<<x<<endl;
#endif
return 0;
}
程序的运行结果是:
File:test.cpp,Line:7,x:10
当_DEBUG_没有被定义的时候,仅编译#else与#endif之间的代码;当定义了_DEBUG_符号之后,则会编译#ifdef与#else之间的代码。要想定义一个符号很简单,只需要在文件头部加上像这样的一条语句:
#define _DEBUG_
用#def?ine命令的目的不在于用_DEBUG_代表一个字符串,而只是表示已定义过_DEBUG_,因此_DEBUG_后面写什么字符串都无所谓,甚至可以不写字符串。
4.?extern
"C"块的应用
经常能在C与C++混编的程序中看到这样的语句:
#ifdef
__cplusplus
extern "C" {
#endif
...
#ifdef __cplusplus
}
#endif
其中,__cplusplus是C++的预定义宏,表示当前开发环境是C++。在C++语言中,为了支持重载机制,在编译生成的汇编代码中,会对函数名字进行一些处理(通常称为函数名字改编),如加入函数的参数类型或返回类型等,而在C语言中,只是简单的函数名字而已,并不加入其他信息,如下所示:
int func(int
demo);
int func(double
demo);
C语言无法区分上面两个函数的不同,因为C编译器产生的函数名都是_func,而C++编译器产生的名字则可能是_func_Fi和_func_Fd,这样就很好地把函数区别开了。
所以,在C/C++混合编程的环境下,extern
"C"块的作用就是告诉C++编译器这段代码要按C标准编译,以尽可能地保持C++与C的兼容性。例1.20说明了__cplusplus的使用方法。
【例1.20】 __cplusplus的使用方法。
#include<stdio.h>
int main() {
#define TO_LITERAL(text) TO_LITERAL_(text)
#define TO_LITERAL_(text) #text
#ifndef __cplusplus
/* this translation unit is being treated
as a C one */
printf("a C program\n");
#else
/*this translation unit is being treated
as a C++ one*/
printf("a C++ program\n__cplusplus
expands to \""
TO_LITERAL(__cplusplus)
"\"\n");
#endif
return 0;
}
程序的执行结果是:
a C++ program
__cplusplus
expands to "1"
例1.20中程序的意思是:如果没有定义__cplusplus,那么当前源代码就会被当作C源代码处理;如果定义了__cplusplus,那么当前源代码会被当中C++源代码处理,并且输出__cplusplus宏被展开后的字符串。