[C陷阱和缺陷] 第6章 预处理器
在严格意义上的编译过程开始之前,C语言预处理器首先对程序代码作了必要的转换处理。因此,我们运行的程序实际上并不是我们所写的程序。预处理器使得编程者可以简化某些工作,它的重要性可以由两个主要的原因说明:
第一个原因是,我们也许会遇到这样的情况,需要将多个在程序中出现的所有实例统统加以修改。我们希望在程序中只改动一处数值,然后重新编译就可以实现。预处理器要做到这一点可以说是轻而易举(使用宏定义即可: #define N 1024)。而且,预处理器还能够很容易地把所有常量定义都集中在一起,这样要找到这些常量也非常容易。
第二个原因是,大多数C语言实现在函数调用时都会带来重大的系统开销。因此,我们也许希望有这样一种程序块,它看上去像一个函数,但却没有函数调用的开销。举例来说, getchar 和 putchar 经常被实现为宏,以避免每次执行输入或输出一个字符这样简单的操作时,都要调用相应的函数而造成系统效率的下降。(注:C++的内联函数也有这种异曲同工之妙)
虽然宏非常有用,但如果程序员没有认识到宏只是对程序文本的替换,那么他们很容易对宏的作用感到迷惑。因而,宏既可以使一段看上去不合语法的代码成为一个有效的C程序,也能使一段看上去无害的代码成为一个可怕的怪物。
6.1 不能忽视宏定义中的空格
一个函数如果不带参数,在调用时只需在函数名后加上一对括号即可加以调用了。而一个宏如果不带参数,则只需要使用宏名即可,括号无关紧要。只要宏已经定义过了,就不会带来什么问题:预处理器从宏定义中就可以知道宏调用时是否需要参数。
与宏调用相比,宏定义显得有些“暗藏机关”。例如,下面的宏定义中 f 是否带了一个参数呢?
#define f (x) ( (x)-1 )
答案只可能有两种: 调用 f(x) 或者代表
( (x)-1 )
或者代表
(x)( (x)-1 )
在上述宏定义中,第二个答案是正确的,因为在 f 和后面的 (x) 之间多了一个空格!所以,如果希望定义 f(x) 为 ((x)-1),必须像下面这样写:
#define f(x) ( (x)-1 )
这一规则不适用于宏调用,而只对宏定义适用。因此,在上面完成宏定义后, f(3) 与 f (3)求值后都等于2。
6.2 宏并不是函数
因为宏从表面上看其行为与函数非常类似,程序员有时会禁不住把两者视为完全相同。因此,我们常常可以看到类似下面的写法:
#define abs(x) ( (x) >= 0 ? (x) : -(x) )
或者:
#define max(a,b) ( (a) > (b) ? (a) : (b) )
请注意宏定义中出现的所有这些括号,它们的作用是预防引起与运算符优先级有关的问题。假如,假设宏 abs 被定义成了这个样子:
#define abs(x) x > 0 ? x : -x
让我们来看 abs(a-b)求值后会得到怎样的结果。表达式
abs( a-b )
会被展开为 (宏定义只是简单的文本替换,相当于用 a-b 替换x )
a-b > 0 ? a-b : -a-b
这里的子表达式 -a-b 相当于(-a)-b,而不是我们期望的 -(a-b),因此上式无疑会得到一个错误的结果。因此,我们最好在宏定义中把每个参数都用括号括起来。同样,整个结果表达式也应该用括号括起来,以防止当宏用于一个更大一些的表达式中可能出现的问题。如果不这样,
abs(a)+1
展开的结果为:
a > 0 ? a : -a + 1
这个表达式很显然是错误的,我们期望得到的是 -a ,而不是-a+1! abs 的正确定义应该是这样的:
#define abs(x) ( ( (x) >= 0 ) ? (x) : -(x) )
这时,
abs(a-b)
才会被正确地展开为:
( ( (a-b) >= 0) ) ? (a-b) : -(a-b)
而 abs(a)+1 也会被正确地展开为:
( ( (a) >= 0) ? (a) : -(a) ) + 1
即使宏定义中的各个参数与整个结果表达式都用括号括起来,也依然还可能有其他问题存在,比如说,一个操作数如果在两处被用到,就会被求值两次。例如,在表达式max(a,b)中,如果 a 大于 b,那么 a 将被求值两次:第一次是在 a 与 b 比较期间,第二次是在计算 max 应该得到的结果值时。
这种做法不但效率低下,而且可能是错误的:
biggest = x[0];
i = 1;
while (i < n)
biggest = max(biggest, x[i++]);
如果 max 是一个真正的函数,上面的代码可以正常工作;而如果 max 是一个宏。那么就不能正常工作。要看清楚这一点,我们首先初始化数组 x 中的一些元素:
x[0] = 2;
x[1] = 3;
x[2] = 1;
然后考察在循环的第一次迭代时会发生什么。上面代码中的赋值语句将被扩展为:
biggest = ( (biggest) > (x[i++]) ? (biggest) : (x[i++]) );
会发现由于 i++ 的存在,经过一次循环后,赋给 biggest 的值是 x[2] ,即1。这时,又因为 i++ 的副作用,i 的值成为3。
解决这类问题的一个办法是,确保宏 max 中的参数没有副作用:
biggest = x[0];
i = 1;
while (i < n)
biggest = max(biggest, x[i]);
另一个办法是让 max 作为函数而不是宏,或者直接编写比较两数取较大者的代码:
biggest = x[0];
i = 1;
while (i < n)
{
if( x[i] > biggest )
biggest = x[i];
}
使用宏的另一个危险是,宏展开可能产生非常庞大的表达式,占用的空间远远超过了编程者所期望的空间。例如,让我们再看宏 max 的定义:
#define max(a,b) ( (a)>(b)?(a):(b) )
假定我们需要使用上面定义的宏 max,来找到 a、b、c、d 四个数的最大者,最显而易见的写法是:
max(a, max(b, max(c, d)))
上面展开就是个非常庞大的表达式了,还不如写成函数:
biggest = a ;
if (biggest < b) biggest = b;
if (biggest < c) biggest = c;
if (biggest < d) biggest = d;
6.3 宏并不是语句
编程者有时会试图定义宏的行为与语句类似,但这样做的实际困难往往令人吃惊!举例来说,考虑以下 assert 宏,它的参数是一个表达式,如果该表达式为0,就使程序终止执行,并给出一条适当的出错消息。把 assert 作为宏来处理,这样就使得我们可以在出错信息中包括有文件名和断言失败处的行号。也就是说,
assert(x > y);
在 x 大于 y 时什么也不做,其他情况下则会终止程序。
下面是我们定义 assert 宏的第一次尝试:
#defien assert(e) if(!e) assert_error(__FILE__,__LINE__)
因为考虑到宏 assert 的使用者会加上一个分号,所以在宏定义中并没有包括分号。__FILE__和__LINE__是内建于C语言预处理器中的宏,它们会被扩展为所在文件的文件名和所处代码行的行号。
宏 assert 的这个定义,即使用在一个再明白不过的情形中,也会有一些难以察觉的错误:
if (x>0 && y>0)
assert(x > y);
else
assert(y > x);
上面的写法似乎很合理,但是它展开之后就是这个样子:
if (x>0 && y>0)
if( !(x > y) ) assert_error("foo.c",37);
else
if( !(y > x) ) assert_error("foo.c",39);
读者也许会想到,在宏 assert 的定义中用大括号把宏体整个给“括”起来,就能避免这样的问题产生:
#define assert(e) \
{ if (!e) assert_error(__FILE__,__LINE__); }
然而,这样做又带来了一个新的问题。我们上面提到的例子展开后就成了:
if (x > 0 && y > 0)
{ if( !(x > y) ) assert_error("foo.c",37); };
else
{ if( !(y > x) ) assert_error("foo.c",39); };
在 else 之前的分号是一个语法错误。要解决这个问题,一个办法是对 assert 的调用后面都不再跟一个问号,但这样的用法显得有些“怪异”:
if (x>0 && y>0)
assert(x > y)
else
assert(y > x)
宏 assert 的正确定义很不直观,编程者很难想到这个定义不是类似于一个语句,而是类似一个表达式
#define assert(e) \
( (void) ( (e) || _assert_error(__FILE, __LINE__) ) )
这个定义实际上利用了 || 运算符对两侧的操作符依次顺序求值的性质。如果 e 为 true,就不会执行_assert_error(__FILE, LINE) 了,否则 e 为 false,则会执行 _assert_error(__FILE, LINE) ,并打印出
一条恰当的“断言失败”的出错消息。
6.4 宏并不是类型定义
宏的一个常见用途是,使多个不同变量的类型可在一个地方说明:
#define FOOTYPE struct foo
FOOTYPE a;
FOOTYPE b,c;
这样,编程者只需要在程序中改动一行代码,即可改变 a、b、c、的类型,而与a、b、c在程序中的什么地方定义无关。
宏定义的这种做法有一个优点 - 可移植性,得到了所有C编译器的支持。但是,我们最好还是使用类型定义:
typedef struct foo FOOTYPE
这个语句定义了 FOOTYPE 为一种新的类型,与 struct foo 完全等效。
这两者命名类型的方式似乎都差不多,但是使用 typedef 的方式要更加可靠一些。例如,考虑下面的代码:
#define T1 struct foo *
typedef struct foo * T2;
从上面两个定义来看, T1 和 T2 从概念上完全相同,都是指向结构体foo的指针。但是,当我们试图用它们来声明多个变量时,问题就来了:
T1 a, b;
T2 a, b;
第一个声明被扩展为:
struct foo *a, b;
这个语句 a 被定义为一个指向结构体的指针,而 b 被定义为一个结构体(而不是指针)。第二个声明则不同,它定义了 a 和 b 都是指向结构的指针,因为 T2 的行为完全与一个真实的类型相同。
练习
练习6-1 请使用宏来实现 max 的一个版本,其中 max 的参数都是整数,要求在宏 max 的定义中这些整型参数都只被求值一次。
max 宏的每个参数的值都有可能使用两次:一次是在两个参数作比较时;一次是在把它作为结果返回时。因此,我们有必要把每个参数存储在一个临时变量中,这样max 宏的每个参数的值只使用一次.:宏参数的值赋给临时变量时。
如果 max 宏用于不止一个程序文件,我们应该把这些临时变量声明为 static,以避免命名冲突。不妨假定,这些定义将出现在某个头文件中:
static int max_temp1,max_temp2;
#define max(p, q) ( max_temp1 = (p), max_temp2 = (q), \
max_temp1 > max_temp2 ? max_temp1 : max_temp2 )
只要对 max 宏不是嵌套调用,上面的定义都能正常工作:在 max 宏嵌套调用的情况下,我们不可能做到让他正常工作。
练习6-2 本章第1节中提到的表达式 (x) ( (x) -1 ) 能否称为一个合法的C表达式?
一种可能是,如果 x 是类型名,例如 x 被这样定义:
typedef int x;
在这种情况下, (x) ( (x) -1 ) 等价于:
(int) ( (int) -1 )
这个式子的含义是把常数 -1 转换为 int 类型两次。
另一种可能是当 x 为函数指针:
typedef void (*T) (void *);
这个练习的用意在于说明,对于那些看上去无从着手、形式“怪异”的表达式,我们不应该轻率地一律将其作为错误来处理。
[C陷阱和缺陷] 第6章 预处理器的更多相关文章
- [C陷阱和缺陷] 第1章 词法“陷阱”
有感自己的C语言在有些地方存在误区,所以重新仔细把"C陷阱和缺陷"翻出来看看,并写下这篇博客,用于读书总结以及日后方便自身复习. 第1章 词法"陷阱" 1.1 ...
- C和指针 第十四章 预处理器 头文件
编写一个C程序,第一个步骤称为预处理,预处理在代码编译之前,进行一些文本性质的操作,删除注释.插入被include的文件.定义替换由#define定义的符号,以及确定代码的部分内容是否应该按照条件编译 ...
- c缺陷与陷阱笔记-第六章 预处理器
1.这一章貌似有个小错误,开始时定义 #define f (x) ((x)-1),然后f(x)代表什么,书上说是(x) ((x)-1),应该是 (x) ((x)-1)(x) 2.关于宏定义中参数的2次 ...
- [C陷阱和缺陷] 第3章 语义“陷阱”
第3章 语义"陷阱" 一个句子哪怕其中的每个单词都拼写正确,而且语法也无懈可击,仍然可能有歧义或者并非书写者希望表达的意思.程序也有可能表面上是一个意思,而实际上的意思却相 ...
- [C陷阱和缺陷] 第7章 可移植性缺陷
C语言在许多不同的系统平台上都有实现.的确,使用C语言编写程序的一个首要原因就是,C程序能够方便地在不同的编程环境中移植. 不同的系统有不同的需求,因此我们应该能够预料到,机器不同则其上的C语 ...
- [C陷阱和缺陷] 第2章 语法“陷阱”
第2章 语法陷阱 2.1 理解函数声明 当计算机启动时,硬件将调用首地址为0位置的子例程,为了模拟开机时的情形,必须设计出一个C语言,以显示调用该子例程,经过一段时间的思考,得出语句如下: ( * ...
- [C陷阱和缺陷] 第5章 库函数
有关库函数的使用,我们能给出的最好建议是尽量使用系统头文件,当然也可以自己造轮子,随个人喜好.本章将探讨某些常用的库函数,以及编程者在使用它们的过程中可能出错之处. 5.1 返回整数的getc ...
- [C陷阱和缺陷] 第4章 连接
一个C程序可能是由多个分别编译的部分组成,这些不同部分通过连接器合并成一个整体.在本章中,我们将考查一个典型的连接器,注意它是如何对C程序进行处理的,从而归纳出一些由于连接器的特点而可能导致的错误. ...
- 《C和指针》 读书笔记 -- 第14章 预处理器
1.相邻字符串常量被自动链接为一个字符串:"my""name"="myname" 2.##把位于两边的符号连接成一个符号: #define ...
随机推荐
- [luoguP1494] 岳麓山上打水 && [luoguP2744] [USACO5.3]量取牛奶Milk Measuring
传送门 传送门 dfs选取集合,dp背包判断 虽然我觉的会TLE.. 但是的确是AC了 #include <cstdio> #include <cstring> #includ ...
- bzoj 3173 [Tjoi2013]最长上升子序列 (treap模拟+lis)
[Tjoi2013]最长上升子序列 Time Limit: 10 Sec Memory Limit: 128 MBSubmit: 2213 Solved: 1119[Submit][Status] ...
- [K/3Cloud] 如何代码中动态设置当前活动页签
this.GetControl<TabControl>(key).SelectedIndex=目标Index Ps:如下方式隐藏页签: this.View.GetControl(" ...
- 【BZOJ1758】重建计划(点分治)
题意: 给定一棵n个点的树,每条边有权值.求一条链,这条链包含的边数在L和U之间,且平均边权最大.N﹤=100000 思路:RYZ作业 二分答案再点分治,寻找是否有大于0且边数在L和U之间的链 f[i ...
- [bzoj4994][Usaco2017 Feb]Why Did the Cow Cross the Road III_树状数组
Why Did the Cow Cross the Road III bzoj-4994 Usaco-2017 Feb 题目大意:给定一个长度为$2n$的序列,$1$~$n$个出现过两次,$i$第一次 ...
- 浅识MySQL
MySQL常用语句 #操作数据库 ##创建数据库 CREATE DATABASE `dbName`; ##切换数据库 USE `dbName`; ##查看所有数据库 SHOW DATABASES; # ...
- Linux命令输出头(标题)、输出结果排序技巧
原文:http://blog.csdn.net/hongweigg/article/details/65446007 ----------------------------------------- ...
- Android 圆形/圆角图片的方法
Android 圆形/圆角图片的方法 眼下网上有非常多圆角图片的实例,Github上也有一些成熟的项目.之前做项目,为了稳定高效都是选用Github上的项目直接用.但这样的结束也是Android开发必 ...
- Android上拉查看详情实现
京东淘宝有那么一种效果就是,上拉能够查看宝贝的详情,这里我也实现了一个类似的效果,也能够移植到商业项目上:先看看简单的效果图 实现原理事实上是利用了ScrollView的滚动和view的touch事件 ...
- 10.11无法打开Xcode6.4的解决方法
前言 mac升级到10.11版本号并安装Xcode7.0Beta之后,Dock中的Xcode6.3图标上出现一个禁止符号,打开提示到App store更新最新版本号,更新到6.4之后问题依然,还是提示 ...