【转载】C++ 的编译预处理

408 阅读7分钟

原文地址:C++ 的编译预处理

我对文章的格式进行了调整,并对关键部分进行了标注

C++中,在编译器对源程序进行编译之前,首先要由预处理对程序文本进行预处理。预处理器提供了一组预编译处理指令和预处理操作符。预处理指令实际上不是 C++ 语言的一部分,它只是用来扩充 C++ 程序设计的环境。所有的预处理指令在程序中都是以 # 来引导,每一条预处理指令单独占用一行,不要用分号结束。预处理指令可以根据需要出现在程序的位置。

先来看看一些预处理指令

image.png

C++提供的编译预处理功能主要有以下三种:
① 宏定义
② 文件包含
③ 条件编译

首先是宏定义

    C++ 宏定义将一个标识符定义为一个字符串,源程序中的该标识符均以指定的字符串来代替。因此预处理命令后通常不加分号。这并不是说所有的预处理命令后都不能有分号出现。由于宏定义只是用宏名对一个字符串进行简单的替换,因此如果在宏定义命令后加了分号,将会连同分号一起进行置换。

在 C++ 中,我们一般用 const 定义符号常量。很显然,用 const 定义常量比用 define 定义常量更好。

在使用宏定义时应注意的是:

(1) 在书写 #define 命令时,注意 <宏名> 和 <字符串> 之间用空格分开,而不是用等号连接。
(2) 使用 #define 定义的标识符不是变量,它只用作宏替换,因此不占有内存。
(3) 习惯上用大写字母表示<宏名>,这只是一种习惯的约定,其目的是为了与变量名区分,因为变量名通常用小写字母。

  如果某一个标识符被定义为宏名后,在取消该宏定义之前,不允许重新对它进行宏定义。取消宏定义使用如下命令:

#undef <标识符>

  其中,undef 是关键字。该命令的功能是取消对 <标识符> 已有的宏定义。被取消了宏定义的标识符,可以对它重新进行定义。
宏定义可以嵌套,已被定义的标识符可以用来定义新的标识符。例如:

  #define PI 3.14159265
  #define R 10
  #define AREA (PI*R*R)

1> #define 指令

#define 预处理指令是用来定义宏的。该指令最简单的格式是:首先声明一个标识符,然后给出这个标识符代表的代码。在后面的源代码中,就用这些代码来替代该标识符。这种宏把程序中要用到的一些全局值提取出来,赋给一些记忆标识符。

#define MAX_SIZE 10
int array[MAX_SIZE];
for (i = 0; i < MAX_SIZE; i++) /*……*/

在这个例子中,对于阅读该程序的人来说,符号 MAX_SIZE 就有特定的含义,它代表的值给出了数组所能容纳的最大元素数目。程序中可以多次使用这个值。作为一种约定,习惯上总是全部用大写字母来定义宏,这样易于把程序红的宏标识符和一般变量标识符区别开来。如果想要改变数组的大小,只需要更改宏定义并重新编译程序即可。

宏表示的值可以是一个常量表达式,其中允许包括前面已经定义的宏标识符。例如:

#define ONE 1
#define TWO 2
#define THREE (ONE+TWO)

注意上面的宏定义使用了括号。尽管它们并不是必须的。但出于谨慎考虑,还是应该加上括号的。例如:

six = THREE*TWO;

预处理过程把上面的一行代码转换成:

six = (ONE+TWO)*TWO;

结果为 6 ;正确

如果没有那个括号,就转换成

six = ONE+TWO*TWO;

结果为 5 ;错误

宏还可以代表一个字符串常量,例如:

#define VERSION "Version 1.0 Copyright(c) 2003"

又如:用预处理指令 #define 定义一个常数,表示一年有多少秒;

#define SECONDS_PER_YEAR (60*60*24*365)UL

(表达式将使一个 16 位机的整形数溢出,因此要用到长整型符号 L ,告诉编译器该数是一个无符号长整型数 UL )。

2> 带参数的 #define 指令

带参数的宏和函数调用看起来有些相似。看一个例子:

#define Cube(x) (x)*(x)*(x)

可以时任何数字表达式甚至函数调用来代替参数 x。这里再次提醒大家注意括号的使用。宏展开后完全包含在一对括号中,而且参数也包含在括号中,这样就保证了宏和参数的完整性。看一个用法:

int num = 8+2;
volume = Cube(num);

展开后为 (8+2)*(8+2)*(8+2);
如果没有那些括号就变为 8+2*8+2*8+2 了。

下面的用法是不安全的:

volume = Cube(num++);

如果 Cube 是一个函数,上面的写法是可以理解的。但是,因为 Cube 是一个宏,所以会产生副作用。这里的参数不是简单的表达式,它们将产生意想不到的结果。它们展开后是这样的:

volume = (num++)*(num++)*(num++);

很显然,结果是 10*11*12, 而不是 10*10*10;

那么怎样安全的使用 Cube 宏呢?必须把可能产生副作用的操作移到宏调用的外面进行:

int num = 8+2;
volume = Cube(num);
num++;

写一个标准宏 MIN  这个宏输入两个参数并返回较小的那个

#define MIN(A,B) ((A)<=(B)?(A):(B))   

获取键盘输入

#define KEY_DOWN(vk_code) (GetAsyncKeyState(vk_code)&0x80000?1:0)     

3> # 运算符

出现在宏定义中的 # 运算符把跟在其后的参数转换成一个字符串。有时把这种用法的 # 称为字符串化运算符。例如:

#define  PASTE(n) "adhfkj"#n
void main()         
{
    printf("%s\n", PASTE(15));          
}

宏定义中的 # 运算符告诉预处理程序,把源代码中任何传递给该宏的参数转换成一个字符串。所以输出应该是

adhfkj15

4> ## 运算符

## 运算符用于把参数连接到一起。预处理程序把出现在 ## 两侧的参数合并成一个符号。看下面的例子:

#define  NUM(a, b, c) a##b##c
#define  STR(a, b, c) a##b##c
void main()
{
    printf("%d\n", NUM(1, 2, 3));
    printf("%s\n", STR("aa", "bb", "cc"));
}

最后程序的输出为:

123
aabbcc

复杂一点的如

#define CC_READONLY_FUN(varType, varName, funName) \
public:                                            \
    void set##funName(varType var)                 \
    {                                              \
        varName = var;                             \
    }                                              \
    varType get##funName()                         \
    {                                              \
        return varName;                            \
    }                                              \
                                                   \
protected:                                         \
    varType varName;

那么 CC_READONLY_FUN(int, m_Size, Size)  就等同于

public:
    void setSize(int var)
    {
        m_Size = var;
    }

    int getSize()
    {
        return m_Size;
    }

protected:
    int m_Size

千万别担心,除非需要或者宏的用法恰好和手头的工作相关,否则很少有程序员会知道 ## 运算符。绝大多数程序员从来没用过它,但是还是要理解一下的。

然后是文件包含

#include 预处理指令的作用是在指令处展开被包含的文件。包含可以是多重的,也就是说一个被包含的文件中还可以包含其他文件。标准 C 编译器至少支持八重嵌套包含。

预处理过程不检查在转换单元中是否已经包含了某个文件并阻止对它的多次包含。这样就可以在多次包含同一个头文件时,通过给定编译时的条件来达到不同的效果。例如:

#define  AAA
#include  "t.c"
#undef  AAA
#include  "t.c"

为了避免那些只能包含一次的头文件被多次包含,可以在头文件中用编译时条件来进行控制。例如:

/*my.h*/
#ifndef MY_H
#define MY_H
……
#endif

在程序中包含头文件有两种格式:

#include <my.h>
#include "my.h"

第一种方法是用尖括号把头文件括起来。这种格式告诉预处理程序在编译器自带的或外部库的头文件中搜索被包含的头文件。第二种方法是用双引号把头文件括起来。这种格式告诉预处理程序在当前被编译的应用程序的源代码文件中搜索被包含的头文件,如果找不到,再搜索编译器自带的头文件。\

采用两种不同包含格式的理由在于,编译器是安装在公共子目录下的,而被编译的应用程序是在它们自己的私有子目录下的。一个应用程序既包含编译器提供的公共头文件,也包含自定义的私有头文件。采用两种不同的包含格式使得编译器能够在很多头文件中区别出一组公共的头文件

最后是条件编译指令

使用条件编译指令,可以限定程序中的某些内容要在满足一定条件的情况下才参与编译。因此,利用条件编译可以使同一个源程序在不同的编译条件下产生不同的目标代码。例如,可以在调试时增加一些调试语句,以达到跟踪的目的,并利用条件编译指令,限定当程序调试后,重新编译时,使调试语句不参与编译。常用的条件编译语句有下列 5 种形式:

形式一

image.png

形式二

image.png

形式三

image.png

形式四

image.png

如果标识符经 #defined 定义过,且未经 #undef 删除,则编译程序段1,否则编译程序段 2. 如果没有程序段 2 ,则 #else 可以省略:

image.png

形式五

image.png

如果 “标识符” 未被定义过,则编译程序段 1 ,否则编译程序段 2 . 如果没有程序段 2 ,则 #else 可以省略:

image.png

至此,结束。