1、为什么要有宏定义?
代码中某个特定数值需要参与运算,而且该数值作用于多个地方,当需要对该数值进行修改时,希望只改动一个地方就能实现该数值的全部更新;即便某个数值只用到一次,当修改时也会面临搜索阅读大量代码、数值含义不明晰的问题;某些“操作块”封装成函数时,调用函数开销(保存上下文环境、参数调用、堆栈分配等)太大影响效率,不封装则需要多次输入相同的“操作块”内容,并且修改麻烦容易出现不一致的问题,这时就需要把“操作块”定义成宏,该方案本质是“空间换时间”。
总之,宏的出现时为了:提供代码可读性、增加代码书写效率和提高系统运行效率。
2、宏的产生背景是什么?
宏的本质就是“无条件替换”,而且宏只对程序的文本起作用,它眼中的世界只有文本(字符),没有逻辑运算。记得学习C时,老师告我们:“每条语句后边都要加分号“;”,但是宏定义不能加“,问原因竟然是:”就是不能,记住就行!“。现在知道原因了:如果加了分号,分号就成为了宏定义的一部分,在进行”宏展开“时直接截断语句,当然会出问题。
确切的说, 程序源代码(*.c文件)是不能被计算机直接执行的,编译器需要对程序代码进行“一系列处理”,转换成计算机能读懂的二进制文件(*.bin)。这涉及到编译原理的知识了(如果有机会,我会就编译原理进行讲解),一系列处理包括:预处理(Preprocessing)、编译(Compilation)、汇编(Assembly)、链接(Linking)。“宏展开”过程就发生在“预处理”阶段,编译器负责用实际数据替换掉宏名,所以经过了“预处理”,函数代码中就已经没有所谓宏的概念了。
下面我将做个试验,进行验证:
(1)在Fedora环境下,键入“vi test_macro.c”输入下面的代码并保存
(2)退出vi环境,键入“gcc -E test_macro.c”执行,效果如下
(3)试验结果分析:
"gcc -E test_macro.c" 命令就是告诉编译器只对代码进行预处理,不再进行后续工作直接输出。可以看到,我们定义的宏PI和R都已经不存在了,而且都被替换成了它们各自的常量值。main函数上面的那些函数声明,是stdio.h文件的展开,预处理阶段不只进行了宏展开,还对包含的头文件进行了展开,这里不再细说。
下一节,我们将结合试验数据,对宏使用过程中的一些注意事项进行分析。
1、如何区分宏定义中的“宏名称”和“宏字符串”?对于带参数的宏又该注意什么?
在宏定义中,“宏名称”和“宏字符串”是通过“空格”来区分的。编译器在处理时宏定义时,首先从“#define”后第一个空格开始读取字符串,直到遇见下一个空格为止,两个空格之间的字符串为“宏名称”,确定好“宏名称”之后,本行的所有其他字符串都为“宏字符串”。图示:#define + N个空格(1 < N) + 宏名称(中间没有空格) + N个空格(1 < N) + 宏字符串(直到本行结束)。这里讲到的都是一行之内的宏定义,如果跨越多行则用“\”字符进行“续行”,本质上可以当做一行来对待。
对于“带参数宏”,宏名称和“( )”之间不能有空格,否则就变成了“无参数宏”(根据上面的原则)。而且当“无参数宏”和“带参数宏”的名字相同时,“无参数宏”会屏蔽掉“带参数宏”,即使以“带参数宏”的方式调用,也行不通。
试验内容及结果:
实验分析:
可以看到“#define PI 3.1415”和“#define P I 689”分别是两个不同的宏定义“PI”和“P”;"G(4)"被“(X) (2*X) (4)”替换掉;当调用“F(4)”时,系统并没有替换成“2*4”,而是替换成了“123(4)”,说明“#define F 123”完全屏蔽掉了“#define F(X) (2*X)”,当注释掉“#define F 123”后"#define F(X) (2*X)"可以正常工作。因此,在进行宏定义时,要密切关注空格的影响,并且“带参数宏”和“无参数宏”的名称一定不能相同,否则会出现混乱。但是,在宏调用时空格并不影响效果,例如F(3)和F (3)效果相同(F(X)是带参数宏)。
2、宏和函数在使用方式和效果上有何异同?
在宏定义时,要善于利用括号对变量进行封装,把每个参数都括起来,预防出现与优先级有关的问题;同时整个结果表达式也应该括起来,以防止当宏用于一个更复杂的表达式时可能出现问题。尽量提高宏的可靠性,。例如:“#define ABS(x) (((x) > 0) ? (x) : (-x))”的可靠性要远远优于“#define ABS(x) x > 0 ? x : -x”,可以以ABS(a-b)来进行试验。
在宏调用时,如果有自增(++)或自减(--)操作符,一定要注意很可能会产生副作用。因为宏在替换时,如果变量出现了多次,就相当于自增或自减操作进了多次,这个跟函数调用是完全不同的,函数调用中形参会复制实参的数值,并对形参进行操作并不会影响实参,而宏调用就是直接多次修改实参。例如:a = 5; "ABS(a++) “展开后就变成“(((a++) > 0) ? (a++) : (-a++))”,操作完成后”a = 7“而不是”a = 6“;当写成函数就完全不用担心这个问题。
如果在宏调用时,进行了了多层嵌套调用,则宏展开后会产生非常庞大的表达式,而且相当复杂;函数调用则不会出现这种情况。
3、宏和类型定义typedef的区别
由于宏的本质就是替换,所以可以对变量类型进行一层封装,利用该封装做变量定义,这样做的好处是增加可移植性,当修改时只需要改动宏定义即可。例如:
- #define MY_TYPE uint_8
- MY_TYPE a;
- MY_TYPE b,c,d;
- #define MY_TYPE uint_8
- MY_TYPE a;
- MY_TYPE b,c,d;
- #define MY_TYPE1 uint_8 *
- typedef uint_8 * MY_TYPE2
- MY_TYPE1 a,b;
- MY_TYPE2 c,d;
- #define MY_TYPE1 uint_8 *
- typedef uint_8 * MY_TYPE2
- MY_TYPE1 a,b;
- MY_TYPE2 c,d;
从概念上看,MY_TYPE1 和 MY_TYPE2 完全相同,都是指向uint_8的指针,但是当我们声明多个变量时,就出现问题了。它们分别被扩展成了:
- uint_8 *a,b;
- MY_TYPE2 c,d; //因为MY_TYPE2已经是一种类型了
- uint_8 *a,b;
- MY_TYPE2 c,d; //因为MY_TYPE2已经是一种类型了
在内存中, char 型是以一个字节八位二进制数保存的,int型是4个字节,32位二进制数保存的,当(int)char 时,赋予int型在32位的最后八位char 的二进制数,剩余的24位不做处理既为0
当(char)int 时,由于char 型只能是八位,而int型的32位放不下便会截断,丢失精度。
比如 int a=542;char ch=(char)a;printf("%d",ch);输出为30;
分析: 542的二进制为 0000 0000 0000 0000 0000 0010 0001 1110当放在char型的八位中时,只有最后八位能放下,其余的都会丢失,所以变成了 00011110它转换为十进制便是30;
如果int a =45;char ch=(char)a;printf("%d",ch);
输出依然是45,没有丢失精度因为45的
二进制为 0000 0000 0000 0000 0000 0000 0000 00101101最后八位能完全放在char 中,高位都是0,所以不丢失精度。
也可以这样说,char 型的十进制范围为 -128~127 所以只要在此范围内的int 型,便能不丢失精度的转换
而(int)char 是绝对没有问题的。
1661

被折叠的 条评论
为什么被折叠?



