VC中预处理指令与宏定义的妙用之二
在 上一篇 文章中,我演示了几个常用的宏定义和预处理指令,但可以说这些都是相当常规的技巧。下面要介绍的宏定义与预处理指令的用法也是ATL,MFC以及 LINUX 中使用得比较多的非常重要的技巧。 ## 连接符与# 符 ## 连接符号由两个井号组成,其功能是在带参数
在
上一篇文章中,我演示了几个常用的宏定义和预处理指令,但可以说这些都是相当常规的技巧。下面要介绍的宏定义与预处理指令的用法也是ATL,MFC以及
LINUX中使用得比较多的非常重要的技巧。
## 连接符与# 符 ## 连接符号由两个井号组成,其功能是在带参数的宏定义中将两个子串(token)联接起来,从而形成一个新的子串。但它不可以是第一个或者最后一个子串。所谓的子串(token)就是指编译器能够识别的最小语法单元。具体的定义在编译原理里有详尽的解释,但不知道也无所谓。同时值得注意的是#符是把传递过来的参数当成字符串进行替代。下面来看看它们是怎样工作的。这是MSDN上的一个例子。
假设程序中已经定义了这样一个带参数的宏:
#define paster( n ) printf( "token" #n " = %d", token##n )
同时又定义了一个整形变量:
int token9 = 9;
现在在主程序中以下面的方式调用这个宏:
paster( 9 );
那么在编译时,上面的这句话被扩展为:
printf( "token" "9" " = %d", token9 );
注意到在这个例子中,paster(9);中的这个”9”被原封不动的当成了一个字符串,与”token”连接在了一起,从而成为了token9。而#n也被”9”所替代。
可想而知,上面程序运行的结果就是在屏幕上打印出token9=9
在ATL的
编程中,我们查看它的源代码就会经常看见这样的一段:
#define IMPLEMENTS_INTERFACE(Itf) \
{&IID_##Itf, ENTRY_IS_OFFSET,BASE_OFFSET(_ITCls, Itf) },
我们经常不假思索的这样使用它:
……
IMPLEMENTS_INTERFACE(ICat)
……
实际上IID_ICat 已经在别的地方由ATL向导定义了。当没有向导的时候,你只要遵循把IID_加在你的接口名前面来定义GUID的规则就也可以使用这个宏。在实际的
开发过程中可能很少用到这种技巧,但是ATL使用得如此广泛,而其中又出现了不少这样的源代码,所以明白它是怎么一回事也是相当重要的。我的一个朋友就是因为不知道IMPLEMENTS_INTERFACE宏是怎么定义的,而又不小心改动了IID_ICat的定义而忙活了一整天。
Linux的怪圈 在刚开始阅读Linux的时候有一个小小的宏让我百思不得其解:
#define wait_event(wq,condition) \
do{ \
if(condition) \
break; \
__wait_event(wq,condition); \
}while(0)
这是一个奇怪的循环,它根本就只会运行一次,为什么不去掉外面的do{..}while结构呢?我曾一度在心里把它叫做“怪圈”。原来这也是非常巧妙的技巧。在工程中可能经常会引起麻烦,而上面的定义能够保证这些麻烦不会出现。下面是解释:
假设有这样一个宏定义
#define macro(condition) \
if(condition) dosomething();
现在在程序中这样使用这个宏:
if(temp)
macro(i);
else
doanotherthing();
一切看起来很正常,但是仔细想想。这个宏会展开成:
if(temp)
if(condition) dosomething();
else
doanotherthing();
这时的else不是与第一个if语句匹配,而是错误的与第二个if语句进行了匹配,编译通过了,但是运行的结果一定是错误的。
为了避免这个错误,我们使用do{….}while(0) 把它包裹起来,成为一个独立的语法单元,从而不会与上下文发生混淆。同时因为绝大多数的编译器都能够识别do{…}while(0)这种无用的循环并进行优化,所以使用这种方法也不会导致程序的
性能降低。
几个小小的警告 正如微软声称的一样,宏定义与预编译器指令是强大的,但是它又使得程序难以调试。所以在定义宏的时候不要节省你的字符串,一定要力争完整的描述这个宏的功能。同时在定义宏的时候如有必要(比方使用了if语句)就要使用do{…}while(0)将它封闭起来。在宏定义的时候一定要注意各个宏之间的相互依赖关系,尽量避免这种依赖关系的存在。下面就有这样一个例子。
设有一个静态数组组成的整型队列,在定义中使用了这样的方法: int array[]={5, 6, 7, 8};
我们还需要在程序中遍历这个数组。通常的做法是使用一个宏定义
#define ELE_NUM 4
…………………………..
……………………………..
for(int I=0;I<ELE_NUM;I++)
{
cout<<array[I];
}
由于某种偶然的原因,我们删除了定义中的一个元素,使它变成:
array[]={5,6,7}
而却忘了修改ELE_NUM的值。那么在上面的代码中马上就会发生访问异常,程序崩溃。然后是彻夜不眠的调试,最后发现问题出在这个宏定义上。解决这个问题的方法是不使用
array[]={….}这样的定义,而显式的申明数组的大小:
array[ELE_NUM]={….}
这样在改动数组定义的时候,我们就不会不记得去改宏定义了。总之,就是在使用宏定义的时候能够用宏定义的地方统统都用上。
我发现的另一个有趣的现象是这样的:
假设现在有一个课程管理系统,学生的人数用宏定义为:
#define STU_NUM 50
而老师的人数恰好也是50人,于是很多人把所有涉及到老师人数的地方通通用上STU_NUM这个宏。另一个学期过去,学生中的一个被开除了,系统需要改变。怎么办呢?简单的使用#define STU_NUM 49 么?如果是这样,一个老师也就被开除了,我们不得不手工在程序中去找那些STU_NUM宏然后判断它是否是表示学生的数目,如果是,就把它改成49。天哪,这个宏定义制造的麻烦比使用它带来的方便还多。正确的方法应该是为老师的数目另外定义一个宏:
#define TEA_NUM 50
当学生的数目改变以后只要把STU_NUM 定义为49就完成了系统的更改。所以,当程序中的两个量之间没有必然联系的时候一定不要用其中的一个宏去替代另一个,那只会让你的程序根本无法改动。
最后,建议C/
C++语言的初学者尽可能多的在你的程序中使用宏定义和预编译指令。多看看MFC,ATL或者LINUX的源代码,你会发现C语言强大的原因所在。