引言
C语言中用到宏定义的地方很多,例如为了防止头文件被重复包含,常用到:
#ifndef _CONFIG_H_
#define _CONFIG_H_
//头文件内容
#endif
在常用的标准库头文件中也可以见到很多宏定义,比如stdio.h头文件中:
#define BUFSIZ 512 //缓冲区大小
#define EOF (-1) //表文件末尾
#ifdef __GNUC__
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wshadow"
#endif
C语言的编译过程需要经过四个阶段:预处理—编译—汇编—链接,预处理工作是系统调用预处理程序对源程序中的预处理部分做处理,而预处理部分是指以“#”开头的、放在函数之外的、一般放在源文件前面的预处理命令,如:包括命令 #include,宏命令 #define 等,合理地利用预处理功能可以使得程序更加方便地阅读、修改、移植、调试等,也有利于模块化程序设计。
概念
// 一种最简单的宏的形式如下:
#define 宏名 替换文本
每个`#define`宏定义由三部分组成:第一部分是指令`#define`自身,“`#`”表示这是一条预处理指令,“`define`”是宏命令。第二部分为宏,一般为缩略语,其宏名称一般大写,而且不能用空格,遵循C变量命名规则。“替换文本”可以是任意常数、表达式、字符串等。在预处理工作过程中,代码中所有出现的“宏名”,都会被“替换文本”替换。这个替换的过程被称为“宏代换”或“宏展开”(macro expansion)。“宏代换”是由预处理程序自动完成的。注意宏不是语句,结尾不需要加“;”。在C语言中,“宏”分为两种:无参数 和 有参数。
无参数宏
无参宏是指宏名之后不带参数,例如:
#define MAX_LEN 10 // 宏定义
#define PI 3.14 //宏定义
int a[MAX_LEN]; // 会被替换为: int a[10];
int b = MAX_LEN; // 会被替换为: int b = 10;
printf("PI = %.2f\n", PI); // 输出结果为: PI = 3.14
如果要写的宏不止一行,则在结尾加反斜线符号使得多行能连接上,如:
#define HELLO "hello \
world" // 注意第二行要对齐,否则行与行之间的空格也会被作为替换文本的一部分
// 输出结果为:hello world
#define HELLO "hello wo \
rld"
printf("%s\n", HELLO);
// 输出结果为:hello wo rld
宏可以嵌套,但不参与运算:
#define M 5 // 宏定义
#define MM M * M // 宏的嵌套
printf("MM = %d\n", MM); // MM 被替换为: MM = M * M, 然后又变成 MM = 5 * 5
宏代换的过程在上句已经结束,实际的5 * 5
相乘过程是在编译阶段完成。宏只进行简单的文本替换,无论替换文本中是常数、表达式或者字符串等,预处理程序都不做任何检查,如果出现错误,只能是被宏代换之后的程序在编译阶段发现。
也可以用宏定义表示数据类型,可以使代码简便:
#define STU struct Student // 宏定义STU
struct Student{ // 定义结构体Student
char* name;
int sno;
};
STU stu = {"Jack", 20}; // 被替换为:struct Student stu = {"Jack", 20};
printf("name: %s, sNo: %d\n", stu.name, stu.sNo);
如果重复定义宏,则不同的编译器采用不同的重定义策略。有的编译器认为这是错误的,有的则只是提示警告。
#define M 5 // 第一次定义
#define M 100 // 再次定义, gcc编译器提示:,,lianxi.c:3: warning: "M" redefined
这些简单的宏主要被用来定义那些显式常量,会使得程序更加容易修改,特别是某一常量的值在程序中多次被用到的时候,只需要改动一个宏定义,则程序中所有出现该变量的值都可以被改变。而且宏定义还有更多其他优点,如使得程序更容易理解,可以控制条件编译等。<br />`#define` 与 `#typedef` 的区别:两者区别在于,宏定义只是简单的字符串替换,在预处理阶段完成。而typede不是简单的字符串替换,而是可以用来做类型说明符的重命名,类型的别名可以具有类型定义说明的功能,在编译阶段完成的。
#define INT1 int // 用宏定义表示数据类型
typedef int INT2; // 重命名数据类型
// 当一次声明一个变量时是等效的
INT1 a1 = 1;
INT2 a2 = 2;
// 当声明多个变量时,会出现问题
#define INT1 int*
typedef int* INT2;
INT1 a1, b1;
INT2 a2, b2;
b1 = &m; // warning: assignment to 'int' from 'int *' makes integer from pointer without a cast [-Wint-conversion]
b2 = &n; // OK
这里INT1 a1, b1;
被宏代换后为:int* a1, b1;
即定义的是一个指向int型变量的指针 a1 和一个int型的变量 b1。而INT2 a2, b2;
表示定义的是两个变量 a2 和 b2,这两个变量的类型都是 INT2 的,也就是 int * 的,所以两个都是指向int型变量的指针。
有参数宏
C语言中宏是可以有参数的,这样的宏就成了外形与函数相似的类函数宏,如:
#define MEAN(x, y) (((x)+(y))/2)
// 宏调用,宏名(实参表);
printf(“MEAN = %d\n”, MEAN(7, 9)); // 输出结果: MEAN = 8
和函数类似,在宏定义中的参数成为形式参数,在宏调用中的参数成为实际参数。而且和无参宏不同的一点是,有参宏在调用中,不仅要进行宏展开,而且还要用实参去替换形参。如:
#define M 5 //无参宏
#define COUNT(M) M * M //有参宏
printf("COUNT = %d\n", COUNT(10)); // 替换为: COUNT(10) = 10 * 10
// 输出结果: COUNT = 100
需要注意优先级问题,如:
#define COUNT(M) M * M //定义有参宏
int x = 6;
printf("COUNT = %d\n", COUNT(x + 1)); // 输出结果: COUNT = 13
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,解决办法最简单的是:不要在有参宏用使用到“++”、“–-”等。
宏定义也可以用来定义表达式或者多个语句。如:
#define JI(a,b) a = i + 3; b = j + 5; //宏定义多个语句
int i = 5, j = 10;
int m = 0, n = 0;
JI(m, n); // 宏代换后为: m = i + 3, n = j + 5;
printf("m = %d, n = %d\n", m, n); // 输出结果为: m = 8, n = 15
#运算符
比如我们宏定义了:
#define SUM (a,b) ((a) + (b))
我们想要输出“1 + 2 + 3 + 4 = 10”,用以下方式显得比较麻烦,有重复代码,而且中间还有括号:
printf("(%d + %d) + (%d + %d) = %d\n", 1, 2, 3, 4, SUM(1 + 2, 3+ 4));
此时可以用#
运算符在字符串中包含宏参数,把语言符号转化为字符串。例如,如果 a 是一个宏的形参,则替换文本中的#a
则被系统转化为“a”。而这个转化的过程成为“字符串化”。用这个方法实现上面的要求:
#define SUM(a,b) printf(#a" + "#b" = %d\n", ((a) + (b))) //宏定义,运用 # 运算符
SUM(1 + 2, 3 + 4); //宏调用
// 输出结果: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"
。
##运算符
和#
运算符一样,##
运算符也可以用在替换文本中,而它的作用是起到粘合的作用,即将两个语言符号组合成一个语言符号,所以又称为“预处理器的粘合剂”。用法:
#define NAME(n) num ## n //宏定义,使用 ## 运算符
int num0 = 10;
printf("num0 = %d\n", NAME(0)); //宏调用,NAME(0)被替换为 num ## 0,被粘合为:num0。
// 输出 num0 = 10
可变宏:… 和 VA_ARGS
我们经常要输出结果时要多次使用prinf("…", …)
,如果用上面例子#define SUM(a,b) printf(#a " + "#b" = %d\n",((a) + (b)))
,则格式比较固定,不能用于输出其他格式。这时我们可以考虑用可变宏。用法是:
#define PR2(X, ...) printf("Message"#X":"__VA_ARGS__) //宏定义
int msg = 10;
PR2(1, "msg = %d\n", msg); //宏调用
//输出结果:Message1:msg = 10
在宏定义中,形参列表的最后一个参数为省略号“…
”,而“__VA_ARGS__
”就可以被用在替换文本中,来表示省略号“…
”代表了什么。在宏调用中,X的值为1,所以#X
被替换为“1”。宏替换后为:printf("Message""1"":""msg = %.2f\n", msg)
,接着这4个字符串连接成一个:printf("Message1:msg = %.2f\n", msg)
。