c/c++ 宏
宏替换是 C/C++ 系列语言的技术特色,C/C++ 语言提供了强大的宏替换功能,源代码在进入编译器之前,要先经过一个称为“预处理器”的模块,这个模块将宏根据编译参数和实际编码进行展开,展开后的代码才正式进入编译器,进行词法分析、语法分析等等。
常用的宏替换类型:
一、宏常量
在ACM等算法竞赛中,经常会把数组的最大下标通过宏定义的方法给出,以方便调试,例如:
#define MAX 1000
int array[MAX][MAX]
……
for(int i = 0; i < MAX; i++)
……
将一个数字定义成全局的常量,这个用法在国产垃圾教材上十分常见。但在经典著作《Effective C++》中,这种做法却并不提倡,书中更加推荐以 const 常量来代替宏常量。因为在进行词法分析时,宏的引用已经被其实际内容替换,因此宏名不会出现在符号表中。所以一旦出错,看到的将是一个无意义的数字,比如上文中的 1000,而不是一个有意义的名称,如上文中的 MAX。而 const 在符号表中会有自己的位置,因此出错时可以看到更加有意义的错误提示。
二、用于条件编译标识的宏
#define 常与 #ifdef or #ifndef or defined 指令配合使用,用于条件编译。
#ifndef _HEADER_INC_
#define _HEADER_INC_
……
……
#endif
// 这种宏标记在头文件中十分常见,用于防止头文件被反复包含。
// 应该养成习惯在每个头文件中都添加这种标记。还有一种用于条件编译的用法。
#ifdef DEBUG
printf("{“}Debug information\n");
#endif
通过 DEBUG 宏,我们可以在代码调试的过程中输出辅助调试的信息。当 DEBUG 宏被删除时,这些输出的语句就不会被编译。更重要的是,这个宏可以通过编译参数来定义。因此通过改变编译参数,就可以方便的添加和取消这个宏的定义,从而改变代码条件编译的结果。 在条件编译时建议使用 #if defined 和 #if !defined 来代替使用 #ifdef or #ifndef,因为前者更方便处理多分支的情况与较复杂条件表达式的情况。
#ifdef or #ifndef 只能处理两个分支:#ifdef or #ifndef, #else, #endfi;
#if defined 和 #if !defined 可以处理多分支的情况:#if defined or #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
条件编译时,如果一个文件中太多条件编译的代码,有些编辑器的智能感知可能都不能很好地解析,还是保持代码越简单越好。对于函数级别的条件编译主要有两种实现方式:
-
- 同一个函数声明,同一个函数定义,函数体内使用条件编译代码。这种方式有个问题,如果条件编译代码太多,会导致这个函数体很长,不利于阅读与维护;有一个优点是,有利于编辑器的智能感知,因为这样解析函数名比较方便,但随着编辑器功能的完善,这方面的差别就不明显了。
-
- 根据编译条件,将编译条件相同的代码放到单独的文件中,这些文件在顶层文件中使用条件编译指令来引用。这种方式最大的优点就是不同平台的程序由不同的源文件来实现,很便于多人分工合作,对于某一部分代码由一个人实现并测试完成后直接把源文件复制过来就可以了,进行低层次的单元测试非常方便;它的缺点就是增加了目录中的文件数量。
三、宏函数
宏函数的语法有以下特点:
-
- 如果需要换行,则行末要加反斜杠“\”表示换行。宏函数体的最后一行不加反斜杠。
-
- 假设有参数 ARGU,值为 argu,则所有的ARGU被直接替换为argu,#ARGU 被认为是字符串,会被替换成”argu”(带引号)。
-
- 由于宏函数是直接替换,所有一般情况下对宏函数的调用时行末不需要加分号。 宏函数的作用:
- 避免函数调用,提高程序效率
常用的就是最大值与最小值的判断函数,由于函数内容并不多,如果定义为函数在调用比较频繁的场合会明显降低程序的效率,其实宏是用空间效率换取了时间效率。如取两个值的最大值:
#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 ;
}
使用宏函数的优点有两个:
-
- 适用于任何实现了operator<的类型,包括自定义类型;
-
- 效率最高。虽然使用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
- 引用编译期数据
上述的这些作用虽然使用宏函数可以取得更好的性能,但如果从功能上讲完全可以不使用宏函数,而使用模板函数或普通函数实现,但还有些时候只能通过宏实现。例如,程序中在执行某些操作时可能会失败,此时要打印出失败的代码位置,只能使用宏实现。
#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()
}
- 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 像函数一样用,而且还有优点:
-
- 在宏定义中可以使用局部变量;
-
- 在宏定义中可以包含多个语句,但可以当作一条语句使用,如代码中的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 就是唯一一处放置条件编译开关的地方,更有利于维护。
五、注意事项
- 普通宏定义
- 宏名一般用大写;
- 使用宏可提高程序的通用性和易读性,减少不一致性,减少输入错误和便于修改;
- 预处理是在编译之前的处理,而编译工作的任务之一就是语法检查,预处理不做语法检查;
- 宏定义末尾不加分号;
- 宏定义写在函数的花括号外边,作用域为其后的程序,通常在文件的最开头;
- 可以用 #undef 命令终止宏定义的作用域;
- 宏定义可以嵌套;
- 字符串 “” 中永远不包含宏;
- 宏定义不分配内存,变量定义分配内存;
- 带参宏定义
- 实参如果是表达式容易出问题;
- 宏名和参数的括号间不能有空格;
- 宏替换只作替换,不做计算,不做表达式求解;
- 函数调用在编译后程序运行时进行,并且分配内存。宏替换在编译前进行,不分配内存;
- 宏的哑实结合不存在类型,也没有类型转换;
- 函数只有一个返回值,利用宏则可以设法得到多个值;
- 宏展开使源程序变长,函数调用不会;
- 宏展开不占运行时间,只占编译时间,函数调用占运行时间(分配内存、保留现场、值传递、返回值);
六、关于 # 和 ##
# – 字符化操作
作用是将宏定义参数不经任何扩展地转换成字符串常量(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的时候便会在标准错误流上输出一个提示信息。
## 标记连接操作
## 的作用是在宏定义中,用来将两个 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 合法,将不会产生编译错误。
八、关于 ## 消除__VAR_ARGS__前面的逗号
# 与 ## 在宏定义中的–宏展开
#include <stdio.h>
#define f(a,b) a##b
#define g(a) #a
#define h(a) g(a)
int main()
{
printf("%s\n", g(f(1,2))); // f(1,2)
printf("%s\n", h(f(1,2))); // 12
return 0;
}
宏展开时:
-
如果宏定义以 # 开头,不展开参数,直接替换。
故 g(f(1,2))—>#f(1,2)—>”f(1,2)”;
-
如果宏定义不以 # 开头,展开参数,直接替换,由外层向里层,如果碰到的是 # 开头的宏,不继续往里层展开,往外层展开。由外层向里层,如果碰到的是以非 # 开头的宏,继续往里层走,直至最里层,开始一层层往外层展开。
故 h(f(1,2))—>h(12)—>g(12)—->#12—–>”12”。
九、caffe 源码程序入口宏
#define RegisterBrewFunction(func) \
namespace { \
class __Registerer_##func { \
public: /* NOLINT */ \
__Registerer_##func() { \
g_brew_map[#func] = &func; \
} \
}; \
__Registerer_##func g_registerer_##func; \
定义了一个注册函数,把函数的指针保存到一个容器中。容器定义如下
typedef int (*BrewFunction)();
typedef std::map<caffe::string, BrewFunction> BrewMap;
BrewMap g_brew_map;
主函数入口为:
gflags::SetUsageMessage("command line brew\n"
"usage: caffe <command> <args>\n\n"
"commands:\n"
" train train or finetune a model\n"
" test score a model\n"
" device_query show GPU diagnostic information\n"
" time benchmark model execution time");
// Run tool or show usage.
caffe::GlobalInit(&argc, &argv);
if (argc == 2) {
return GetBrewFunction(caffe::string(argv[1]))();
从这个函数中把四种参数传入,函数定义如下
// input : caffe::string(train,test...)
// output : BrewFunction function pointer
static BrewFunction GetBrewFunction(const caffe::string& name) {
// use map type to check name appears frequency
if (g_brew_map.count(name)) {
return g_brew_map[name];
} else {
// if do not find the specified name,the output error
LOG(ERROR) << "Available caffe actions:";
// Traverse the entire map, output an error message
for (BrewMap::iterator it = g_brew_map.begin();
it != g_brew_map.end(); ++it) {
LOG(ERROR) << "\t" << it->first;
}
LOG(FATAL) << "Unknown action: " << name;
return NULL; // not reachable, just to suppress old compiler warnings.
}
}
从这里可以看出,传入参数,看看这个参数和 g_brew_map 是这个容器里面的吗,是的话,然后函数指针,利用该函数指针来调用相应的函数。