毕竟西湖六月中,风光不与四时同。—— 南宋·杨万里
1 概述
如下图所示,一般来说c/c++ 程序的编译过程分为如下几个阶段:预处理、编译、汇编、链接。其中预处理阶段,读取c源程序,对其中的伪指令(以#开头的指令)和特殊符号进行处理。或者说是扫描源代码,对其进行初步的整理和转换,产生新的源代码(还是文本文件)提供给编译器。预处理过程先于编译器对源代码进行处理。目前绝大多数编译器都包含了预处理程序,但通常认为它们是独立于编译器的。预处理过程读入源代码,检查包含预处理指令的语句和宏定义,并对源代码进行相应的转换。预处理过程还会删除程序中的注释和多余的空白字符[1]。
预处理过程由独立的程序执行,与 c/c++语言无关,故而遵循与c/c++不同的语法规则。预处理语句遵循以下几个语法规则[2]:
- 预处理指令必须为所在行的第一个非空白字符;
- 一条完整的预处理指令必须处于同一行中;
- 预处理指令与 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后的第一个空白字符作为MACRO与 EXPRESSION的分界,EXPRESSION部分对应为自MACRO后第一个空白字符开始到行尾换行符的所有内容。例如在#define
后面加上错误的分号(,也会被宏替换进去;
#define
指令还可以定义接收参数的宏,用于定义某些重复使用但又比较简单的计算流程,比如进行两个数大小的比较。
2.2 #include
#include
叫做文件包含命令,用来引入对应的头文件(.h文件)或其他文件。其一般有两种形式,#include
和#include “stdio.h”
。当预处理器遇到#include
指令时,会将该指令指定的头文件内容复制到源文件 #include
指令所在的位置,即使用指定头文件的内容替换#include
指令所在行。
使用尖括号< >
和双引号" “
的区别在于头文件的搜索路径不同:[3]
- 使用尖括号
< >
,编译器会到系统路径下查找头文件;
- 而使用双引号
” "
,编译器首先在当前目录下查找头文件,如果没有找到,再到系统路径下查找;
- 使用双引号比使用尖括号多了一个查找路径,它的功能更为强大。
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
。
#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
的效果等价于指令#if
与defined
运算符一同使用的场合。如下代码所示,例子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
包含当前时间的字符串。
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
#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
;
- 字符串化运算符
#
将宏参数转换为字符串文本;标记粘贴运算符##
把两个参数粘贴在一起,其含义就是粘贴之后所形成标识符的定义。
关注公众号:橙子随记,一起交流、探讨、学习!!!
参考资料
[1]
编译及编译预处理: https://www.cnblogs.com/rusty/archive/2011/03/27/1996806.html
[2]
预处理指令: https://www.cnblogs.com/yhjoker/p/12228761.html
[3]
c语言中文网: http://c.biancheng.net/view/1975.html
发布于 2022-06-26 22:28
赞同 4 添加评论
喜欢 收藏 申请转载