C/C++中几个宏的简单总结
作者:magictong
环境:VS2005 XPSP3
有人视宏为洪水猛兽,甚至要求完全从C/C++中摒弃,有人则认为宏为至尊宝典,在逻辑代码中都大量使用。个人认为这是个仁者见仁智者见智的问题,摒弃就没必要了,看看宏在MFC和ATL中的一些经典应用,你会发现如果不使用宏来实现一些消息映射和对象映射神马的那将让“苦逼”程序员多花费多少宝贵的时间。当然也不能滥用,尤其是尽量不要在逻辑代码中使用,宏中的逻辑出问题后,调试时候的痛苦你就真的会发现原来程序员真的挺“苦逼”的。
其实很多人对宏的“恐惧”可能源于下面的一个简单的宏的实现:
#define SUM(x, y) x+y
看起来是正确的,但是当你这样使用的时候SUM(3, 4) * SUM(2, 5),你会发现结果并不是想象的49,而是诡异的16。其实宏仅仅完成简单的替换,而不会像函数那样进行参数计算并且调用返回,上面那个式子SUM(3, 4) * SUM(2, 5)实际会被替换为:3+4*2+5,现在知道为什么结果是16了吧,那我们实现这样应该咯:
#define SUM(x, y) (x+y)
把整个用小括号包起来,看似好像正确了,但是如果这样调用SUM(3||4, 4),结果会是5吗?呃,不是,结果是1,哪里出了问题?我们看看展开的结果3||4+4,哦,因为加法的运算级别更高3||4+4 = 3||8 = 1,看来我们得把参数也括起来:
#define SUM(x, y) ((x)+(y))
经历了上面的一些跌跌撞撞的失败和尝试后我们终于得出了一个正确的两个数求和的正确版本。
从上面的那个例子我想大家应该会受到一些启发以及可以看到写一个宏要注意的一些要点。不过我们今天要讨论的话题则是另外几个可能用得不多但是比较有用的宏。
1、 怎样通过一个简单的宏得到一个field在结构体(struct)中的偏移量 ?
你可以进行如下的定义:
#define FieldOffset(type, field) ((unsigned int) &((type *)0)->field)
解释:将0强制转换为type类型指针,然后访问field域,注意不是真的访问,而是对它求地址,将该地址减去结构的基地址0就是偏移量了,因为基地址是0省略。
同样得到一个结构体中field所占用的字节数可以定义为:
#define FieldSize(type, field) sizeof(((type *) 0)->field)
2、 #、##和#@的用法
#把一个宏参数变成字符串(也就是给参数加上双引号)
##用来把宏参数简单连接在一起
#@把一个宏参数变成字符(也就是给参数加上单引号)
#define TOSTRING(s) #s
#define TOCHAR(s) #@s
#define TOKENCONN(s, t) s##t
另外,对于#@如果你这样调用TOCHAR(abcd),会返回’d’,但是如果TOCHAR(abcde),则会编译出错,看来这与VS的宏处理器有关系,虽然这么使用很诡异。
更重要的一点如果宏定义里用到#、#@或##的地方宏参数可能不会像想象中展开,怎么理解?一般来讲如果宏参数里面有另外的宏,会进行递归的替换:
#define C 7
#define B C
#define A B
#define SUM(x, y) ((x)+(y))
然后这样调用int x = SUM(A, A);编译没有问题,最后x的值是14,替换过程如下:
SUM(A, A) => ((A)+(A)) => ((B)+(B)) => ((C)+(C)) => ((7)+(7))
但是如果有这样的定义:
#define D 12
#define TOSTRING(s) #s
#define TOKENCONN(s, t) s##t
则,TOSTRING(D)的结果是”D”,而TOKENCONN(D, D)的结果则是DD,如果没有定义DD标识符则会编译报错。原因在于,宏替换是分层次的,先替换最外层的,然后再替换参数,而这三个特殊符号都有改变宏参数的含义的作用(把宏参数改变为字符,字符串或者不同的token),因此造成不会有多级展开。
这样会带来一个问题,就是我需要用到这3个特殊的标识符(#、#@或##),而宏参数我又需要展开,该怎么办,我之前在KM上看到过这样的一个需求,我们先看一个我常用的宏:
#pragma message("messageinfo: no impl, TODO…")
我经常使用这个宏插在代码中来标记我没有实现的功能或者需要后面改进的地方,这个宏的好处是在编译的时候,会在编译信息输出窗口中显示出你写在message里面的信息。防止你写代码到后面的时候忘记哪些地方还没有实现。但是我使用的方法很原始,如果需要实现某个功能的时候,我就去全局搜索那个message里面的信息,然后再找到地方,后来在KM上面看到一种类似的需求,可以利用IDE本身提供的功能直接定位到写这段宏的地方,如果编译报错的时候,你会看到类似下面的输出:
1>f:\e\projects\tu_func\tu_func.cpp(124) : error C2065: 'DD' : undeclared identifier
此时如果你双击这一行,代码窗口就会跳转到编译出错的地方去,我们要做的就是模拟这样的一个输出,利用两个已经定义过的宏__FILE__和__LINE__,这两个宏神马作用我就不讲了,我的第一个模拟版本如下:
#define TOSTRING(s) #s
#define MagicTipsMsg(msg) message(__FILE__"(" TOSTRING (__LINE__)"):"#msg)
结果很悲催,没达到预期:
1>f:\e \projects\tu_func\tu_func.cpp(__LINE__):there is need to implement
很显然出现了上面说到的宏参数没展开的问题,肿么办?其实解决这个问题的方法也不是很复杂,在中间加一层跳板宏就可以了,加这个跳板宏的用意是先把所有宏的参数在这个跳板宏这层里全部展开, 那么最终的字符串转换宏那里(TOSTRING)就可以得到正确的宏参数了。
#define TOSTRING(s) #s
#define __TOSTRING(s) TOSTRING (s)
#define MagicTipsMsg(msg) message(__FILE__"(" __TOSTRING(__LINE__)"):"#msg)
现在在编译窗口的输出终于正确了:
1>f:\e\projects\tu_func\tu_func.cpp(121):there is need to implement
3、 几个预定义的宏
__LINE__
__FILE__
__DATE__
__TIME__
__cplusplu
是否全部支持上面的几个宏,与编译器的具体实现有关。__LINE__是表示宏所在的当前的文件的具体行数,__FILE__是表示当前文件的全路径名,__DATE__宏指令含有形式为月/日/年的串,表示源文件被编译时的日期。而源代码编译为目标代码的时间作为串包含在__TIME__中。具体表现形式大家自己试一试。
在编译C++程序时,编译器自动定义了一个预处理名 __cplusplus,要想知道是否define了这某个宏怎么办?可以做一个类似如下的检查:
#ifdef __cplusplus
#pragma message("__cplusplus definded")
#endif
4、 用#pragma导出dll中的函数
一般我们用的比较多的导出dll中函数的方法是使用模块定义文件(.def),而实际上VC提供了一个扩展的方法,使用__declspec(dllexport)去导出某个函数:
int __declspec(dllexport) add(int a, int b)
此时导出的函数名为“?add@@YAHHH@Z”如果我们希望进行dll动态链接,上面的导出函数的名称就有点太坑爹了,而且一旦对函数的返回值或者参数类型进行了修改,函数名也要跟着修改,我们希望导出函数名是“add”。至少有两种方法,一是使用def文件来导出,二是在函数声明的最前面加上extern “C”:
extern “C” int __declspec(dllexport) add(int a, int b)
不过,我们今天要是要讨论VC 提供的另一个预处理宏来解决这个问题,如下:
#pragma comment(linker,"/EXPORT:add=?add@@YAHHH@Z ")
这时,实际上会导出?add@@YAHHH@Z和add两个函数,但是函数的入口点是一样的。实际上这个宏的强大不仅仅如此,它的格式如下(MSDN:http://msdn.microsoft.com/en-us/library/hyx1zcd3(v=vs.80).aspx):
/EXPORT:entryname[,@ordinal[,NONAME]][,DATA]
说明:@ordinal用来指定顺序,NONAME指定只将函数导出为序号,DATA 关键字指定导出项为数据项。例如:
#pragma comment(linker,"/EXPORT:add=?add@@YAHHH@Z,@2,NONAME")
就是把add函数无名导出并且设置顺序为2。
因此如果你想指定导出的顺序,或者只将函数导出为序号,没有函数名,这都是能够实现的,之前和同事讨论到怎么把一个函数无名导出(很多系统dll的导出表里面就没有导出函数的名字),原来通过这个预处理宏就可以简单解决。
另一个问题是无名导出的函数自己怎么动态链接呢(使用如下的方法,假设导出顺序为2,这种无名导出除了增加神秘感还有神马用途暂时还没想到):
typedef int (*PFUNADD)(int, int);
HINSTANCE hIns = LoadLibrary(TEXT("dllname.dll"));
PFUNADD pfnAdd = (PFUNADD)GetProcAddress(hIns, MAKEINTRESOURCE(2));
int c = pfnAdd(1, 2);