文章结构如下:

宏(macro)是基于#define所实现的另一种预处理功能。
与基本的#define定义的是常量相比,宏(macro)允许多个参数化替换,参数中可以是固定的字符串,也被一些变量所替代。这个替换的操作将在预编译的时候完成。
宏(macro)的作用是用比较简单的方式表示复杂的函数调用,以提高程序的可读性。


宏的定义

宏的定义遵循以下格式:

1
#define macro_name(list_of_var) substitution_string

macro_name()括号中支持0个到多个参数,参数间用,分隔开;定义的最后一部分是宏的表达式,在实际的预处理中会将该表达式替换到代码正文中去,这里要特别注意的是替换这个词(坑)。
下面是一个宏的定义示例,用于求出两个数的乘积:

1
#define multi(x, y) x * y;

接收参数的宏看起来就像是函数。


宏的替换与代码展开

编译器在预编译时会把用到宏的代码替换成宏定义的表达式。
如上面定义的宏multi,可以如下代码中使用:
代码文件: macro.c

1
int multi = multi(2, 3);

在预编译时,替换操作的查看可以通过编译器命令查看效果,命令如下:

1
2
3
gcc -E macro.c > result.txt
// 或者 使用 -P 参数可以过滤到一些编译器链接标记符号信息
gcc -E -P macro.c > result.txt

打开result.txt则会看到宏的替换后代码展开的效果为:

1
int multi = 2 * 3;;   // 确实是两个";"  纯替换效果

最后根据表达式的运算,multi的值为6


宏的 替换 产生的问题

如上所示,宏 替换 操作是纯表达式的原文替换,连末尾的“;”都不会落下。那么,这在某此情况下会产生一些不符合预期的结果。
继续以宏multi为例,如果改一下参数如下:

1
int multi = multi(2, 3 + 2);

以上的代码我们希望得到的结果是10,但代码输出却是8。我们使用预编译命令将预处理过宏的代码展开查看如下:

1
int multi = 2 * 3 + 2;;

可见,由于预处理时,对宏的操作只是替换,经替换后的代码由于运算符的优先级问题,结果可能与预期的是不一致的。在这个例子中,会先运行乘法得到6后再加上2结果为8。以上的这种问题还会发生在”++”、”- -“运算上。
所以在有参数的宏中,为了使用运算与预期的一致,需要对参数增加左右括号以保证运算结果,并且取消在宏定义处”;”的结尾。定义写法如下:

1
#define multi(x, y) (x)*(y)

获取宏参数名称

在宏的表达式中,配合”#”的使用可以获取宏参数的名称。
如下定义了一个打印字符串的例子:

1
#define PrintStr(var) printf("var=%s", var)

在上面的表达式中,只能固定的打印出参数名称var=,但在项目中参数var的名称和其值一样是不固定的,有时候需要能够知道该var的命名,则可以这么做:

1
#define PrintStr(var) printf(#var"=%s" , var)

则可以获取当前的字符串名称的命名。运用如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define PrintStr(var) printf(#var"=%s" , var)

struct Student{
char name[20];
int age;
};

int main (int argc, char ** argv) {
struct Student mine = {"jack.ma", 20};
PrintStr(mine.name);
printf("\n\n");
PrintStr("break.line\n");
return 0;
}

则打印输出如下:
PrintStr
再使用gcc命令把宏展开后的代码贴出来看一下:

1
2
3
4
5
6
7
8
9
10
11
12
struct Student{
char name[20];
int age;
};

int main (int argc, char ** argv) {
struct Student mine = {"jack.ma", 20};
printf("mine.name""=%s" , mine.name);
printf("\n\n");
printf("\"break.line\\n\"""=%s" , "break.line\n");
return 0;
}

可以看出,在宏的表达式中#var的结果代码在预编译的时候会把参数的原文一字不动的替换到代码中。这样来获取参数的命名。


宏参数的结合

宏的表达式中还支持对宏参数进行拼接,定义如下:

1
#define macro_name(var1, var2, var3) var1##var2##var3

在上面的例子中,对宏的三个参数进行了拼接,拼接规则是在宏的表达式中,参数与参数之间用##连接起来,中间不能有空格。这个功能可以用于合成变量名称,或是从两个或多个宏参数中生成一个格式控制字符串。
用法示例:

1
2
3
4
5
6
7
8
9
10
#define PrintStr(var) printf(#var"=%s", var)
#define join(vA, vB, vC) vA##vB##vC

int main (int argc, char ** argv) {
char * name_car_1 = "Fit";

PrintStr(join(name, _car_, 1));
printf("\n\n");
return 0;
}

使用gcc命令对宏进行代码展开后的预处理代码为:

1
2
3
4
5
6
7
int main (int argc, char ** argv) {
char * name_car_1 = "Fit";

printf("join(name, _car_, 1)""=%s", name_car_1);
printf("\n\n");
return 0;
}

所以如上代码运行结果为:

1
join(name, _car_, 1)=Fit

宏的参数结合功能可以通过替换功能组合成一个新的参数或变量名称,但值得注意的是这些操作都是在编译的预处理过程中实现的,即代码运行之前就已经决定了新的参数或变量的最终形态,无法在代码的真实运行过程中再加工。


宏的取消

定义了一个宏后,要对宏进行取消,需要使用指令:

1
#undef macro_name

通常#undef会和#ifdef#ifndef等指令配合使用。


宏定义的换行连接

有时候宏的定义可能会很长,为了方便阅读,需要把宏的表达式做换行处理,仍以上面定义的宏multi为例可以这么操作:

1
2
#define multi(x, y) \
x * y

使用\进行换行连接,这样宏定义指令会在下一行的第一个非空白字符处继续。需要注意的是\必须是当前行的最后一个字符,其后必须是回车。


标准预处理宏

C/C++的编译器也支持了大量的标准预处理宏。这里只说下常用的几个,更多的详情资料请点击3.7. Predefined Macros
需要注意的是不同的编译器相同功能的宏名称可能命名不一样。

  • __FILE__
    把当前代码源文件的绝对路径表示为一个字符串常量。

  • __FUNCTION__
    也被命名为__func__,用于该宏所在函数体的函数名称。可以方便调试或异常判断。

  • __LINE__
    得到当前宏所在的代码的行数,是一个int型。一般和__FUNCTIN__中配合来标识源代码中的什么地方发生了某个事件。

  • __DATE__
    生成日期的字符串表达式,格式为Mmm dd yyyy,其中Mmm是月份如”Jan”、”Feb”等等。

  • __TIME__
    提供了包含时间值的字符串,格式是hh:mm:ss。再次强制宏只是在预处理的时间才进行的代码替换,所以这里的时间指的是编译器的运行时间点,而非程序运行时间。一般用这个宏来记录编译器的编译时间点。

代码示例:

1
2
3
4
5
6
7
8
int main (int argc, char ** argv) {
printf("current file name= %s\n", __FILE__);
printf("current function name= %s\n", __FUNCTION__);
printf("current line number= %d\n", __LINE__);
printf("current date= %s\n", __DATE__);
printf("current time= %s\n", __TIME__);
return 0;
}

宏展开后的代码为:

1
2
3
4
5
6
7
8
int main (int argc, char ** argv) {
printf("current file name= %s\n", "macro.c");
printf("current function name= %s\n", __FUNCTION__);
printf("current line number= %d\n", 15);
printf("current date= %s\n", "Mar 15 2015");
printf("current time= %s\n", "12:31:35");
return 0;
}