1. 宏定义
1.1 宏定义的用途
(1)减少重复劳动
比如,程序中有很多地方都要用到100这个数,当需要将100修改为200时,就需要到每个引用100的地方将100改为200,
这就是重复劳动。此时最好就将100定义为宏,
#define NUM 100,
如果需要将100修改为200时,只要修改NUM即可,在预处理时NUM自动就会被替换为200。
(2)方便阅读
我们在定义每个宏时,宏名都是有一定含义的,而不是随便定义的,有意义的宏名能够帮助我们阅读和理解代码,
比如:
#define PI 3.14
一看这个宏名你就知道这是圆周率,如果在程序中直接写3.14的话,很难理解这个数字的含义。
(3)简化复杂表达式
简化使用调用。
#define AREA(a,b) (((a)+(b))*((a)-(b)))/a*b
在代码中直接调用AREA带参宏,显然简化了代码,向这类的简化复杂表达式的宏,在复杂C源码中非常多,由于这类宏的宏体复杂,往往也成为了很多同学阅读复杂C源码的障碍之一。
(4)与条件编译配合使用
#define ARM #ifdef ARM ... #endif
1.2 宏的种类
宏有两种,一种是有宏体宏,另一种是无宏体宏。
· 无宏体宏:
#define X86
· 有宏体宏:
#define NUM 100,#define AREA(r) (r)*(r)*3.14
1.2.1 无宏体宏
(1)什么是无宏体宏
只有宏名、没有宏体。
(2)定义形式
#define 宏名
(3)举例
#define X86
预编译完后,由于这个宏没有宏体,所以宏直接替换为空,空就是啥也没有。
(4)这种宏有作用吗
有作用,后面讲到条件编译时再介绍。
1.2.2 有宏体宏
有宏体宏分为两种,一种是无参宏,另一种是有参宏。
无参宏:没有参数,
#define NUM 100
有参宏:有参数,
#define AREA(r) (r)*(r)*3.14
(1)无参宏
1)定义形式
#define 宏名 宏体
3)举例
#define YES 1 #define NO 0 #define PI 3.1415926 #define OUT printf(“Hello,World”);
预处理后,宏名会被替换为宏体。
(2)带参宏
1)定义形式
#define 宏名(参数表) 宏体
2)举例
#define S(a,b) a*b*10 int main(void) { int va; va = S(3,2); //3对应a,2对应b printf("va = %d\n", va); return 0; }
预编译处理时,将宏体中的a和b,使用参数中的3和2来替换。
va = S(a, b) ——————> va = 3*2*10
3)带参宏需要注意之处
1)宏名和参数列表之间不能有空格
#define S (a,b) a*b*10
由于S和(a, b)之间有空格,宏名变为了S,宏体变为了(a,b) a*b*10,含义发生了变化。
2)写带参宏的时,不要吝啬括号
#define S(a,b) a*b*10
其实这个带参宏是有缺点的,如果参数写成如下形式的话,替换后结果可能就完全背离了你的本意。
a b
S(x+1, y+2) ——————————————> x+1*y+2*10
对于预编译器来说,它再处理宏定义时,它并不知道你的使用意图是什么,它只会忠实的进行替换工作,但是替换之后是否能够达到你要的效果,这个就不一定了。
怎么解决?
为了避免这种情况,大家在定义带参宏时不要吝啬括号。
#define S(a,b) ((a)*(b)*10) //为了保险起见,对整个宏体也要加()。
S(x+1, y+2) ——————————————> ((x+1)*(y+2)*10)
疑问:为什么要对整个宏体加括号?
#define ETH_MMC_BASE (ETH_BASE + 0x0100) #define ETH_PTP_BASE (ETH_BASE + 0x0700) #define ETH_DMA_BASE (ETH_BASE + 0x1000) *ETH_MMC_BASE = 0x67667732;
如果不加括号的话,*ETH_MMC_BASE = 0x67667732;就变为了*ETH_BASE + 0x0100 = 0x67667732。
所以,凡事宏体是一个表达式的情况,我们都要求对整个宏体加(),以保证宏展开一定不会出错。
3)带参宏 与 函数
这两个玩意儿长得很像,但实际上是两个完全不同的东西。
1)例子
#include <stdio.h> #define S(a,b) a*b*10 void s(int a, int b) { return a*b*10; } int main(void) { int va1, va2; va1 = S(3, 2); //引用带参宏 va2 = s(3, 2); //调用函数 printf("va1 = %d, va2 = %d\n", va1, va2); return 0; }
仅仅从调用来看,这两个东西确实长得很像,如果将宏也定义为小写的话,仅看调用的话,很难看出这个到底谁是函数谁宏定义。
为了能够让大家快速的区分带参宏和函数,大家在定义宏的时候,宏名一定要大写,否则在阅读代码时,很容易与函数搞混,非常不利于代码的阅读和理解。
2)二者的区别
二者是有着本质区别的:
(a)带参宏
· 处理阶段:预编译
宏只是一个供我们程序员识别的一个符号,一旦预编译之后带参宏就会消失了,取而代之的是宏体。
· 参数列表
带参宏的形参是没有类型的,我们使用int 、float等类型只有一个目的,就是使用类型来开辟一个变量空间,变量空间的字节数和存储格式是由类型来决定的,所以定义变量时必须要有类型说明。
而带参宏的参数仅仅只起到替换说明的作用,不需要开辟空间来存放实参的值,既然不需要开辟空间,那就不需要类型的说明。
(b)函数
· 处理阶段:由编译、汇编、链接阶段处理
在“预编译阶段”是不会处理函数这个东西的,在预编译前后,函数没有任何变化。
- 编译:将c形式的函数编译为汇编形式的函数。
- 汇编:将汇编形式的函数,转为二进制指令的函数
- 链接:进行符号统一和重定位
· 函数是一个独立体,有调用的过程
运行函数时涉及调用的过程
- 调用时:从当前函数跳转到被调用的函数,开辟形参和自动局部变量时,涉及压栈操作。
- 调用结束:返回到调用函数,释放函数的形参和自动局部变量的空间时,涉及弹栈操作
· 函数的参数列表
函数的形参是需要被开辟空间的,所以必须要要有类型说明。