一点一点前进...

0%

C语言:预处理器

将C语言源代码,处理成可执行的文件,大致需要三步,预处理、编译和链接。下面主要讲讲预处理器的作用,回头有时间再写一篇关于编译和链接的文章。

预处理器的作用是根据一定的规则,进行纯文本的替换,递归的执行,直到没有可替换的内容结束。使用gcc -E可以仅做预处理。

C语言中,大致有以下几种做法:
第一种,定义一个常量。

1
2
#define WIDTH 480
#define HEIGHT 720

上述就定义了两个常量,宽和高。我相信,没人想在源代码的很多地方写上480和720这两个数字。使用宏定义可以给这两个数字起一个有意义的名字,代码中使用有意义的名字来提升代码的可读性。预处理器会把代码中的WIDTH和HEIGHT全部替换成480和720。

第二种,定义函数。

1
#define MAX(a,b) (((a)>(b))?(a):(b))

上述就定义了一个返回最大值的函数。
为啥要套这么多括号,因为a和b可以是复杂的运算而不是简单的两个数,不写括号,可能会由于运算优先级被改变而导致出现非程序员期望的结果。
宏定义函数的优点是比普通的函数快,宏展开之后,不会有真正的函数调用。而普通调用需要保存上下文,传参,返回等等,浪费些许时间。
但是缺点很多:缺少必要的检查;
写不好会导致效率低下,比如int max = MAX(fib(a),fib(b)),展开后会调用三次fib函数;
多次或者不正确的副作用,比如int max = MAX(m++,n++),展开后,m和n回调用不同次++。
assert就是一个经典的宏定义函数,大致如下:

1
2
3
4
5
6
7
#ifdef NDEBUG
#define assert(cond) (void)0
#else
#define assert(cond) \
((cond)?(void)0:\
fprint(xxxx), exit(0))
#endif

对于宏函数,我个人觉得弊大于利,所以还是应该谨慎使用。
联系到C语言被创造的时间,上个世纪六七十年代,信条是程序员知道他做得事情,让他去做,甚至包括硬件。所以C语言设计的是基于硬件层的抽象,都能直接的映射到硬件操作。但是时至今日,操作系统,软件,硬件都变得日益复杂,程序员这个行业也越来越庞大,codebase也以数量级的方式增长,不能指望每个程序员都能明白他们在做的事情。所以,现代语言都从语言层面来限制程序员,最大效率的生产出软件,而软件本身的效率只是诸多关注点的一个罢了。

第三种,包含头文件。

1
2
#include <assert.h>
#include "yourheaderfile.h"

预处理器会查找对应文件并作替换,本来很小的文件会变的很大。
如果出现了循环依赖,那么预处理器就会一直递归的处理下去,无穷无尽。
如何解决这个问题呢?在需要的地方使用宏定义。

1
2
3
4
#ifndef XXX_H
#define XXX_H
// code
#endif

智能的现代预处理器应该会自己处理这种问题,即使你没写上述的代码。但是,写上述的东西很容易,而且就算没有循环依赖也没有什么副作用,所以最佳实践是,头文件都写上上述三行代码。