C/C++宏的使用总结
宏替换是C/C++系列语言的技术特色,C/C++语言提供了强大的宏替换功能,源代码在进入编译器之前,要先经过一个称为“预处理器”的模块,这个模块将宏根据编译参数和实际编码进行展开,展开后的代码才正式进入编译器,进行词法分析、语法分析等等。
我们常用的宏替换主要有这么几个类型。
1.宏常量
在ACM等算法竞赛中,经常会把数组的最大下标通过宏定义的方法给出,以方便调试,例如:
#define MAX 1000
int array[MAX][MAX]
......
for(int i = 0; i < MAX; i++)
......
将一个数字定义成全局的常量,这个用法在国产垃圾教材上十分常见。但在经典著作《Effective C++》中,这种做法却并不提倡,书中更加推荐以const常量来代替宏常量。因为在进行词法分析时,宏的引用已经被其实际内容替换,因此宏名不会出现在符号表中。所以一旦出错,看到的将是一个无意义的数字,比如上文中的1000,而不是一个有意义的名称,如上文中的MAX。而const在符号表中会有自己的位置,因此出错时可以看到更加有意义的错误提示。
2.用于条件编译标识的宏
#define常与#ifdef/#ifndef/defined指令配合使用,用于条件编译。
#ifndef _HEADER_INC_
#define _HEADER_INC_
……
……
#endif
这种宏标记在头文件中十分常见,用于防止头文件被反复包含。应该养成习惯在每个头文件中都添加这种标记。
还有一种用于条件编译的用法
#ifdef DEBUG
printf("{“}Debug information\n");
#endif
通过DEBUG宏,我们可以在代码调试的过程中输出辅助调试的信息。当DEBUG宏被删除时,这些输出的语句就不会被编译。更重要的是,这个宏可以通过编译参数来定义。因此通过改变编译参数,就可以方便的添加和取消这个宏的定义,从而改变代码条件编译的结果。
在条件编译时建议使用#if defined和#if !defined来代替使用#ifdef/#ifndef,因为前者更方便处理多分支的情况与较复杂条件表达式的情况。#ifdef/#ifndef只能处理两个分支:#ifdef/#ifndef,#else,#endfi;#if defined和#if !defined可以处理多分支的情况:#if defined/#if !defined, #elif defined, #else, #endif。#ifdef只能判断是否定义,但是#if defined可以判断复杂的表达式的值是否为真。
#if defined(OS_HPUX)&&(defined(HPUX_11_11)|| defined(HPUX_11_23)
// for HP-UX 11.11 and 11.23
#elif defined(OS_HPUX) && defined(HPUX_11_31
// for HP-UX 11.31
#elif defined(OS_AIX)
// for AIX
#else
…
#endif
条件编译时,如果一个文件中太多条件编译的代码,有些编辑器的智能感知可能都不能很好地解析,还是保持代码越简单越好。对于函数级别的条件编译主要有两种实现方式:
(1) 同一个函数声明,同一个函数定义,函数体内使用条件编译代码。这种方式有个问题,如果条件编译代码太多,会导致这个函数体很长,不利于阅读与维护;有一个优点是,有利于编辑器的智能感知,因为这样解析函数名比较方便,但随着编辑器功能的完善,这方面的差别就不明显了。
(2) 根据编译条件,将编译条件相同的代码放到单独的文件中,这些文件在顶层文件中使用条件编译指令来引用。这种方式最大的优点就是不同平台的程序由不同的源文件来实现,很便于多人分工合作,对于某一部分代码由一个人实现并测试完成后直接把源文件复制过来就可以了,进行低层次的单元测试非常方便;它的缺点就是增加了目录中的文件数量。
3.宏函数
宏函数的语法有以下特点:
(1)、如果需要换行,则行末要加反斜杠“\”表示换行。宏函数体的最后一行不加反斜杠。
(2)、假设有参数ARGU,值为argu,则所有的ARGU被直接替换为argu,#ARGU被认为是字符串,会被替换成"argu"(带引号)。
(3)、由于宏函数是直接替换,所有一般情况下对宏函数的调用时行末不需要加分号。
宏函数的作用:
1)、避免函数调用,提高程序效率
常用的就是最大值与最小值的判断函数,由于函数内容并不多,如果定义为函数在调用比较频繁的场合会明显降低程序的效率,其实宏是用空间效率换取了时间效率。如取两个值的最大值:
#define MAX(a,b) ((a)<(b) ? (b) : (a))
定义为函数:
inline int Max(int a, int b)
{
return a<b ? b : a;
}
定义为模板:
template <typename T>
inline T TMax(T a, T b)
{
return a < b ? b : a ;
}
使用宏函数的优点有两个:
(1)适用于任何实现了operator<的类型,包括自定义类型;
(2)效率最高。虽然使用inline提示符也将函数或模板定义为内联的,但这只是一种提示而已,到底编译器有没有优化还依赖于编译器的实现,而使用宏函数则是完全由代码本身控制。
需要注意的是,由于宏的本质是直接的文本替换,所以在宏函数的“函数体”内都要把参数使用括号括起来,防止参数是表达式时造成语法错误或结果错误,如:
#define MIN( a, b) b < a ? b : a
#define SUM( a, b) a + b
cout<<MIN(3,5)<<endl; // 语法错误:cout<<b < a ? b : a<<endl;
int c = SUM(a,b)*2; // c的期望值:16,实际值:13
2)、引用编译期数据
上述的这些作用虽然使用宏函数可以取得更好的性能,但如果从功能上讲完全可以不使用宏函数,而使用模板函数或普通函数实现,但还有些时候只能通过宏实现。例如,程序中在执行某些操作时可能会失败,此时要打印出失败的代码位置,只能使用宏实现。
#define SHOW_CODE_LOCATION() cout<<__FILE__<<':'<<__LINE__<<'\n'
if( 0 != rename("oldFileName", "newFileName") )
{
cout<<"failed to move file"<<endl;
SHOW_CODE_LOCATION();
}
虽然宏是简单的替换,所以在调用宏函数SHOW_CODE_LOCATION时,分号可以直接写到定义里,也可以写到调用处,但最好还是写到调用处,看起来更像是调用了函数,否则看着代码不伦不类,如:
#define SHOW_CODE_LOCATION() cout<<__FILE__<<':'<<__LINE__<<'\n'
if( 0 != rename("oldFileName", "newFileName") )
{
cout<<"failed to move file"<<endl;
SHOW_CODE_LOCATION()
}
3)、do-while的妙用
do-while循环控制语句的特点就是循环体内的语句至少会被执行一次,如果while(…)内的条件始终为0时,循环体内的语句就会被执行且只被执行一次,这样的执行效果与直接使用循环体内的代码相同,但这们会得到更多的益处。
#define SWAP_INT(a, b) do
{\
int tmp = a; \
a = b; \
b = tmp; \
}while(0)
int main( void )
{
int x = 3, y = 4;
if( x > y )
{
SWAP_INT(x, y);
}
return 0;
}
通过do-while代码块的宏定义我们不仅可以把SWAP_INT像函数一样用,而且还有优点:
(1)、在宏定义中可以使用局部变量;
(2)、在宏定义中可以包含多个语句,但可以当作一条语句使用,如代码中的if分支语句,如果没有do-while把多条语句组织成一个代码块,则程序的运行结果就不正确,甚至不能编译。
其实我们定义的SWAP_INT(a, b)相当于定义了引用参数或指针参数的函数,因为它可以改变实参的值。在C++0X中有了decltype关键词,这种优势就更显示了,因为在宏中使用了局部变量必须确定变量的类型,所以这个宏只能用于交换int型的变量值,如果换作其它类型则还必须定义新的宏,如SWAP_FLOAT、SWAP_CHAR等,而通过decltype,我们就可以定义一个万能的宏。
#include <iostream>
using namespace std;
#define SWAP(a, b) do
{ \
decltype(a) tmp = a; \
a = b; \
b = tmp; \
}while(0)
int main( void )
{
int a = 1, b = 2;
float f1 = 1.1f, f2 = 2.2f;
SWAP(a, b);
SWAP(f1,f2);
return 0;
}
通过宏实现的SWAP“函数”要比使用指针参数效率还要高,因为它连指针参数都不用传递而是使用直接代码,对于一些效率要求比较明显的场合,宏还是首选。
4、取消宏定义
#undef指令用于取消前面用#define定义的宏,取消后就可以重新定义宏。该指令用的并不多,因为过多的#undef会使代码维护起来非常困难,一般也只用于配置文件中,用来清除一些#define的开关,保证宏定义的唯一性。
// config.h
#undef HAS_OPEN_SSL
#undef HAS_ZLIB
#if defined(HAS_OPEN_SSL)
…
#endif
#if defined(HAS_ZLIB)
…
#endif
将对该头文件的引用放到所有代码文件的第一行,就可以保证HAS_OPEN_SSL没有被定义,即使是在编译选项里定义过一宏,也会被#undef指令取消,这样使得config.h就是唯一一处放置条件编译开关的地方,更有利于维护。
5、注意事项
1)、普通宏定义
(1)宏名一般用大写
(2)使用宏可提高程序的通用性和易读性,减少不一致性,减少输入错误和便于修改。
(3)预处理是在编译之前的处理,而编译工作的任务之一就是语法检查,预处理不做语法检查。
(4)宏定义末尾不加分号;
(5)宏定义写在函数的花括号外边,作用域为其后的程序,通常在文件的最开头。
(6)可以用#undef命令终止宏定义的作用域
(7)宏定义可以嵌套
(8)字符串""中永远不包含宏
(9)宏定义不分配内存,变量定义分配内存。
2)、带参宏定义
(1)实参如果是表达式容易出问题
(2)宏名和参数的括号间不能有空格
(3)宏替换只作替换,不做计算,不做表达式求解
(4)函数调用在编译后程序运行时进行,并且分配内存。宏替换在编译前进行,不分配内存
(5)宏的哑实结合不存在类型,也没有类型转换。
(6)函数只有一个返回值,利用宏则可以设法得到多个值
(7)宏展开使源程序变长,函数调用不会
(8)宏展开不占运行时间,只占编译时间,函数调用占运行时间(分配内存、保留现场、值传递、返回值)
6、关于#和##
在C语言的宏中,#的功能是将其后面的宏参数进行字符串化操作(Stringfication),简单说就是在对它所引用的宏变量通过替换后在其左右各加上一个双引号。比如下面代码中的宏:
#define WARN_IF(EXP) \
do{ if (EXP) \
fprintf(stderr, "Warning: " #EXP "\n"); } \
while(0)
那么实际使用中会出现下面所示的替换过程:
WARN_IF (divider == 0);
被替换为
do {
if (divider == 0)
fprintf(stderr, "Warning" "divider == 0" "\n");
} while(0);
这样每次divider(除数)为0的时候便会在标准错误流上输出一个提示信息。
而##被称为连接符(concatenator),用来将两个Token连接为一个Token。注意这里连接的对象是Token就行,而不一定是宏的变量。比如你要做一个菜单项命令名和函数指针组成的结构体的数组,并且希望在函数名和菜单项命令名之间有直观的、名字上的关系。那么下面的代码就非常实用:
struct command
{
char * name;
void (*function) (void);
};
#define COMMAND(NAME) { NAME, NAME ## _command }
// 然后你就用一些预先定义好的命令来方便的初始化一个command结构的数组了:
struct command commands[] = {
COMMAND(quit),
COMMAND(help),
...
}
COMMAND宏在这里充当一个代码生成器的作用,这样可以在一定程度上减少代码密度,间接地也可以减少不留心所造成的错误。我们还可以n个##符号连接 n+1个Token,这个特性也是#符号所不具备的。比如:
#define LINK_MULTIPLE(a,b,c,d) a##_##b##_##c##_##d
typedef struct _record_type LINK_MULTIPLE(name,company,position,salary);
// 这里这个语句将展开为:
// typedef struct _record_type name_company_position_salary;
7、关于...的使用
在C宏中称为Variadic Macro,也就是变参宏。比如:
#define myprintf(templt,...) fprintf(stderr,templt,__VA_ARGS__)
// 或者
#define myprintf(templt,args...) fprintf(stderr,templt,args)
第一个宏中由于没有对变参起名,我们用默认的宏__VA_ARGS__来替代它。第二个宏中,我们显式地命名变参为args,那么我们在宏定义中就可以用args来代指变参了。同C语言的stdcall一样,变参必须作为参数表的最有一项出现。当上面的宏中我们只能提供第一个参数templt时,C标准要求我们必须写成:
myprintf(templt,);
的形式。这时的替换过程为:
myprintf("Error!\n",);
替换为:
fprintf(stderr,"Error!\n",);
这是一个语法错误,不能正常编译。这个问题一般有两个解决方法。首先,GNU CPP提供的解决方法允许上面的宏调用写成:
myprintf(templt);
而它将会被通过替换变成:
fprintf(stderr,"Error!\n",);
很明显,这里仍然会产生编译错误(非本例的某些情况下不会产生编译错误)。除了这种方式外,c99和GNU CPP都支持下面的宏定义方式:
#define myprintf(templt, ...) fprintf(stderr,templt, ##__VAR_ARGS__)
这时,##这个连接符号充当的作用就是当__VAR_ARGS__为空的时候,消除前面的那个逗号。那么此时的翻译过程如下:
myprintf(templt);
被转化为:
fprintf(stderr,templt);
这样如果templt合法,将不会产生编译错误。 这里列出了一些宏使用中容易出错的地方,以及合适的使用方式。