C/C++预处理指令总结

时间:2023-01-21 17:52:27

前言

C/C++源码需要经过四个阶段才能得到可执行程序,这个四个阶段依次是:预处理、编译、汇编、链接。具体过程可以参考一文看懂C/C++编译过程以及g++编译选项。今天重点讲一下预处理指令,显然这些指令都是在预处理阶段起作用。

预处理指令可以分为三大类:宏定义、include指令以及条件编译指令

1 宏定义define

宏定义,即define指令。所谓宏,就是C/C++源程序中,允许使用一个标识符来表示一个字符串,即宏。这个标识符称为宏名,源程序中所有用到宏名的地方,预处理时都会替换为后面的字符串。

1.1 不带参数宏定义

基本格式为:

#define 宏名 字符串。

字符串可以是常数、表达式、格式字符串。举例如下:

#define PI 3.14      // 字符串为常数
#define AREA PI*2*2  // 字符串为表达式
#define IP "192.254.1.16" // 字符串为格式字符串

宏定义用法比较简单,但是也有几点需要注意:

  • 宏定义中,宏名应该简单且有明确意义。宏名习惯用大写字母表示,以便与变量名分开

  • 宏名后面的字符串可以包含任何字符,预处理时对它不作任何语法检查,语法错误编译时才能发现

  • 宏定义不是说明语句,末尾不需要加分号,否则连分号一起替换

  • 宏定义之后,也可以成为为其它宏定义的一部分。比如AREA包含PI宏定义

另外,宏定义的作用域为宏定义命令起到源程序结束,如果要提前终止,可以使用#undef,如:

#define PI 3.14
// 这一段为PI 作用域
#undef PI
// 这一段不属于PI的作用域

1.2 带参数宏定义

和函数一样,宏定义也可以带参数。在替换的时候,参数也会跟着一起替换。定义形式为:

#define 宏名(形参表) 字符串 需要特别注意:宏名和形参之间不能有空格

举例如下:

#define FUNC(a, b) 2*(a) + (b)   // 宏定义
int k = FUNC(1,3);  // 宏调用
// 替换后的结果如下
k = 2 * (1) + (3)

要特别注意,如果不对宏定义中参数加上括号,结果可能是错误的。例如,对于上面的宏定义,不括号:

#define FUNC(a, b) 2*a + b   // 宏定义
int k = FUNC(1 + 5,3);  // 宏调用
// 替换后的结果如下
k = 2 * 1 + 5 + 3
// 本意是希望做如下替换
k = 2 * (1 + 5) + 3

2 include指令

include指令常用于头文件的包含(实际上,包含其它文件也是可以的,它才不管具体文件是啥)。

// main.cpp
#include <iostream>
#include "add.h"

在预处理的时候,会将iostream以及add.h两个文件对应的具体内容替换main.cpp中来。可以看到上面两种文件包含,一种使用了<>,一种使用了"",这两种包含方式的区别,可以参考C/C++尖括号和双引号包含头文件的区别

3 条件编译指令

预处理程序提供了条件编译的功能,一般情况下,源程序中的所有代码都是参与编译的。但是,有时希望只对其中一部分内容在进行一定条件的基础之上才进行编译。

这时就需要用到条件编译指令,这样编译程序就按不同的条件去编译程序不同的部分,因而产生不同的目标代码文件,对程序的移植和调试是很有帮助的。

3.1 #if指令

基本格式如下:

#if 常量表达式1
    语句段1
#elif 常量表达式2
    语句段2
#else 
    语句段3
#endif

#if表达式的基本含义是,和if语句一样,从前往后判断,哪一个分支的表达式的值为真,即编译那个分支,并跳过其它分支。

常量表达式可以包含宏、算术运算和逻辑运算。

3.2 #ifdef以及#ifndef指令

前面介绍的#if条件编译指令中,需要判断常量表达式的具体值,有时候并不需要判断具体的值,只需要判断这个符号常量有没有定义即可。这时就需要用到#ifdef以及#ifndef指令,含义是如果有定义和如果没有定义。基本格式如下:

#ifdef 宏名
    语句段1;
#else 
    语句段2;
#endif

含义是,如果宏名有定义,则对语句段1进行编译,否则对语句段2进行编译。ifdef换成ifndef则表示相反的含义。

具体例子:

#include <iostream>
using namespace std;
#define DEBUG
int main()
{
#ifdef DEBUG
    cout << "DEBUG is defined\n";
#else 
    cout << "DEBUG is undefined\n";
#endif
#ifndef LOG
    cout << "LOG is undefined\n";
#else 
    cout << "LOG is defined\n";
#endif

    return 0;
}

打印如下:、

DEBUG is defined
LOG is undefined

值得一提的是,#if指令中也可以包含对宏定义的判断,但是要借助defined。例如:

#if defined(_GLIBCXX_HAS_GTHREADS) && defined(_GLIBCXX_USE_C99_STDINT_TR1)
    语句段1;
#endif

如果上面的#if指令要换成#ifdef指令的话,需要嵌套定义:

// 相当于下面
#ifdef _GLIBCXX_HAS_GTHREADS
    #ifdef _GLIBCXX_USE_C99_STDINT_TR1
       语句段1;
    #endif
#endif

3.3 #undef指令

前面将#define指令的时候已经提到,#undef可以使宏定义失效。

#define SIZE 100
...
#undef SIZE

从第3行开始宏定义SIZE就失效了。

3.4 其它已经定好的宏

ANSI标准中已经给出一些定义好的宏,这些宏代表不同的含义。列举如下:

_LINE_ :当前被编译代码的行号
_FILE_ :当前源文件的名称
_DATE_ :当前源程序的创建日期
_TIME_ :当前源文件的创建时间
_STDC_ :判断当前编译器是否为标准c