本文介绍在C语言中一位万能的,却经常耍大牌的替身演员,那就是宏。
阅读本文内容建议初步了解一下C语言代码的翻译过程,具体可见博文简单解析vim和gcc,为方便演示,本文将在Linux环境中进行演示。
一.宏常量
1.数值宏常量
(1)基本定义
注:按照规范,宏名称必须全大写。
#include <stdio.h>
# define M 20 //宏定义
int main()
{
printf("%d\n", M);
printf("%d\n", M*M);
return 0;
}
在第三行,我们定义了一个宏M,表示20这个数值,那么在下面的使用中,我们就可以用M代表20这个数值,运行看结果:
接下来我们只让代码进行预处理,我们看一看宏到底是如何操作的:
这是gcc编译器让代码只进行预处理之后的结果,当然,前面大段的头文件展开部分我们就不看了,可以看到我们的宏M的定义已经消失不见,而代码中所有使用M的地方都被替换成了20。
所以,我们可以得出结论,宏在使用的时候,是在预处理阶段进行文本替换。
(2)作用
这里我猜有些刚学习C语言的小伙伴就不理解了,为啥非要定义宏,我直接在代码中用20这个数字不香吗,我的回答是,真的不香~
比如,你要定义一个这样的宏:
#define PI 3.1415
表示数学中的圆周率的值,假设有一天你要修改PI的精度,把它改成3.14159265,如果定义成宏,你只要在宏定义处修改一次。否则的话,你要在代码出现这个数字的地方都要修改,不仅麻烦,而且很难不重不漏。
2.字符串宏常量
宏也可用来定义常量字符串,常用来表示路径:
#include <stdio.h>
# define PATH "C:\\Users\\25445\\Desktop\\tmp\\C++"
int main()
{
printf("%s\n", PATH);
return 0;
}
运行结果如下:
预处理后如下:
当然如果路径太长,你也可以使用续行符 \ 来进行多行书写。
#define PATH "C:\\Users\
\\25445\
\\Desktop\
\\tmp\\C++"
注意:
- 不要带一些多余的空格或缩进,因为这会包含着你的字符串中。
- 宏定义字符串时,必须带引号,因为宏的使用是进行文本替换。
- 表达文件路径时,因为 \ 转义字符的特殊性质,所以字符串中要使用 \\ 这点定义普通字符串常量时也要注意。
二.宏充当注释符号
我们上文说过,宏在使用时进行的是文本替换,那么我们可不可以这样使用宏:
#include <stdio.h>
#define BSC //
int main()
{
BSC printf("Hello World\n");
printf("Hey\n");
return 0;
}
把宏BSC定义成注释符 //,然后利用该宏进行注释操作,我们先思考一下,按照之前的理解,宏的使用是文本替换,那么BSC应该会被替换成 // 从而起到注释效果,好,我们直接看运行结果:
我们看到,编译器编译的时候并未保存,而运行结果却和我们意料的不同,原本应该被注释的语句依然正常的执行了,这是为什么呢?
同样的,我们看一下预处理的结果:
我们的宏BSC就像从未存在过一样。
下面解释原因:
我们知道预处理阶段所做的工作,可不仅仅是宏替换,还有去注释。而预处理时编译器去注释的工作要优先于宏替换,所以,对于 #define BSC // 这个宏,编译器首先会把 // 理解为注释,然后使用空格代替它,这样的话,我们的宏就变成了:
#define BSC
没错,就是一个空宏,所以 BSC printf(“Hello World\n”); 这条语句可以理解为就是在printf()前面加了一个空格,并没有任何影响。
至此,我们也可以理解下面这段代码:
#include <stdio.h>
#define BMC /*
#define EMC */
int main()
{
BSC printf("Hello World\n");
printf("Hey\n"); EMC
return 0;
}
这段代码意图想用宏 BSC 代替 /*, ESC 代替*/ ,从而起到注释的作用。细心的同学已经看到,当你在编译器里写下这段代码时 /* 后面的内容会被理解成注释,在预处理阶段去掉,所以这段代码会报错,原因是找不到 ESC 的定义。
三.宏定义表达式
1.简单表达式
宏不仅单纯的替换数值,还能替换表达式,如下:
#include <stdio.h>
#define SUM(x) x + x
int main()
{
printf("%d\n", SUM(10));
return 0;
}
运行结果为:
这里的宏使用有点类似于函数调用,但是请切记,宏的使用永远是文本替换。
我们看它预处理的结果:
同样的,宏的定义已经不在,SUM(10) 直接被替换成了 10 + 10,注意这里是直接替换成10+10,而不是替换成20,有什么区别?我们看下面这段代码:
#include <stdio.h>
#define SUM(x) x + x
int main()
{
printf("%d\n", 10*SUM(10));
return 0;
}
输出结果是什么?如果你对宏的使用是文本替换并不理解,那么你可能会说 200。我们看运行结果:
为什么是110?我们看一下预处理结果就明白了:
这就叫 文本替换~。
那么,如何解决这个问题?
很简单,这样定义宏就可以了:
#define SUM(x) ((x) + (x))
另外,还有一个小问题,就是定义宏的时候,不能随便带空格,比如这样:
#define SUM (x) ((x) + (x))
你在 SUM 和 (x)之间带了空格,编译就会认为,你定义了一个宏 SUM 内容是 (x) ((x) + (x)) 在宏替换的时候直接替换成 (x) ((x) + (x)) 就不对了。
但是,你在使用宏的时候,可以带空格,这时候编译器会自动忽略这个空格,比如这样是被允许的:
printf("%d\n", SUM (10)); //但是,这样写,到底想干什么呢~,严重不推荐。
总结:宏定义表达式时,一定不要吝啬括号的使用。空格的使用依照正常习惯就可,但要知道这个问题的存在。
2.复杂表达式
宏还可以定义多条语句,如下:
#include <stdio.h>
#define INIT_VALUES(a,b) \
a = 0;\
b=0;
int main()
{
int a = 100, b = 200;
printf("before:a=%d, b=%d\n", a, b);
INIT_VALUES(a, b);
printf("after:a=%d, b=%d\n", a, b);
return 0;
}
运行结果为:
我们看到a,b确实被初始化为0,0,但是,还是由于宏的文本替换的性质,这份代码还是有问题,比如:
#include <stdio.h>
#define INIT_VALUES(a,b) \
a = 0;\
b=0;
int main()
{
int a = 100, b = 200;
printf("before:a=%d, b=%d\n", a, b);
int flag = 1;
if (flag)
INIT_VALUES(a, b);
else
printf("NOT INIT\n");
printf("after:a=%d, b=%d\n", a, b);
return 0;
}
我们运行一下:
它报错了,信息竟然是没有与 else 匹配的 if , 我们知道if后面加一条语句是不用带大括号的,那为什么会报这种错误?
很简单,我们来看预处理结果(预处理阶段仅对代码做初步处理,是可以执行的):
由于文本替换的性质,我们a=0; b=0;会直接替换到 if 判断的下面,在加上你写的 ; 一共就是三条语句,所以 else 自然就找不到属于它的 if 了~
解决方案其实也很简单,只要养成良好的编程习惯,即使 if 后面看似只有一条语句,我们也给它加上大括号,这个问题就解决了,可是,这并不是一个健壮的宏的写法。
有的同学可能会想,既然如此,我们这样定义宏:
#define INIT_VALUES(a,b) {\
a = 0;\
b=0;}
手工加上大括号不就行了?我们试一试:
依然报错,依然是 else 匹配不到 if 为什么?简单,看预处理:
没错,你调完这个宏后,直接习惯性的加了个分号,这个分号形成了一条空语句夹在了 if 和 else 之间~。
好,我们来看看正确的写法吧:
#define INIT_VALUES(a,b) \
do{\
a = 0;\
b=0;\
}while(0)
结果为:
程序正确运行了。
我们来看看预处理:
这种结果被称为 do-while-zero结构 ,这种结构巧妙的运用了 do {}while(); 的语法特性,避免了上述代码中的定义,请注意,定义宏的时候,while() 后面的分号不要带,留到你使用的时候再带。
四.取消宏定义 #undef
既然有宏定义,那么就宏的取消定义,这就是 #undef,不过再看这个之前,我们首先看两个问题。
- 宏只能在main上面定义吗?
- 在一个源文件内,宏的有效范围是什么?
1.宏的有效范围
我们上面定义的宏都是在文件开头,这也是最为规范的写法,那么我们能不能在函数内定义宏呢?
#include <stdio.h>
int main()
{
#define M 20
printf("%d\n", M);
return 0;
}
没有问题:
那么这样呢:
#include <stdio.h>
void show()
{
#define M 20
printf("%d\n", M);
}
int main()
{
show();
printf("%d\n", M);
return 0;
}
注意,我的宏是在show()函数中定义,调用show()函数,打印宏,然后main()中又单独打印这个宏。
结果如下:
我们发现,两次打印均成功。到这里,我们可以看到,宏的有效范围,确实和普通变量有很大差距。
下面给出结论:
宏的有效范围,是从定义处往下到#undef处有效,之前无效,若无#undef则向下一直有效。
这里的向下就是指文本层面上的向下,和是否定义在函数或代码块内部无关。
2.undef
顾名思义,取消宏的定义,演示如下。
#include <stdio.h>
#define M 20
int main()
{
printf("%d\n", M);
#undef M
printf("%d\n", M);
return 0;
}
会报错,因为执行到第二个printf()的时候,宏M的定义已经被取消。
结论:undef是取消宏的意思,可以用来限定宏的有效范围。
关于这个限定,要正确理解,如这段代码:
#include <stdio.h>
#define M 20
void show()
{
printf("%d\n", M);
}
int main()
{
#undef M
show();
return 0;
}
在取消宏定义后调用show()函数,打印宏,结果为:
仍然打印出了宏M的值,因为show()函数被定义时,宏并未取消,预处理阶段,show()函数中的宏 M 会被直接替换为20。
五.总结
本文介绍了宏的几种使用方式,常见错误,与注意点,但我想大家也感受到了,就像文章开头说的一样,宏这位演技超高的替身演员经常会"耍大牌",时不时就给你一个意想不到的Bug,所以除非我们使用宏的确利大于弊,否则,宏这种东西尽量少用,定义常量可以用const修饰变量,表达式可以使用函数。
最后,鉴于笔者水平有限,若有大佬指出错误或不足之处,笔者感激不尽。