2.3 #define 替换原则
在程序中扩展#define定义符号和宏时,需要涉及几个步骤。
1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
注意:
1. 宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
2.3.1 #的使用
正常情况下字符串中的是不会发生替换的,但是#可以使它们发生替换。
在这里使用#使得字符串中的N部分变成了N+1,发生替换。
没错#的作用就是把一个宏参数变成对应的字符串.
2.3.2 ##的使用
##可以把位于它两边的符号合并成一个符号,它允许宏定义从分离的文本片段创建标志符。
2.4 带副作用的宏参数
当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。
例如:
x+1;//不带副作用 x++;//带有副作用
这也是比较好理解的,毕竟本身后者表达式带有副作用,宏去替换的话可能放大这个作用,我们可以举个例子:
#define MAX(a, b) ( (a) > (b) ? (a) : (b) ) int main() { int x = 5; int y = 8; int 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 MAX( a, b ) ( (a)> (b)? (a) : (b))
我们已经有了函数那么,为什么不用函数来完成这个任务呢?
原因有二:
1.用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多,所有宏比函数在程序的规模和速度方面更胜一筹。
2.函数的参数必须声明为特定的类型,所以函数只能在类型合适的表达式上使用。反之这个宏可以适用于整型,长整型,浮点型等可以用来比较的类型--宏是类型无关的,所以宏的参数可以出现类型,但是函数做不到。
#define MALLOC(num, type)\ (type *)malloc(num * sizeof(type)) ... //使用 MALLOC(10, int);//类型作为参数 //预处理器替换之后: (int *)malloc(10 * sizeof(int));
这是宏的优点,当然宏也有它的缺点:
1、每次使用宏的时候,一份宏定义的代码将插入到程序中,除非宏比较短,否则可能大幅度增加程序的长度。
2、宏是无法调试的。
3、宏由于是类型无关的,也就不够严谨。
4、宏可能会带来运算符优先级的问题,导致程序容易出错。
所以简单来说:比较简单的函数,可以使用宏,但要注意多加括号,注意对于递归是不能使用宏的。
图示比较函数和宏:
还有一点细节需要注意:对于命名来说,一般来讲函数和宏的使用语法很相似,所以我们平时区分它们的一个习惯:
1、宏名全部大写。
2、函数名不要全部大写。
四、#undef
这条指令用于移除一个宏定义。
#undef NAME //如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。
#define M 100 int main() { printf("%d\n", M); #undef M printf("%d\n", M); return 0; }
五、条件编译
在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。
因为我们有条件编译指令。
比如说:
调试性的代码,删除可惜,保留又碍事,所以我们选择性的编译
#include<stdio.h> #define __DEBUG__ int main() { int i = 0; int arr[10] = { 0 }; for (i = 0; i < 10; i++) { arr[i] = i; #ifdef __DEBUG__ printf("%d\n", arr[i]);//为了观察数组是否复制成功 #endif // __DEBUG__ //如果DEBUG定义了,就打印 //满足条件就编译,不满足就不编译 } return 0; }
常见的条件编译指令:
//1. #if 常量表达式 //#endif //常量表达式由处理器求值 #if 0 int main() { printf("hehe\n"); //如果为假,预处理阶段就删除了 //拿来注释,就很离谱 return 0; } #endif // __DEBUG__ 表示它们是一对 //2. 多分支的条件编译 #if 常量表达式 //... #elif 常量表达式 //... #else //... #endif //3.判断是否被定义 #if defined(symbol) #ifdef symbol #if !defined(symbol)//不定义参与编译 #ifndef symbol #define MAX 100 int main() { #if defined(MAX) printf("max\n"); #endif return 0; } 也可以这样写 #ifdef MAX //4.嵌套指令 #if defined (OS UNIX) #define M 3 int main() { #if M<5 printf("hehe\n"); #elif M==5 printf("haha\n"); #else printf("heihei\n"); #endif //满足条件就编译(留下来),不满足条件就在预处理阶段被删除 return 0; } 4.嵌套指令 #if defined(OS_UNIX) #ifdef OPTION1 unix_version_option1(); #endif #ifdef OPTION2 unix_version_option2(); #endif #elif defined(OS_MSDOS) #ifdef OPTION2 msdos_version_option2(); #endif #endif
六、文件包含
我们都知道#include指令可以使另外一个文件被编译。就像它实际出现于#include指令的地方一样
这种替换方式很简单:
预处理器先删除这条指令,并用包含文件的内容替换。
这样一个源文件被包含10次,那就实际被编译10次。
头文件包含多次,真的会多次被拷贝到头文件。
6.1 头文件被包含的方式
① 本地文件包含
#include ”filename “
双引号""查找策略:
1.先去代码所在路径下查找
2.如果找不到,再去库目录下查找
如果还找不到,就提示编译错误。所以说库文件比如说 #include "stdio.h"
也是可以的,只不过效率会比较低。
②库文件包含
#include <filename.h>
查找头文件直接去标准路径下查找,如果找不到就提示错误。