宏定义

#define命令还可以定义带参数的宏定义,用于实现某种特定的功能,其定义型式为:

1
2
3
4
5
6
//不带参数宏
#define PI 3.14159 // 定义圆周率pi的值
//带参数宏
#define MAX(a,b) ((a)>(b) ? (a) : (b)) // 求最大值
//变参宏
#define eprintf(format, ...) fprintf (stderr, format, ##__VA_ARGS__)

不过,由于C++增加了内联函数(inline),实现起来比带参数的宏更方便,这样的宏在C++中已经很少使用了。

优点

  • 提高了程序的可读性,同时也方便进行修改;
  • 提高程序的运行效率:使用带参的宏定义既可完成函数调用的功能,又能避免函数的出栈与入栈操作,减少系统开销,提高运行效率;
  • 宏是由预处理器处理的,通过字符串操作可以完成很多编译器无法实现的功能。比如##连接符。

缺点

  • 由于是直接嵌入的,所以代码可能相对多一点
  • 嵌套定义过多可能会影响程序的可读性,而且很容易出错;
  • 对带参的宏而言,由于是直接替换,并不会检查参数是否合法,存在安全隐患。

    补充: 预编译语句仅仅是简单的值代替,缺乏类型的检测机制。这样预处理语句就不能享受C++严格的类型检查的好处,从而可能成为引发一系列错误的隐患。

的确,宏定义给我们带来很多方便之处,但是必须正确使用,否则,可能会出现一些意想不到的问题。

1 可变参数宏

变参宏可以传递变参,类似于函数,如

1
2
#define myprintf(templt,…) fprintf(stderr,templt,__VA_ARGS__)
#define myprintf(templt,args…) fprintf(stderr,templt,args)
  • 第一个宏中由于没有对变参起名,我们用默认的宏VA_ARGS来替代它。
  • 第二个宏中,我们显式地命名变参为args,那么我们在宏定义 中就可以用args来代指变参了。
  • 同C语言的stdcall一样,变参必须作为参数表的最后一项出现

当上面的宏中我们只能提供第一个参数templt 时,C标准要求我们必须写成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
myprintf(templt,);

myprintf("Error!/n",);//只有一个参数
//会被替换为
fprintf(stderr,"Error!/n",);//语法错误,不能正常编译
//换一种写法
myprintf(templt);
//会被替换为
fprintf(stderr,"Error!/n",);//依然是错误

//正确的写法如下
#define myprintf(templt, …) fprintf(stderr,templt, ##__VAR_ARGS__)
//##这个连接符号充当的作用就是当__VAR_ARGS__为空的时候,消除前面的那个逗号
myprintf(templt);
//被转换为
fprintf(stderr,templt);

总结:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//gcc特色的“双井号”(##),是用于解决尾随逗号问题的,例如:

//错误示例
//如果
#define eprintf(format, args...) fprintf (stderr, format, args)
//或
#define eprintf(format, ...) fprintf (stderr, format, __VA_ARGS__)
//那么
eprintf("%d %d\n", 10, 14);
//扩展成
fprintf (stderr, "%d %d\n", 10, 14);
//编译通过。
//但是,
eprintf("Hello World\n");
//扩展成
fprintf (stderr, "Hello World\n", );//此代码存在尾随逗号,于是编译错误。


//正确示例
//如果
#define eprintf(format, args...) fprintf (stderr, format, ##args)
//或
#define eprintf(format, ...) fprintf (stderr, format, ##__VA_ARGS__)
//或
#define eprintf(format, ...) fprintf (stderr, format __VA_OPT__(,) __VA_ARGS__)
//那么
eprintf("%d %d\n", 10, 14);
//扩展成
fprintf (stderr, "%d %d\n", 10, 14);
//编译通过。
eprintf("Hello World\n");
//扩展成
fprintf (stderr, "Hello World\n");//编译通过。

1.2 宏的运算符

1.2.1 换行符

作用:当定义的宏不能用一行表达完整时,可以用\表示下一行继续此宏的定义。

1
2
3
4
5
6
7
8
9
10
#define NAME "Zhang"  \
"fei" \
" 你好!"

int main(int argc, char **argv) {
// 范例1
std::cout << NAME << std::endl;

return 0;
}

输出

1
Zhangfei 你好

1.2.2 字符串化运算符

#将宏定义中的传入参数名转换成用一对双引号括起来参数名字符串;

1
2
3
4
5
#define example( instr )  printf( "the input string is:\t%s\n", #instr )
#define example1( instr ) #instr

example( abc ); // 在编译时将会展开成:printf("the input string is:\t%s\n","abc")
string str = example1( abc ); // 将会展成:string str="abc"

注意, 对空格的处理:

  • 忽略传入参数名前面和后面的空格。

    1
    2
    如:str=example1(   abc );//将会被扩展成 str="abc"

  • 当传入参数名间存在空格时,编译器将会自动连接各个子字符串,用每个子字符串之间以一个空格连接,忽略剩余空格。

    1
    如:str=exapme( abc    def);//将会被扩展成 str="abc def"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define checkRuntime(op)  __check_cuda_runtime((op), #op, __FILE__, __LINE__)

bool __check_cuda_runtime(cudaError_t code, const char* op, const char* file, int line){
if(code != cudaSuccess){
const char* err_name = cudaGetErrorName(code);
const char* err_message = cudaGetErrorString(code);
printf("runtime error %s:%d %s failed. \n code = %s, message = %s\n", file, line, op, err_name, err_message);
return false;
}
return true;
}

//调用
checkRuntime(cudaMalloc(&device_ptr, size));

分析:

  • 调用checkRuntime(cudaMalloc(&device_ptr, size));时展开为__check_cuda_runtime(cudaMalloc(&device_ptr, size), "cudaMalloc(&device_ptr, size)", __FILE__, __LINE__)

1.2.3 符号连接操作符

##作用:将宏定义的多个形参转换成一个实际参数名。

1
2
3
4
5
6
7
8

#define exampleNum( n ) num##n

int num9 = 9;
int num = exampleNum( 9 ); // 将会扩展成 int num = num9

#define exampleNum( n ) num ## n
// 相当于 #define exampleNum( n ) num##n

1.2.4 单字符化操作符

#@将传入单字符参数名转换成字符,以一对单引用括起来。

1
2
#define makechar(x)    #@x
char a = makechar( b ); //展开后变成了:a = 'b';

条件表达式

空语句

1
(c % 2 !=0) ? c *= 2 : **(void)**1;