目录
一、什么是宏以及宏的声明方式
1.宏常量:
2.宏函数:
二、宏的替换原则
三、宏设计的易犯错误
ERROR1:尾部加分号(当然有些特定需要加了分号,这里说明一般情况)
ERROR2:宏函数定义时,没有保证其参数在表达式中的独立性,以及表达式的独立性
ERROR3:带副作用的宏参数
四、#,##
1.使用# ,把一个宏参数变成对应的字符串
2.使用##,允许宏定义从分离的文本片段创建标识符
五、宏函数特性分析
前言:
宏,每个C语言学习者绕不开的话题,其使用简便又十分易错,稍不注意甚至处处小心仍然容易写出BUG。那么宏是什么,应该如何正确使用(减小使用错误概率)?今天让我们来一探究竟。
一、什么是宏以及宏的声明方式
#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)。
宏的声明方式:
1.宏常量:
#define 宏常量名 宏常量值 ------- 例:#define MAX 1000
说明:
a.宏常量名和普通常量名没什么区别,不过习惯上使用大写字母表示
b.语句不同成分之间用空格隔开,末尾不需要加分号
2.宏函数:
#define 宏函数名(参数列表) 宏函数表达式 ------ 例: #define ADD ( x, y ) ( ( x ) + ( y ) )
ps.上述函数表达式看起来很别扭,但是这是比较安全的写法,下文会进行解释。
说明:
a.宏函数名和普通变量名没什么区别,不过习惯上使用大写字母表示
b.语句不同成分之间用空格隔开,末尾不需要加分号
注意:
参数列表的左括号必须与宏函数名紧邻。
如果两者之间有任何空白存在,参数列表就会被解释为宏函数体的一部分。
二、宏的替换原则
我们在设计宏和使用宏时,往往因为对宏的替换原则不够了解而出现错误,而宏的替换总结起来就是两个字——“生硬”。
宏是在编译的阶段进行的替换,编译器会对代码进行检查,对使用了宏常量名和宏函数名的代码段将对应的常量值或函数表达式直接在原位置处嵌入式替换。
例:
#define MAX 1000
#define ADD( x, y) ( ( x ) + ( y ) )int main()
{
// 替换前printf("%d",MAX);printf("%d",ADD( 1, 2));
// 替换后printf("%d",1000);printf("%d",( ( 1 ) + ( 2 ) ));return 0;
}
这就如同我们进行覆盖式 ctrl + c/v 一样,如此这般确实显得编译器很生硬,但这也是没有办法,毕竟语法规定,同时编译器也不能自作主张的替换,这就要求我们编程者根据特性来好好设计宏了 。
三、宏设计的易犯错误
了解了宏的替换原则,想必对于设计宏犯错误的原因也大概知道了,就是由于生硬的替换而导致逻辑或者语义甚至语法的错误出现。
ERROR1:尾部加分号(当然有些特定需要加了分号,这里说明一般情况)
宏替换后, max = MAX;就变成 max = 1000;; ,这也就导致了在if else语句之间出现了一个空语句,else找不到if而落单了,这很明显是有问题的。
宏替换后,printf("%d",MAX) ;就变成了 printf("%d",1000;) ; ,这一个明眼人都能看出明显问题。
ERROR2:宏函数定义时,没有保证其参数在表达式中的独立性,以及表达式的独立性
这里我们设计一下加法宏函数
这样的结果会是多少呢?想必根据上文提到的替换规则,应该很清楚,发生替换后,变为
5 * 2 + 3 = 13
回答正确!
那如果这样呢?还是13吗?
这时,就昏头了,是先计算宏参数值呢?还是先替换?测试瞧瞧
这是 5 * 1 + 1 + 2 + 1的结果,看来是先直接替换无疑了,这也很体现宏的“生硬”
宏函是仿照函数的,应该和函数基本性质相同才对,而对于函数,函数参数传参时应该先计算后传入,对于函数表达式更是一个整体,全算完了才进行外部动作。
所以对于宏函数的函数表达式,我们应该用括号将参数以及函数表达式的独立性体现出来,于是我们这样定义: #define ADD(x,y) (( x )+( y ))
ERROR3:带副作用的宏参数
当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。
这里的副作用通常是指对自身进行改变的作用。如:x + 1 与 x++
我们进行替换
结果
在进行判断后,x,y自增,选择结束后y又进行自增,所以这即便给每个参数都确保的独立性,也无法保证其正确。
四、#,##
1.使用# ,把一个宏参数变成对应的字符串
#对于宏,有这么一个技巧,可以把一个宏参数变成对应的字符串:
PRINT("%d", i + 3) 变为 printf("the value of "i + 3" is"“%d” "\n", 1 + 3)
ps.这里还涉及printf字符串拼接的特性printf("haha""nihao") -> printf("hahanihao")
2.使用##,允许宏定义从分离的文本片段创建标识符
这样的连接必须产生一个合法的标识符,否则其结果就是未定义的。
五、宏函数特性分析
通过上文,应该不会有人还觉得宏函数那么好干嘛还要使用普通函数了吧。
a.调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。(创建函数栈桢有内存消耗)所以宏比函数在程序的规模和速度方面更胜一筹。
b.函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之这个宏可以适用于整形、长整型、浮点型等可以用于来比较的类型。宏是类型无关的。但也意味着宏没有类型检查,这在一些时候是不安全的体现。
c. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度,增加编译工作量。
d.宏在编译阶段就进行替换,是没法调试的。
4. 宏可能会带来运算符优先级的问题,导致程容易出现错。
综上宏函数适合短小计算,可以替代小型函数,但大型函数也是无能为力,对于类型检查有要求的,可以使用enum枚举常量来替代。