第六章 预处理器
《C陷阱与缺陷》学习笔记
posts
导读
预处理器使得编程者可以简化某些工作,它的重要性可以由两个主要的原因说明。
- 第一,有时候我们希望将某个特定数量(如数据表的大小)在程序中出现的所有实例统统加以修改。只要改一个地方,其他所有地方都修改。
- 第二,C语言函数调用时都会带来巨大的系统开销。因此,我们希望有这样一种程序块,它看上去像一个函数,但却没有函数调用。比如,
getchar
和putchar
经常被实现为宏,
宏的作用非常强大,有时候可以使代码看起来更加容易理解,将数字或符号替换为自己熟悉的命名。
6.1 不能忽视宏定义中的空格
宏也是有参数的,和函数一样。函数有空格无关紧要,但是如果宏定义带了空格意思就不一样了。 比如
1.#define f (x) ((x) - 1)
可能观察不仔细的人会以为是将f(x)替换成((x) - 1),这样编译通过不了!所以必须要像下面这样写:
1.#define f(x) ((x) - 1)
这一规则不适用与宏调用,只对宏定义有用。因此,在上面完成宏定义后,f(3)与f (3)求值后都等于2。
6.2 宏并不是函数
6.2.1 括号预防优先级问题
宏严格来说并不是函数,但是很多程序员都喜欢把一些简单的函数定义为宏。比如如下写法:
1.#define abs(x) (((x) >= 0) ? (x) : -(x))
2.#define max(a,b) ((a) > (b) ? (a) : (b))
之所以这么多括号是预防引起优先级
有关的问题,如果没有括号可能会造成优先级的问题。因为宏不像函数,宏只是起到了一个替换的作用,直接将define的中间替换为后面的表达式。比如
1.#define abs(x) x>0?x:-x
2.abs(a-b); //
会被展开为 a-b>0?a-b:-a-b
,其中的-a-b相当于(-a)-b,而不是想象中的-(a-b)。所以最好用括号括起来。
6.2.2 避免参数副作用
在用到类似的三元运算符宏定义#define max(a,b) a>b ? a : b
时,因避免在三元运算符里对数进行改变而产生副作用。比如:
1.biggest = ((biggest) > (x[i++]) > (biggest) : (x[i++]));
在上述表达式中,若i
为0
,那么biggest
会与x[0]
先比较,然后因为i++
的作用,此时i
为1,然后再在后面的赋值中又有i++
,此时计算完毕i
的值已经为2
,而不是预料之中的1
。
所以要确保max中的参数没有副作用。
6.3 宏并不是语句
有些人会认为宏也是语句,如果单独使用也和语句一样,有分号作终止符,其实大错特错! 比如assert宏,它的参数是一个表达式,如果该表达式为0,就使程序终止执行并给出一条错误信息。
1.#define assert(e) if(!e) assert_error(_FILE,_LINE_)
2.
3.if(x > 0 && y > 0)
4. assert(x > y);
5.else
6. assert(y > x);
上面的式子通过宏替换后就是这样样子:
1.if( x > 0 && y > 0)
2. if(!(x > y)) assert_error("foo.c",37);
3.else
4. if(!(y > x)) assert_error("foo.c",39);
请注意之前的if-else结合优先级问题,未匹配的else
会与其最近的if
结合。将代码适当缩排一下:
1.if( x > 0 && y > 0)
2. if(!(x > y))
3. assert_error("foo.c",37);
4. else
5. if(!(y > x))
6. assert_error("foo.c",39);
可以看到实际流程与我们想象中的有所出入,那么如何解决这个问题? 这样定义assert:
1.#define assert(e) ((void)((e))||_assert_error(__FILE__,__LINE__)))
这个定义实际上利用了||
运算符对两侧的操作数依次顺序求值的性质。具体的也不是很理解。
宏并不是类型定义
宏的一个常见用途是,使多个不同变量的类型可在一个地方说明:
1.#define FOOTYPE struct foo
2.FOOTYPE a;
3.FOOTYPE b,c;
这样,编程者只需在程序中改动一行代码,即可改变a、b、c
的类型,而与a、b、c
在程序中的什么地方声明无关。这种用法有一个优点——可移植性
。但是我们最好还是使用类型定义:
1.typedef struct foo FOOTYPE;
看起来差不多,但是使用起来就会有很大的差别。例如,如下代码:
1.#define T1 struct foo *
2.typedef struct foo *T2;
从上面看,T1
和T2
好像完全一样,都是指向结构foo
的指针。但是当我们试图用他们来声明多个变量时,问题就来了。
1.T1 a, b;
2.T2 a, b;
3.//第一个声明被扩展为:
4.struct foo *a, b;
这个语句中a
被定义为一个指向结构的指针,而b
却被定义为一个结构。第二个声明则不同,它定义的都是指向结构的指针。
总结
宏是一个强大的功能,用好了则事半功倍,否则事倍功半!总结以下几点,写代码的时候要十分注意。
- 宏不要随便加空格,因为
define
后面的两个式子
就是相互替换
的关系,如果多了一个空格那么就会变成三个式子; - 宏不是函数,是直接替换内容,要考虑运算符优先级的问题,必要时应该加上空格防止优先级错误;
- 宏要避免参数副作用,不要在宏里进行多余的操作,比如
++
操作; - 宏不是语句,不要想当然的当成语句来处理,需要加分号等结束符;
- 宏不适用于类型定义,多重定义的时候会出现歧义。
查看原文:http://tanwenbo.top/c/%e7%ac%ac%e5%85%ad%e7%ab%a0-%e9%a2%84%e5%a4%84%e7%90%86%e5%99%a8.html