引言
我们在初识C语言部分已经了解过,#define可以用来定义标识符常量和宏:
//#define定义标识符常量
#define MAX 10
//#define定义宏
#define SQUARE(n) n*n
在上面的代码中使用#define定义了一个标识符常量MAX,值为10;又定义了一个宏ADDONE(n),可以实现对n+1。到现在,相信大家对定义的格式与大致的使用已经有了解了。
在这篇文章中就详细介绍一下关于#define定义标识符常量和宏的知识,包括该定义作用的阶段、效果以及需要注意的一些问题。
#define作用的阶段与特点
#define其实是C语言的一个预处理指令,在代码开始执行前作用。
可能会有小伙伴不知道预处理是什么,这里简单补充一些程序翻译与执行的知识:
C语言程序的翻译与执行
我们都知道,数据在内存中是以二进制的形式存储的,但是我们编写的代码又是字符的形式。所以在我们编写的代码被执行前,是需要进行一系列的操作的,包括但不限于将其转化为可执行的二进制指令:
翻译部分
这部分的操作需要又可以分为两个部分:编译与链接。
编译
我们一个工程中,可能会有许多的文件,包括源文件与头文件。
编译器会将每一个源文件分别进行编译操作:
分为三个阶段:预编译、编译与汇编。
预编译部分,就是预处理指令执行的部分:
包括将#include包含的头文件拷贝到源文件中; #define定义标识符常量的替换;注释的删除等。
预编译结束后,会生成一个.i文件。
编译部分,将预编译后的文件转换为汇编代码:
包括语法分析、词法分析、语义分析、符号汇总。
编译结束后,会生成一个.s文件。
汇编部分,将汇编代码转化为二进制指令:
并会形成符号表,这个符号表就是一个大致为符号+其地址的列表。
在汇编结束后,会生成一个.o文件。
链接
链接时会将每一个编译过的源文件链接起来,形成可执行文件(.exe)
在这其中,会进行符号表的合并与符号表的重定位。
比如在一个源文件中通过extern声明了另一个源文件中的函数。在分别将这两个源文件进行编译时,每一个源文件的符号表中都会有这个函数的函数名以及其地址,但是声明函数的那个源文件中的地址是“假的”。符号表的合并与重定位就是使不同源文件中的符号汇总,重定位重复的符号。
当然,如果你在代码中使用了没有声明或没有定义的符号,在链接时就会被发现,报错或报警告。
执行部分
在翻译完成之后,就是执行了:
执行一个可执行程序时,运行环境会将这个可执行程序写入内存,然后找到main函数,开始执行代码,最后终止程序。
当然,这里只是简单介绍程序运行之前的操作,真正的翻译与执行过程是要比我所说的要复杂许多的。
但至少我们对#define定义的标识符常量与宏的阶段有了简单的认识。
接下来就开始相信关于#define的知识:
#define定义标识符
在使用#define定义标识符时,语法为
#define name stuff
在预编译时,代码中的所有name会被替换为stuff。
我们可以使用#define定义常量,也可以用来定义一些关键字:
#define MAX 10 //定义标识符常量
#define CASE: break;case: //定义关键字
#define INT int*
定义常量的情况想必大家都有所了解,需要注意的是:
#define定义常量在替换时,是直接替换的,不会加上括号:
#include<stdio.h>
#define MAX 2+5
int main()
{
printf("%d\n", MAX * 5);
return 0;
}
这段代码的结果是27,而不是35的原因是替换后的代码为2+5*5,而不是(2+5)*5。
同样的我们在定义标识符的时候也是直接替换的,举个栗子:
#include<stdio.h>
#define CASE break;case
int main()
{
int input = 0;
scanf("%d", &input);
switch (input)
{
case 1:
printf("1");
CASE 2:
printf("2");
CASE 3:
printf("3");
break;
}
return 0;
}
在这段代码中,我们使用CASE替换了代码中的break;case。使得在编写代码时可以更简便,当然,这样也导致了C代码可读性的下降。
最后需要注意的就是:
不建议在#define定义的标识符后加";“。由于是直接替换,它会将”;"与需要替换的符号或常量一起替换过去,从而产生一些难以预料的问题。
#define定义宏
宏的简介
#define机制包括了一个规定,允许把参数替换到文本中,这种实现称为宏。
语法为:
#define name(parament_list) stuff
//parament_list是由逗号给开的符号表
例如:
#include<stdio.h>
#define SQUARE(n) n*n
int main()
{
int n = 0;
scanf("%d", &n);
printf("%d\n", SQUARE(n));
return 0;
}
宏的使用
在使用宏时,有一些需要注意的点:
第一点需要强调的仍然是直接替换, 不会给每一个参数添加括号,也不会给整个宏添加括号:
#include<stdio.h>
#define ADD(a,b) a+b
#define MUL(a,b) a*b
int main()
{
printf("%d\n", 4*ADD(2,3));
printf("%d\n", MUL(1 + 5, 2 + 3));
return 0;
}
这段代码中:
宏ADD替换的结果为4*2+3,而不是4*(2+3)。
宏MUL替换后的结果为1+5*2+3,而不是(1+5)*(2+3)。
解决这个问题的办法就是:不要吝啬括号,将这两个宏定义为如下即可:
#define ADD(a,b) ((a)+(b))
#define MUL(a,b) ((a)*(b))
第二点是:
在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号,如果有,他们首先被替换。
第三点是:
在调用宏时,一些包括自增自减的参数是有副作用的:
#include<stdio.h>
#define MAX(a,b) ((a)>(b)?(a):(b))
int main()
{
int a = 3;
int b = 6;
printf("%d\n", MAX(a++, b++));
printf("%d\n", b);
return 0;
}
在这段代码中,宏MAX的作用是比较两个参数的大小。
但当参数自增时,替换后就是:(a++)>(b++)?(a++):(b++)。即在比较的过程中,b自增了两次。所以在结束比较厚打印b时的结果为8。
宏与函数的对比
到这里不难发现,宏与函数是有着相似之处的:
它们都有name、参数与实现。
但是区别也是很大的:
1、因为用于调用函数和从函数返回的代码要比执行一个简单的计算所需的代码多,所以宏在调用时的效率是比函数高很多的(小型的计算);
2、函数的参数是有类型的,也存在类型检查。但宏的参数是没有类型与类型检查的(不够严谨);
3、函数可以递归,而宏不可以递归;
4、函数方便调试,而宏是不方便调试的;
5、对于参数而言,宏的参数是直接替换的,所以会有一些参数具有副作用。而函数的参数是临时拷贝的,没有副作用的情况;
总结
到这里,关于#define的知识就介绍完了。
如果大家认为我对某一部分没有介绍清楚或者某一部分出了问题,欢迎大家在评论区提出
如果本文对你有帮助,希望一键三连哦
希望与大家共同进步哦