万能的替身演员 #define

本文介绍在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++"

注意:

  1. 不要带一些多余的空格或缩进,因为这会包含着你的字符串中。
  2. 宏定义字符串时,必须带引号,因为宏的使用是进行文本替换。
  3. 表达文件路径时,因为 \ 转义字符的特殊性质,所以字符串中要使用 \\ 这点定义普通字符串常量时也要注意。

二.宏充当注释符号

我们上文说过,宏在使用时进行的是文本替换,那么我们可不可以这样使用宏:

#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,不过再看这个之前,我们首先看两个问题。

  1. 宏只能在main上面定义吗?
  2. 在一个源文件内,宏的有效范围是什么?

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修饰变量,表达式可以使用函数。

最后,鉴于笔者水平有限,若有大佬指出错误或不足之处,笔者感激不尽。

  • 12
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值