《笨办法学C》笔记之C预处理器

撰写于 2017-01-01 修改于 2018-12-15 标签 笔记

预处理(或称预编译)是指在进行编译的第一遍扫描(词法扫描和语法分析)之前所作的工作。预处理指令指示在程序正式编译前就由编译器进行的操作,可放在程序中任何位置。

预处理是C语言的一个重要功能,它由预处理程序负责完成。当对一个源文件进行编译时,系统将自动引用预处理程序对源程序中的预处理部分作处理,处理完毕自动进入对源程序的编译。

C语言的预处理器主要可以做到:

  • 文件包含
  • 条件编译
  • 宏定义

合理使用预处理功能编写的程序便于阅读、修改、移植和调试,也有利于模块化程序设计。

常用的预处理器指令

序号 指令 描述
1 #define 定义宏
2 #include 包含一个源代码文件
3 #undef 取消已定义的宏
4 #ifdef 如果宏已经定义,则返回真
5 #ifndef 如果宏没有定义,则返回真
6 #if 如果给定条件为真,则编译下面代码
7 #else #if 的替代方案
8 #elif 如果前面的 #if 给定条件不为真,当前条件为真,则编译下面代码
9 #endif 结束一个 #if……#else 条件编译块
10 #error 当遇到标准错误时,输出错误消息
11 #pragma 使用标准化方法,向编译器发布特殊的命令到编译器中

文件包含

C语言使用#include标识符,来表明.c文件内将要调用.h头文件所定义的数据结构或函数。

#include <stdio.h>

条件编译

C语言的预处理器,通过前表中第2-9项指令能够做到:对.c文件中的代码或者对.h文件中的定义有选择的编译。

其编译的条件是:首先满足宏定义的条件,然后才会执行编译。

#ifdef XXX
   /* <C code> */
#endif

如果编译器读取到这段代码的时候,XXX已经定义了,那么<C code>也会得到编译,否则编译器就会忽略这段代码,直到#endif位置。

宏定义

预定义宏

ANSI C 定义了许多宏。在编程中可以使用这些宏,但是不能直接修改这些预定义的宏。

描述
DATE 当前日期,一个以 “MMM DD YYYY” 格式表示的字符常量。
TIME 当前时间,一个以 “HH:MM:SS” 格式表示的字符常量。
FILE 这会包含当前文件名,一个字符串常量。
LINE 这会包含当前行号,一个十进制常量。
STDC 当编译器以 ANSI 标准编译时,则定义为 1。
# include <stdio.h>

main()
{
   printf("File :%s\n", __FILE__ );
   printf("Date :%s\n", __DATE__ );
   printf("Time :%s\n", __TIME__ );
   printf("Line :%d\n", __LINE__ );
   printf("ANSI :%d\n", __STDC__ );

}

当上面的代码(在文件 test.c 中)被编译和执行时,它会产生下列结果:

File :test.c
Date :Jun 2 2012
Time :03:36:24
Line :8
ANSI :1

自定义宏

常量定义

最简单的宏定义就是常量定义。

#define CONST 123
#define MESSAGE "This is a pre-defined msg"

这句定义就指定后文中的CONST都要被替换为123。如果遇到#undef CONST,那后面的CONST就不会被这样替换了。

需要注意的是,宏定义的常量没有占用内存。编译器会在编译后面的代码中,直接将所有CONST替换为123,将所有MESSAGE替换为"This is a pre-defined msg"。编译后的二进制可执行文件不会意识到CONSTMESSAGE的存在。因为,这些定义过的宏在编译的第一步——“预处理”中就被替换完毕了。

函数模版

C预处理器还能够预定义函数模版。例如:

#define MAX(x,y) ((x) > (y) ? (x) : (y))

int main() {
    int i = 12;
    int j = 33;
    int res = MAX(i, j); 
    return res;
}

上例中,i,j被带入MAX宏中进行计算,res存储着计算结果。

这和函数调用很像,然而两者并不相同。

编译器在预处理阶段,直接将预定义的函数模版在代码中展开。在二进制文件执行过程中,并不存在函数调用过程中的栈帧调度、参数传递以及结果回传等等,也不会占用内存空间。如果去查看等价的汇编代码,也不会发现call指令。因为预处理阶段结束后,函数模版在嵌入实际参数之后,成为代码的一部分,而不是函数形式。

小小总结

显而易见,宏定义不占用内存空间,不涉及函数的栈帧切换,在性能方面有很强的优势。

但是,宏定义也有严重的问题。那就是这里面没有任何类型检查。编译器会无条件的直接做替换,而如果存在类型不匹配,只有在编译时替换完成后才能发现。

参考资料

Site by Zhang,Xin using Hexo & Random

Hide