C++ 笔记(32)— 预处理、文件包含include、宏替换define、条件包含ifndef、define

本文详细介绍了C/C++预处理器的功能,包括删除注释、处理#include指令、宏定义(如#define、宏替换、参数化宏)以及条件编译(如#if、#ifdef等)。还探讨了文件包含的用法及其可能带来的问题,如多重包含,并展示了如何使用条件编译防止此类问题。预处理器在源代码编译前进行文本替换,对于理解和优化C/C++程序的编译过程至关重要。
摘要由CSDN通过智能技术生成

C/C++预处理器在源代码编译之前对其进行一些文本性质的操作。 它的主要任务包括删除注释 、 插入 #include 指令包含的文件的内容 、 定义和替换由 #defme 指令定义的符号以及确定代码的部分内容是否应该根据一些条件编译指令进行编译 。

从概念上讲,预处理器是编译过程中单独执行的第一个步骤。两个最常用的预处理器指令是:

  • #include指令( 用于在编译期间把指定文件的内容包含进当前文件中);
  • #define指令( 用任意字符序列替代一个标记);

1. 预定义符号

预定义符号
示例代码

int main() 
{
    cout << "__FILE__:" << __FILE__ <<  endl;   // __FILE__:main.cpp
    cout << "__LINE__:" << __LINE__ <<  endl;   // __LINE__:11
    cout << "__DATE__:" << __DATE__ <<  endl;   // __DATE__:Jul  1 2021
    cout << "__TIME__:" << __TIME__ <<  endl;   // __TIME__:11:57:34
    cout << "__STDC__:" << __STDC__ <<  endl;   // __STDC__:1
    return 0;
}

2. 宏替换

宏定义的形式如下:

#define 名字  要替换的文本

这是一种最简单的宏替换——后续所有出现名字记号的地方都将被替换为替换文本。 #define指令中的名字与变量名的命名方式相同,替换文本可以是任意字符串。通常情况下,#define指令占一行,替换文本是 #define指令行尾部的所有剩余部分内容,但也可以把一个较长的宏定义分成若干行,这时需要在待续的行末尾加上一个反斜杠符 \

#define指令定义的名字的作用域从其定义点开始,到被编译的源文件的末尾处结束。宏定义中也可以使用前面出现的宏定义。替换只对记号进行,对括在引号中的字符串不起作用。例如,如果 YES是一个通过 #define指令定义过的名字,则在 printf( "YES")YESMAN中将不执行替换。

替换文本可以是任意的,例如:

#define forever for (;;) / * 无限循环 */

该语句为无限循环定义了一个新名字 forever

宏定义也可以带参数,这样可以对不同的宏调用使用不同的替换文本。例如,下列宏定义定义了一个宏 max:

#define max(A, B) ((A) > (B) ?  (A) : (B) )

使用宏 max 看起来很像是函数调用,但宏调用直接将替换文本插入到代码中。形式参数( 在此为A或B ) 的每次出现都将被替换成对应的实际参数。因此,语句:

x = max(p+q, r+s) ;

将被替换为下列形式:

x = ((p+q) > (r+s) ?  (p+q) : (r+s) )

仔细考虑一下 max的展开式,就会发现它存在一些缺陷。其中,作为参数的表达式要重复计算两次,如果表达式存在副作用( 比如含有自增运算符或输入/ 输出),则会出现不正确的情况。例如:

max(i++, j++)

它将对每个参数执行两次自增操作。同时还必须注意,要适当使用圆括号以保证计算次序的正确性。考虑下列宏定义:

#define square(x) x*x

当用 square(a+1)调用该宏定义时就会出现问题。

可以通过 #undef指令取消名字的宏定义,这样做可以保证后续的调用是函数调用,而不是宏调用:

#undef getchar
int getchar(void) 
{
    ...
}

3. 条件编译

使用条件编译,你可以选择代码的一部分是被正常编译还是完全忽略,用于支持条件编译的基本结构是 #if 指令和与其匹配的 #endif 指令 。

#if语句对其中的常量整型表达式(其中不能包含 sizeof、类型转换运算符或 enum常量)进行求值,若该表达式的值不等于 0,则包含其后的各行,直到遇到 #endif#elif#else语句为止(预处理器语句 #elif类似于 else if)。

#if语句中可以使用表达式 defined(名字),该表达式的值遵循下列规则:当名字已经定义时,其值为 1 ;否则,其值为 0 。

例如,为了保证 hdrh文件的内容只被包含一次,可以将该文件的内容包含在下列形式的条件语句中:

#if !defined ( HDR )
#define HDR
/* hdr.h 文件的内容放在这里 */
#endif

第一次包含头文件 hdrh时,将定义名字 HDR;此后再次包含该头文件时,会发现该名字已经定义,这样将直接跳转到 #endif处。类似的方式也可以用来避免多次重复包含同一文件。

如果多个头文件能够一致地使用这种方式,那么,每个头文件都可以将它所依赖的任何头文件包含进来,用户不必考虑和处理头文件之间的各种依赖关系。

下面的这段预处理代码首先测试系统变量 SYSTEM,然后根据该变量的值确定包含哪个版本的头文件:

#if SYSTEM == SYSV
	#define HDR "sysv.h"
#elif SYSTEM == BSD
	#define HDR "bsd.h"
#elif SYSTEM == MSDOS
	#define HDR "msdos.h"
#else
	#define HDR "default.h"
#endif

# include HDR

C/C++ 语言专门定义了两个预处理语句 #ifdef#ifndef 定义。上面有关 #if的第一个例子可以改写为下列形式:

#ifndef HDR
#define HDR
/* hdr.h 文件的内容放在这里 */
#endif

4. 文件包含

4.1 库文件和本地文件包含

文件包含指令( 即 #include指令)使得处理大量的 #define指令以及声明更加方便。在源文件中,任何形如:

#include “文件名”

或者

#include <文件名>

的行都将被替换为由文件名指定的文件的内容。

  • 如果文件名用引号引起来,则在源文件所在位置査找该文件;
  • 如果在该位置没有找到文件,或者如果文件名是用尖括号 <>括起来的,则将根据相应的规则查找该文件,这个规则同具体的实现有关;

被包含的文件本身也可包含 #include指令。

源文件的开始处通常都会有多个 #include指令,它们用以包含常见的 #define语句和 extern声明,或从头文件中访问库函数的函数原型声明,比如 <stdio.h>

在大的程序中, #include指令是将所有声明捆绑在一起的较好的方法。它保证所有的源文件都具有相同的定义与变量声明,这样可以避免出现一些不必要的错误。很自然,如果某个包含文件的内容发生了变化,那么所有依赖于该包含文件的源文件都必须重新编译。

4.2 嵌套文件包含

嵌套 #include 文件的缺点

  • 它使得我们很难判断源文件之间的真正依赖关系 ;
  • 一个头文件可能会被多次包含;

为了说明这 种错误,看下面代码:

#include "x.h"
#include "x.h"

显然 , 这里文件 x.h 被包含了两次 。 没有人会故意编写这样的代码 。 但下面的代码

#include "a.h"
#include "b.h"

看上去没什么问题 。如果 a.hb.h 都包含一个嵌套的 #include "x.h",那么 x.h 在此处也同样出现了两次,只不过它的形式不是那么明显而已 。

多重包含在绝大多数情况下出现于大型程序中,它往往需要使用很多头文件,因此要发现这种情况并不容易。要解决这个问题 , 我们可以使用条件编译 。

#ifndef _HEADERNAME_H
#define _HEADERNAME_H
/* hdr.h 文件的内容放在这里 */
#endif
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

wohu007

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值