C/C++宏的用法

今天看caffe源码的时候看到了不少宏定义的内容,苦于代码基础薄弱,没法所有理解,故在网上搜得此篇好文,转载一发
附原文地址:http://blog.csdn.net/hanchaoman/article/details/8809951/   侵删~ios

 

宏替换是C/C++系列语言的技术特点,C/C++语言提供了强大的宏替换功能,源代码在进入编译器以前,要先通过一个称为“预处理器”的模块,这个模块将宏根据编译参数和实际编码进行展开,展开后的代码才正式进入编译器,进行词法分析、语法分析等等。算法

    咱们经常使用的宏替换主要有这么几个类型。
1.宏常量
    在ACM等算法竞赛中,常常会把数组的最大下标经过宏定义的方法给出,以方便调试,例如:
#define MAX 1000c#

int array[MAX][MAX]
......数组

for(int i = 0; i < MAX; i++)
......编辑器

    将一个数字定义成全局的常量,这个用法在国产垃圾教材上十分常见。但在经典著做《Effective C++》中,这种作法却并不提倡,书中更加推荐以const常量来代替宏常量。由于在进行词法分析时,宏的引用已经被其实际内容替换,所以宏名不会出如今 符号表中。因此一旦出错,看到的将是一个无心义的数字,好比上文中的1000,而不是一个有意义的名称,如上文中的MAX。而const在符号表中会有自 己的位置,所以出错时能够看到更加有意义的错误提示。ide

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“函数”要比使用指针参数效率还要高,由于它连指针参数都不用传递而是使用直接代码,对于一些效率要求比较明显的场合,宏仍是首选。

四、取消宏定义
#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就是惟一一处放置条件编译开关的地方,更有利于维护。

五、注意事项
1)、普通宏定义
(1)宏名通常用大写
(2)使用宏可提升程序的通用性和易读性,减小不一致性,减小输入错误和便于修改。
(3)预处理是在编译以前的处理,而编译工做的任务之一就是语法检查,预处理不作语法检查。
(4)宏定义末尾不加分号;
(5)宏定义写在函数的花括号外边,做用域为其后的程序,一般在文件的最开头。
(6)能够用#undef命令终止宏定义的做用域
(7)宏定义能够嵌套
(8)字符串""中永远不包含宏
(9)宏定义不分配内存,变量定义分配内存。
2)、带参宏定义
(1)实参若是是表达式容易出问题
(2)宏名和参数的括号间不能有空格
(3)宏替换只做替换,不作计算,不作表达式求解
(4)函数调用在编译后程序运行时进行,而且分配内存。宏替换在编译前进行,不分配内存
(5)宏的哑实结合不存在类型,也没有类型转换。
(6)函数只有一个返回值,利用宏则能够设法获得多个值
(7)宏展开使源程序变长,函数调用不会
(8)宏展开不占运行时间,只占编译时间,函数调用占运行时间(分配内存、保留现场、值传递、返回值)

六、关于#和##
在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;

七、关于...的使用
在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合法,将不会产生编译错误。 这里列出了一些宏使用中容易出错的地方,以及合适的使用方式。

相关文章
相关标签/搜索