引言

C语言中用到宏定义的地方很多,例如为了防止头文件被重复包含,常用到:

  1. #ifndef _CONFIG_H_
  2. #define _CONFIG_H_
  3. //头文件内容
  4. #endif

在常用的标准库头文件中也可以见到很多宏定义,比如stdio.h头文件中:

  1. #define BUFSIZ 512 //缓冲区大小
  2. #define EOF (-1) //表文件末尾
  3. #ifdef __GNUC__
  4. #pragma GCC diagnostic push
  5. #pragma GCC diagnostic ignored "-Wshadow"
  6. #endif

C语言的编译过程需要经过四个阶段:预处理—编译—汇编—链接,预处理工作是系统调用预处理程序对源程序中的预处理部分做处理,而预处理部分是指以“#”开头的、放在函数之外的、一般放在源文件前面的预处理命令,如:包括命令 #include,宏命令 #define 等,合理地利用预处理功能可以使得程序更加方便地阅读、修改、移植、调试等,也有利于模块化程序设计。

概念

  1. // 一种最简单的宏的形式如下:
  2. #define 宏名 替换文本
  1. 每个`#define`宏定义由三部分组成:第一部分是指令`#define`自身,“`#`”表示这是一条预处理指令,“`define`”是宏命令。第二部分为宏,一般为缩略语,其宏名称一般大写,而且不能用空格,遵循C变量命名规则。“替换文本”可以是任意常数、表达式、字符串等。在预处理工作过程中,代码中所有出现的“宏名”,都会被“替换文本”替换。这个替换的过程被称为“宏代换”或“宏展开”(macro expansion)。“宏代换”是由预处理程序自动完成的。注意宏不是语句,结尾不需要加“;”。在C语言中,“宏”分为两种:无参数 有参数。

无参数宏

无参宏是指宏名之后不带参数,例如:

  1. #define MAX_LEN 10 // 宏定义
  2. #define PI 3.14 //宏定义
  3. int a[MAX_LEN]; // 会被替换为: int a[10];
  4. int b = MAX_LEN; // 会被替换为: int b = 10;
  5. printf("PI = %.2f\n", PI); // 输出结果为: PI = 3.14

如果要写的宏不止一行,则在结尾加反斜线符号使得多行能连接上,如:

  1. #define HELLO "hello \
  2. world" // 注意第二行要对齐,否则行与行之间的空格也会被作为替换文本的一部分
  3. // 输出结果为:hello world
  4. #define HELLO "hello wo \
  5. rld"
  6. printf("%s\n", HELLO);
  7. // 输出结果为:hello wo rld

宏可以嵌套,但不参与运算:

  1. #define M 5 // 宏定义
  2. #define MM M * M // 宏的嵌套
  3. printf("MM = %d\n", MM); // MM 被替换为: MM = M * M, 然后又变成 MM = 5 * 5

宏代换的过程在上句已经结束,实际的5 * 5相乘过程是在编译阶段完成。宏只进行简单的文本替换,无论替换文本中是常数、表达式或者字符串等,预处理程序都不做任何检查,如果出现错误,只能是被宏代换之后的程序在编译阶段发现。
也可以用宏定义表示数据类型,可以使代码简便:

  1. #define STU struct Student // 宏定义STU
  2. struct Student{ // 定义结构体Student
  3. char* name;
  4. int sno;
  5. };
  6. STU stu = {"Jack", 20}; // 被替换为:struct Student stu = {"Jack", 20};
  7. printf("name: %s, sNo: %d\n", stu.name, stu.sNo);

如果重复定义宏,则不同的编译器采用不同的重定义策略。有的编译器认为这是错误的,有的则只是提示警告。

  1. #define M 5 // 第一次定义
  2. #define M 100 // 再次定义, gcc编译器提示:,,lianxi.c:3: warning: "M" redefined
  1. 这些简单的宏主要被用来定义那些显式常量,会使得程序更加容易修改,特别是某一常量的值在程序中多次被用到的时候,只需要改动一个宏定义,则程序中所有出现该变量的值都可以被改变。而且宏定义还有更多其他优点,如使得程序更容易理解,可以控制条件编译等。<br />`#define` `#typedef` 的区别:两者区别在于,宏定义只是简单的字符串替换,在预处理阶段完成。而typede不是简单的字符串替换,而是可以用来做类型说明符的重命名,类型的别名可以具有类型定义说明的功能,在编译阶段完成的。
  1. #define INT1 int // 用宏定义表示数据类型
  2. typedef int INT2; // 重命名数据类型
  3. // 当一次声明一个变量时是等效的
  4. INT1 a1 = 1;
  5. INT2 a2 = 2;
  6. // 当声明多个变量时,会出现问题
  7. #define INT1 int*
  8. typedef int* INT2;
  9. INT1 a1, b1;
  10. INT2 a2, b2;
  11. b1 = &m; // warning: assignment to 'int' from 'int *' makes integer from pointer without a cast [-Wint-conversion]
  12. b2 = &n; // OK

这里INT1 a1, b1;被宏代换后为:int* a1, b1;即定义的是一个指向int型变量的指针 a1 和一个int型的变量 b1。而INT2 a2, b2;表示定义的是两个变量 a2 和 b2,这两个变量的类型都是 INT2 的,也就是 int * 的,所以两个都是指向int型变量的指针。

有参数宏

C语言中宏是可以有参数的,这样的宏就成了外形与函数相似的类函数宏,如:

  1. #define MEAN(x, y) (((x)+(y))/2)
  2. // 宏调用,宏名(实参表);
  3. printf(“MEAN = %d\n”, MEAN(7, 9)); // 输出结果: MEAN = 8

和函数类似,在宏定义中的参数成为形式参数,在宏调用中的参数成为实际参数。而且和无参宏不同的一点是,有参宏在调用中,不仅要进行宏展开,而且还要用实参去替换形参。如:

  1. #define M 5 //无参宏
  2. #define COUNT(M) M * M //有参宏
  3. printf("COUNT = %d\n", COUNT(10)); // 替换为: COUNT(10) = 10 * 10
  4. // 输出结果: COUNT = 100
  1. 需要注意优先级问题,如:
  1. #define COUNT(M) M * M //定义有参宏
  2. int x = 6;
  3. printf("COUNT = %d\n", COUNT(x + 1)); // 输出结果: COUNT = 13
  4. printf("COUNT = %d\n", COUNT(++x)); // 输出结果: COUNT = 56

如果是像函数那样的话,COUNT(x + 1)应该相当于COUNT(7),结果应该是7 7 = 49,但输出结果却是13。原因在于,预处理器不进行计算,只是进行字符串替换,而且也不会自动加上括号(),所以COUNT(x + 1)被替换为`COUNT(x + 1 x + 1),代入x = 6,即为6 + 1 * 6 + 1 = 13。而解决办法则是:尽量用括号把整个替换文本及其中的每个参数括起来:#define COUNT(M) ((M) (M))。但即使用括号,也不能解决上面例子的最后一个情况,COUNT(++x) 被替换为++x ++x`,即为 7 8 = 56,而不是想要 7 7 = 49,解决办法最简单的是:不要在有参宏用使用到“++”、“–-”等。
宏定义也可以用来定义表达式或者多个语句。如:

  1. #define JI(a,b) a = i + 3; b = j + 5; //宏定义多个语句
  2. int i = 5, j = 10;
  3. int m = 0, n = 0;
  4. JI(m, n); // 宏代换后为: m = i + 3, n = j + 5;
  5. printf("m = %d, n = %d\n", m, n); // 输出结果为: m = 8, n = 15

#运算符

比如我们宏定义了:

  1. #define SUM (a,b) ((a) + (b))

我们想要输出“1 + 2 + 3 + 4 = 10”,用以下方式显得比较麻烦,有重复代码,而且中间还有括号:

  1. printf("(%d + %d) + (%d + %d) = %d\n", 1, 2, 3, 4, SUM(1 + 2, 3+ 4));

此时可以用#运算符在字符串中包含宏参数,把语言符号转化为字符串。例如,如果 a 是一个宏的形参,则替换文本中的#a则被系统转化为“a”。而这个转化的过程成为“字符串化”。用这个方法实现上面的要求:

  1. #define SUM(a,b) printf(#a" + "#b" = %d\n", ((a) + (b))) //宏定义,运用 # 运算符
  2. SUM(1 + 2, 3 + 4); //宏调用
  3. // 输出结果:1 + 2 + 3 + 4 = 10

调用宏时,用 1 + 2 代替 a,用 3 + 4 代替 b,则替换文本为:printf("1 + 2"" + ""3 + 4"" = %d\n", ((1 + 2) + (3 + 4))),接着字符串连接功能将四个相邻的字符串转换为一个字符串:"1 + 2 + 3 + 4 = %d\n"

##运算符

#运算符一样,##运算符也可以用在替换文本中,而它的作用是起到粘合的作用,即将两个语言符号组合成一个语言符号,所以又称为“预处理器的粘合剂”。用法:

  1. #define NAME(n) num ## n //宏定义,使用 ## 运算符
  2. int num0 = 10;
  3. printf("num0 = %d\n", NAME(0)); //宏调用,NAME(0)被替换为 num ## 0,被粘合为:num0。
  4. // 输出 num0 = 10

可变宏:… 和 VA_ARGS

我们经常要输出结果时要多次使用prinf("…", …),如果用上面例子#define SUM(a,b) printf(#a " + "#b" = %d\n",((a) + (b))),则格式比较固定,不能用于输出其他格式。这时我们可以考虑用可变宏。用法是:

  1. #define PR2(X, ...) printf("Message"#X":"__VA_ARGS__) //宏定义
  2. int msg = 10;
  3. PR2(1, "msg = %d\n", msg); //宏调用
  4. //输出结果:Message1:msg = 10

在宏定义中,形参列表的最后一个参数为省略号“”,而“__VA_ARGS__”就可以被用在替换文本中,来表示省略号“”代表了什么。在宏调用中,X的值为1,所以#X被替换为“1”。宏替换后为:printf("Message""1"":""msg = %.2f\n", msg),接着这4个字符串连接成一个:printf("Message1:msg = %.2f\n", msg)