① 宏定义(#define)是什么
#define可以将一对文本进行替换,在编译器读到需要被替换的文本的时候,会将这些文本全部替换成我们给定的文本,这样说还是有点抽象,我们拿一段示例在演示说明
#define A 100 #define B 200 int main() { int C = 0; C = A + B; printf("C = %d\n", C); return 0; }
在这里,我们将 “A”和“B” 分别使用宏定义,定义为 100和200,在以后的语句中,一旦编译器读取到 “A”和“B” 就会直接将该位置的 “A”和“B” 替换为对应的数字文本,因此 “C” 的最后值为 300
例外
但需要注意,这里的替换文本是不包含在字符串内部的,也就是说,我们要替换的文本必须的一段完整的,他不能是一段字符串的一部分,我们还是用示例来说明一下
#define A 100 #define B 200 int main() { int C = 0; C = A + B; printf("C = %d\n", C); int ABC = 0; char arr[] = "ABD"; printf("ABC=%d\n", ABC); printf("arr=%s\n", arr); return 0; }
我们在刚才的代码后再加一段,定义一个整形变量 ABC,定义一个字符数组 arr (内容是“ABC”) ,我们试着运行一下会发现,对于一个完整的变量或者字符串,假如他的内部的一部分字符是我们宏定义的对象的话,他是不会进行文本替换操作的
正如这里的 ABC 变量,他并没有被替换为 “100200C”,后面的字符数组也是,它并没有被替换为 “100200C”
② 常用宏定义用法
宏定义的内容是多种多样的,可以是数字可以是字符,可以是变量,可以是遇见,也可以是函数,以下给出一些宏定义使用方法和示例
#define MAX 1000 // MAX 的大小定义为 1000 #define reg register //为 register这个关键字,创建一个简短的名字 #define do_forever for(;;) //用更形象的符号来替换一种实现 #define CASE break;case //在写case语句的时候自动把 break写上
- 前面俩种就是很简单的文本替换,在前文中已经进行了讲解,这里就不再继续赘述
- 对于第三种,一般在实际中没有什么意义,当编译器读取到 do_forever 的时候,就直接替换为 for(;;) 相当于写了个一直不会停止的 for 循环,程序就会进入死循环
我们重点来看最后一个,我们先写出一个普通的 switch语句:
int main() { int i = 0; scanf("%d", &i); switch (i) { case 1: printf("%d\n", i); break; case 2: printf("%d\n", i); break; case 3: printf("%d\n", i); break; case 4: printf("%d\n", i); break; } return 0; }
在加入宏定义后
#define CASE break;case //在写case语句的时候自动把 break写上
int main() { int i = 0; scanf("%d", &i); switch (i) { case 1: printf("%d\n", i); CASE 2: printf("%d\n", i); CASE 3: printf("%d\n", i); CASE 4: printf("%d\n", i); } return 0; }
每一个大写的 CASE 语句都相当于为上一个 case 自动补写了个 break,因此整个程序除了第一个 case 以外其余的全部用宏定义进行替换,我们就可以省去写 break 的步骤,但是达到同样的目的
③ 续行操作
我们知道,我们写的宏定义(#define)是一行一行的,前面是宏定义,中间是被替换的文本,最后是替换后的文本,但假如我们写了个宏定义,非常的长,一行根本写不下怎么办呢,这就需要使用续行操作符了
// 如果定义的 stuff过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符)。 #define DEBUG_PRINT printf("file:%s\tline:%d\t \ date:%s\ttime:%s\n" ,\ __FILE__,__LINE__ , \ __DATE__,__TIME__ )
④ #define 定义宏
#define 机制包括了一个规定,允许吧参数替换到文本中,这种实现通常被称为宏(macro)和定义宏(define macro),一下是宏的申明方式:
#define name(parament-list) stuff
其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在 stuff 中
注意:
- 参数列表的左括号必须与name紧邻
- 如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分
宏内部的隐患
以下面的代码举例:
#define SQUARE( x ) x * x
#define SQUARE( x ) x * x
这个宏接收一个参数 x,假如代码块中写有 SQUARE ( 5 ); 的话,当编译器读到的时候,预处理器就会替换为 5*5 , 这给我们的感觉就和函数一样,你给定一个值,他给你返回一个值 ,但是这样的操作也会带来部分的问题,我们给定以下代码,大家可以思考判断会输出什么
int a = 5; printf("%d\n" ,SQUARE( a + 1) );
你觉得会是何种输出结果?
- A. 36
- B. 11
结果非常的出人意料啊,按道理来说我们计算 5+1 的平方应该是 36 啊,为什么会输出 11 呢?
我们还是从宏定义的本质上来看,宏定义是替换文本,那我们试着替换一下,替换文本时,参数 x 被替换成 a + 1,所以这条语句实际上变成了:
printf ("%d\n",a + 1 * a + 1 )
因为乘号的优先级更大,所以运行的结果就是 5+1*5+1 ,也就是 11
其实要避免这样的问题也很简单,我们在宏定义内容中加上括号就好了
#define SQUARE(x) (x) * (x)
我们再试着运行一下:
宏外部的隐患
刚才我们出现的问题是来自于宏定义内部的问题,那如果是外部呢?我们还是给出一个宏定义:
#define DOUBLE(x) (x) + (x)
吸取了刚才的教训,我们提前在内容中加上了括号,我们试着运行下面的代码,看看会输出什么样的结果
int a = 5; printf("%d\n" ,10 * DOUBLE(a));
大家可以思考思考会是何种输出:
- A. 100
- B. 55
我们试着运行一下,结果还是和我们预期的不同:
对于这样的情况,我们给整个宏定义一个括号就可以解决问题了
#define DOUBLE(x) ((x) + (x))
⑤ #define 替换规则
通过以上的学习了解,我们可以大概总结如下:
在程序中扩展 #define 定义符号和宏时,需要涉及几个步骤
- 在调用宏时,首先对参数进行检查,看看是否包含任何由 #define 定义的符号。如果是,它们首先被替换
- 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换
- 最后,再次对结果文件进行扫描,看看它是否包含任何由 #define 定义的符号。如果是,就重复上述处理过程
另外需注意:
- 宏参数和 #define 定义中可以出现其他 #define 定义的符号。但是对于宏,不能出现递归
- 当预处理器搜索 #define 定义的符号的时候,字符串常量的内容并不被搜索
⑥ 带副作用的宏参数
当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能 出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果
x+1;//不带副作用 x++;//带有副作用
MAX 宏可以证明具有副作用的参数所引起的问题
#define MAX(a, b) ( (a) > (b) ? (a) : (b) ) x = 5; y = 8; z = MAX(x++, y++); printf("x=%d y=%d z=%d\n", x, y, z);//输出的结果是什么?
我们尝试替换:
z = ( (x++) > (y++) ? (x++) : (y++));
所以输出的结果是:
x=6 y=10 z=9
⑦ 宏与函数的相似
经过上述的讲解后,笔者相信大家对宏定义应该都有了很深刻的理解,接下里我们来探讨探讨宏定义和函数的关系,如下面的代码,我们能给宏传参,能输出结果,我们可以很明确的感受到一种函数的感觉
#define PRINT(FORMAT, VALUE)\ printf("the value is "FORMAT"\n", VALUE);
PRINT("%d", 10);
# 的使用
我们这里还有更高级的用法:使用 “#” 可以将宏的参数变为对应的字符串
#define PRINT(FORMAT, VALUE)\ printf("the value of " #VALUE " is "FORMAT "\n", VALUE);
int i = 10; PRINT("%d", i+3);
## 的使用
## 可以把位于它两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符
#define ADD_TO_SUM(num, value)\ sum##num += value;
ADD_TO_SUM(5, 10);//作用是:给sum5增加10.
注意:这样的连接必须产生一个合法的标识符,否则其结果就是未定义的
⑧ 宏与函数的区别
宏通常被应用于执行简单的运算,比如在两个数中找出较大的一个:
#define MAX(a, b) ((a)>(b)?(a):(b))
#define MAX(a, b) ((a)>(b)?(a):(b))
那为什么不用函数来完成这个任务?
原因如下:
- 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多, 所以宏比函数在程序的规模和速度方面更胜一筹
- 更为重要的是函数的参数必须声明为特定的类型, 所以函数只能在类型合适的表达式上使用,反之这个宏怎可以适用于整形、长整型、浮点型等可以用于>来比较的类型,宏是类型无关的
宏的缺点:
- 每次使用宏的时候,一份宏定义的代码将插入到程序中,除非宏比较短,否则可能大幅度增加程序 的长度
- 宏是没法调试的
- 宏由于类型无关,也就不够严谨
- 宏可能会带来运算符优先级的问题,导致程容易出现错
总结
属性 |
#define定义宏 |
函数 |
代码长度 |
每次使用时,宏代码都会被插入到程序中,除了非常小的宏之外,程序的长度会大幅增长 |
函数代码只出现于一个地方:每次使用这个函数时,都调用那个地方的同一份代码 |
执行速度 |
更快 |
存在函数的调用和返回的额外开销,所以相对慢一些 |
操作符优先级 |
宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则临近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多些括号 |
函数参数只在函数调用的时候求值一次,它的结果值传递给函数,表达式的求值结果更容预测 |
带有副作用的参数 |
参数可能被替换到宏体中的多个位置,所以带有副作用的参数求值可能会产生不可预料的结果 |
函参数值在传参的时候求值一次,结果更容易控制 |
参数类型 |
宏的参数与类型无关,只要对参数的操作是合法的,它就可以使用于任何参数类型 |
函数的参数与类型有关的,如果参数的类型不同,就需要不同的函数,即使他们执行的任务是不同的 |
调试 |
宏是不方便调试的 |
函数时可以逐语句调试的 |
递归 |
宏是不能递归的 |
函数是可以递归的 |