c/c++中,预编译指令用法汇总

606 阅读7分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

毕竟西湖六月中,风光不与四时同。—— 南宋·杨万里

1 概述

如下图所示,一般来说c/c++ 程序的编译过程分为如下几个阶段:预处理编译汇编链接。其中预处理阶段,读取c源程序,对其中的伪指令(以#开头的指令)和特殊符号进行处理。或者说是扫描源代码,对其进行初步的整理和转换,产生新的源代码(还是文本文件)提供给编译器。预处理过程先于编译器对源代码进行处理。目前绝大多数编译器都包含了预处理程序,但通常认为它们是独立于编译器的。预处理过程读入源代码,检查包含预处理指令的语句和宏定义,并对源代码进行相应的转换。预处理过程还会删除程序中的注释和多余的空白字符

预处理过程由独立的程序执行,与 c/c++语言无关,故而遵循与c/c++不同的语法规则。预处理语句遵循以下几个语法规则

  • 预处理指令必须为所在行的第一个非空白字符;
  • 一条完整的预处理指令必须处于同一行中;
  • 预处理指令与 c/c++ 语句不同,在指令末尾不应该加入分号( ';' )。

预处理程序依次扫描源文件,并对遇到的预处理指令进行处理,直到扫描完所有源文件内容,完成预处理过程,经过预处理过程的文件一般使用 .i 作为后缀。

2 预编译指令

本文总结的预编译指令如下,下面将逐个讨论分析。

#define   //宏定义命名,定义一个标识符来表示一个常量
#include  //文件包含命令,用来引入对应的头文件或其他文件
#undef    //来将前面定义的宏标识符取消定义
#ifdef    //条件编译
#ifndef   //条件编译
#if       //条件编译
#else     //条件编译
#elif     //条件编译
#endif    //条件编译
#error    //用于生成一个编译错误消息

__DATE__  //当前日期,一个以 “MMM DD YYYY” 格式表示的字符串常量
__TIME__  //当前时间,一个以 “HH:MM:SS” 格式表示的字符串常量。
__FILE__  //这会包含当前文件名,一个字符串常量。
__LINE__  //这会包含当前行号,一个十进制常量。
__STDC__  //当编译器以 ANSI 标准编译时,则定义为 1;判断该文件是不是标准 C 程序。

2.1 #define

#define又称宏定义,标识符为所定义的宏名,简称宏。#define指令可以认为是给表达式"起"一个别名,在预处理器进行处理时,会将所有出现别名的地方替换为对应的表达式,表达式可以是数字、字符串、计算表达式。其特点是定义的标识符不占内存,只是一个临时的符号,预编译后这个符号就不存在了。其一般用法如下:

#define  标识符  表达式   //注意, 最后没有分号

/*例子*/
#define MACRO EXPRESSION  //预处理在源程序中遇到MACRO时,会将其替换为EXPRESSION

在使用#define语句时,有几个地方需要注意:

  • 预处理程序仅进行字符对象的替换,即将字符串MACRO替换为字符串EXPRESSION,并不会对替换的内容进行语义解析,故而在使用#define定义常量的别名时应该注意直接替换是否会造成潜在的语义改变;
  • #define指令将MACRO后的第一个空白字符作为MACROEXPRESSION的分界,EXPRESSION部分对应为自MACRO后第一个空白字符开始到行尾换行符的所有内容。例如在#define后面加上错误的分号(;),也会被宏替换进去;
  • #define指令还可以定义接收参数的宏,用于定义某些重复使用但又比较简单的计算流程,比如进行两个数大小的比较。

2.2 #include

#include叫做文件包含命令,用来引入对应的头文件(.h文件)或其他文件。其一般有两种形式,#include <stdio,h>#include "stdio.h"。当预处理器遇到#include指令时,会将该指令指定的头文件内容复制到源文件 #include指令所在的位置,即使用指定头文件的内容替换#include指令所在行。

使用尖括号< >和双引号" "的区别在于头文件的搜索路径不同:

  • 使用尖括号< >,编译器会到系统路径下查找头文件;
  • 而使用双引号" ",编译器首先在当前目录下查找头文件,如果没有找到,再到系统路径下查找;
  • 使用双引号比使用尖括号多了一个查找路径,它的功能更为强大。

2.3 #undef

#undef移除(取消定义)之前使用#define创建的标识符(宏)。因此,后续出现的标识符(宏)被预处理器忽略。若要使用#undef删除带参数标识符(带参数宏),请仅#undef 标识符,不用带参数列表。如下代码示例:

#define WIDTH 80
#define ADD( X, Y ) ((X) + (Y))
.
.
.
#undef WIDTH
#undef ADD

2.4 条件编译(一)

#if指令包含#elif#else#endif指令,控制源文件部分编译。当某个条件表达式的值为真时,则预处理器会将对应的代码片段包含进源文件中,而其他部分则被直接忽略。在源文件中,每个#if指令都必须由结束的#endif指令匹配。在#if指令和#endif指令之间可以有任意数目的#elif指令,但最多只允许有一个#else指令。如果存在#else指令,然后其后面只能接上#endif指令,如下代码所示。要强调的一点是,预处理指令均由预处理器进行处理,所以其支持的判断表达式与 c/c++ 本身支持的表达式有所区别。预处理指令中条件判断中的条件表达式仅可以包括 #define定义的常量、整型、以及这些量构成的算数和逻辑表达式 (可以看到 c/c++ 程序中定义的变量是不被支持的,同时也不支持对浮点型的判断 )。


注意如果要完成多个宏定义控制同一代码分支的功能,可以使用如下例子2的写法,#if defined TEST1 || defined TEST2

#if 条件表达式1

    code1

#elif 条件表达式2

    code2

#elif 条件表达式3

    code3

#else
    code4
#endif

/*例子1*/
#define OPTION 2
 
#if OPTION == 1
cout << "Option: 1" << endl;
#elif OPTION == 2
cout << "Option: 2" << endl; //选择这句
#else
cout << "Option: Illegal" << endl;
#endif

/*例子2*/
/* TEST1 或 TEST2被定义,则选择执行printf1,否则执行printf2 */
#if defined TEST1 || defined TEST2
	printf1(".....");
#else
	printf2(".....");
#endif

/* TEST1 或 TEST2未被定义,则选择执行printf1,否则执行printf2 */
#if !defined TEST1 || !defined TEST2
	printf1(".....");
#else
	printf2(".....");
#endif

2.5 条件编译(二)

预处理指令#ifndef#ifdef的效果等价于指令#ifdefined运算符一同使用的场合。如下代码所示,例子1展示了两种等价的写法。但是如果要完成多个宏定义控制同一代码分支的功能,还是需要用#if defined TEST1 || defined TEST2的写法,如条件编译(一) 章节所述。

/*上面两个的写法等价于下面的两个写法*/
#ifdef 宏定义
#ifndef 宏定义

#if defined 宏定义
#if !defined 宏定义

/*例子1*/
#if defined( TEST )
    code
#endif

#ifdef TEST   
    code
#endif

#if !defined( TEST )
    code
#endif

#ifndef TEST   
    code
#endif

2.6 #error

#error指令将使编译器显示一条错误信息,然后停止编译,用法如下。在代码分支较多时,无法判断编译哪一个代码分支,可以用#error指令进行标记。当然在实际工作中,很多时候是写一段乱代码在分支中,看是否有编译报错来判断。但最好是使用已经设计好的#error指令,其可以显示一条自定义报错信息。

#if !defined(__cplusplus)
#error C++ compiler required.
#endif

2.8 特殊符号

预编译程序可以识别一些特殊符号。预编译程序对于在源程序中出现的这些特殊符号将用合适的值进行替换。这些特殊符号包括:__DATE____TIME____FILE____LINE____STDC__。注意,是双下划线,而不是单下划线 :

  • __FILE__包含当前程序文件名的字符串;
  • __LINE__表示当前行号的整数;
  • __DATE__包含当前日期的字符串;
  • __STDC__如果编译器遵循ANSI C标准,它就是个非零值;
  • __TIME__包含当前时间的字符串。
#include<stdio.h>

int main()
{
	printf("Hello World!\n");
	printf("%s\n", __FILE__);
	printf("%d\n", __LINE__);
	printf("%s\n", __DATE__);
	printf("%d\n", __TIME__);
	printf("%d\n", __STDC__);

	return 0;
}

2.8 讨论#和##

字符串化运算符#将宏参数转换为字符串文本;标记粘贴运算符##把两个参数粘贴在一起,其含义就是粘贴之后所形成标识符的定义。如下例子1,定义了一个带参数的宏paster(n),在调用paster(9);后,宏展开为printf_s( "token" #9 " = %d", token##9 );#9的含义为字符串"9",token##9的含义为token9,其是一个标识符,类型为int,值为9。如下例子2#define STR(s) #s,利用#可以很轻松定义出一个字符串转换函数。

/*例子1*/
#include <stdio.h>
#define paster(n) printf_s( "token" #n " = %d", token##n )
int token9 = 9;

int main()
{
   paster(9); //输出:token9 = 9
}

/*例子2*/
#define STR(s)     #s

2.9 杂项

2.9.1 多行宏定义的使用

\是续行操作符,也就是宏定义一行写不完,需要多行写,就需要在每一行的后面加上续行操作符,注意字符\后要紧跟回车键,中间不能有空格或其他字符。

#define __HAL_RCC_GPIOC_CLK_ENABLE()   do { \
                                        __IO uint32_t tmpreg; \
                                        SET_BIT(RCC->IOPENR, RCC_IOPENR_GPIOCEN);\
                                        /* Delay after an RCC peripheral clock enabling */ \
                                        tmpreg = READ_BIT(RCC->IOPENR, RCC_IOPENR_GPIOCEN);\
                                        UNUSED(tmpreg); \
                                      } while(0)

3 总结

  • c/c++ 程序的编译过程分为如下几个阶段:预处理编译汇编链接
  • 预处理过程由独立的程序执行,与c/c++语言无关,故而遵循与c/c++不同的语法规则;
  • 如果要完成多个宏定义控制同一代码分支的功能,可以使用如下的写法,#if defined TEST1 || defined TEST2
  • 预编译程序可以识别一些特殊符号,这些特殊符号包括:__DATE____TIME____FILE____LINE____STDC__
  • 字符串化运算符#将宏参数转换为字符串文本;标记粘贴运算符##把两个参数粘贴在一起,其含义就是粘贴之后所形成标识符的定义。