2.2.5 带副作用的宏参数
副作用就是后遗症的意思,
x+1; 不带副作用 x++; 带有副作用
举个例子:
#define MAX(a, b) ( (a) > (b) ? (a) : (b) ) int main() { x = 2; y = 5; printf("%d\n", MAX(a++, b++)); printf("%d\n", a); printf("%d\n", b); }
请问上面的这段代码输出结果是什么?
输出结果是:6,3,7
原因是,宏是被替换的,首先x是2,y是5,在使用宏时,参数是a++和b++,然后进行替换,替换结果是:
printf("%d\n", ((a++) > (b++) ? (a++) : (b++)));
所以在比较时,先使用,后++,2和5比较完,再各自++,此时2小于5,执行b++,此时b已经是6了,先使用后++,所以打印第一个结果是6,对于a,a只++一次,结果是3,
最后再打印b出来时,b已经++完成了,所以b打印出来是7.
总结:当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能
出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。
2.2.6 宏和函数对比
宏是经常被用来执行计算量较小的计算,如比较大小,那为什么不使用函数来比较呢?
1.我们知道,函数在调用的时候会有返回的开销,反观宏,则没有类似的问题。
以比较大小为例:
#define MAX(a,b) ((a)>(b)?(a):(b)) float MAX2(float c, float d) { return c > d ? c : d; } int main() { int a = 2; int b = 5; float c = 3.0f; float d = 4.0f; float max2 = MAX2(c, d);//函数调用 printf("%f\n", max2); printf("%f\n", MAX(c, d));//宏调用 return 0; }
我们分别使用函数和使用宏来比较大小,
我们调试起来,转到反汇编后,注意看,现在准备进入函数调用,在此之前是准备工作。
当我们调用该函数时,会发现这么一大堆东西,这些都是函数在调用时需要做的工作,以及返回值需要做的工作。
再来看宏的开销:
对比函数和宏调用的开销,会发现,仅仅是比较大小,函数的开销比宏的开销多出了很多。
所以宏比函数在程序的规模和速度方面更胜一筹。
2. 函数的参数必须声明为特定的类型,而宏是类型无关的。
以上面的例子为例:
#define MAX(a,b) ((a)>(b)?(a):(b)) float MAX2(float c, float d) { return c > d ? c : d; } int MAX1(int a, int b) { return a > b ? a : b; } int main() { int a = 2; int b = 5; float c = 3.0f; float d = 4.0f; float max2 = MAX2(c, d);//函数调用 printf("%f\n", max2); int max1 = MAX1(c, d);//函数调用 printf("%d\n", max1); printf("%f\n", MAX(c, d));//宏调用 printf("%d\n", MAX(a, b));//宏调用 return 0; }
分别使用函数和宏对整型和浮点型数据进行大小比较,此时两个没有任何问题,但是接下来,
当我们更改图中数据,用浮点型函数比较整型大小时,回出现警告,可能会丢失数据,
结果也不符合,
所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以用于>来比较的类型。
记住,宏是类型无关的。
宏的缺点:
1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序
的长度。
#define TEST() printf("Test Successfully!\n") int main() { TEST(); TEST(); TEST(); //等价于 printf("Test Successfully!\n"); printf("Test Successfully!\n"); printf("Test Successfully!\n"); }
我们这样复制三份宏,替换后就已经出现代码较冗余的情况, 假如宏定义的代码有五十行,复制三份后就有一百五十行,情况更加严重。
2. 宏是没法调试的
调试起来的时候,按下F11,并没有跳转到宏所在的地方,因为宏在预编译的时候就已经完成了替换。
3. 宏由于类型无关,也就不够严谨。
宏是类型无关的,既是优点,也是缺点。
4. 宏可能会带来运算符优先级的问题,导致程容易出现错。
5.宏还可以做到传一个类型,然而函数做不到。
比如说,宏的参数可以是一个int,但是没有函数传参传一个int的说法,函数只能传一个int类型的值,但是绝对不会传一个int。
举个例子,好好体会一下。
#define MALLOC(num,type) (type*)malloc(num*sizeof(type)) int main() { int ret1 = (int*)malloc(10 * sizeof(int)); int ret = MALLOC(10, int); //等价于 printf("%d\n", ret1); printf("%d\n", ret); return 0; }
2.2.7命名约定
一般函数的宏的使用语法很相似。
所以语言本身没法帮我们区分二者。
我们平时的一个习惯是:
把宏名全部大写
函数名不要全部大写
2.3#undef的用法
#undef 是用来移除一个宏定义的
举个例子:
#define MAX 100 int main() { printf("%d\n", MAX); #undef MAX printf("%d\n", MAX); return 0; }
可以发现,编译都无法编译成功,说明MAX已经被移除了。
2.4 命令行定义
通俗地讲,命令行定义就是再程序预编译的时候改变一些参数,使他们能够随时地发生变化。
比如说下面:
给一个数组赋值,ARRARY_SIZE代表数组的大小,数组的大小通过命令行定义,在预编译阶段是可以发生改变的。
#include <stdio.h> int main() { int array [ARRAY_SIZE]; int i = 0; for(i = 0; i< ARRAY_SIZE; i ++) { array[i] = i; } for(i = 0; i< ARRAY_SIZE; i ++) { printf("%d " ,array[i]); } printf("\n" ); return 0; }
总结:命令行定义就是再程序预编译的时候改变一些参数,使他们能够随时地发生变化。
2.5 条件编译
条件编译,也就是有选择性地编译,把想要的留下。
比方说:
int main() { int arr[10] = { 0 }; for (int i = 0; i < 10; i++) { #ifdef DEBUG arr[i] = i; #endif printf("%d ", arr[i]); } return 0; }
请问这段代码输出结果是什么?
结果输出10个0。
因为这里我们使用了条件编译,#ifdef DEBUG,意思就是如果定义有DEBUG,就使用下面的语句,结束编译语句是#endif,在这区间内,如果条件成立,则执行,不成立就不执行。
由于未定义有DEBUG,所以条件不成立,不执行赋值语句,当我们在前面定义DEBUG,就可以了
在这里可以给DEBUG一个替换对象,也可以仅仅定义DEBUG。
常见的条件编译指令:
1.常量表达式
int main() { #if 1 printf("hehe\n"); #endif return 0; }
2.多分支的条件编译
int main() { #if 1==1 printf("hehe\n"); #elif 2==1 printf("haha\n"); #else printf("heihei\n"); #endif return 0; }
3.判断是否被定义过
#define DEBUG 0 //即使DEBUG被定义为0,为假,但是它已经被定义过了,就打印hehe int main() { #if !defined(DEBUG) // 只要定义过,不管定义什么,满足条件就参与编译 printf("hehe\n"); #endif return 0; }
注意,只要被定义过,不管被定义成什么,都成立。
并且,define后面加了一个字母d,表示defined,定义过的意思。
还有一个是
#ifndef DEBUG //注意这里多了个n,表示no printf("hehe\n",); #endif
4,嵌套定义
嵌套定义可以跟嵌套的条件判断类比,也就是 if 中还有 if 。
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
2.6文件包含
2.6.1 #include <> 和#include " "
我们知道,对于文件来说,假如我们需要打印东西,就需要引一个头文件,引#include
那假如我用 #include "stdio.h "
这样的写法呢?能否通过?
仍然可以打印出来。
#include "filename"
的查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。
如果找不到就提示编译错误。
用#include " "的方式包含文件,会先在源文件所在目录下查找,
也就是这些文件里查找,如果找不到,才会去标准库函数里面查找。
如上图.
而#include< >,则是直接在目录里面查找了。
所以,当我们有成千上万个源文件时,应该使用#include<>去查找。
2.6.2 嵌套文件包含
如果出现了这样的情况,也就是一个多个文件中都包含了同一个头文件,这会重复调用头文件,造成代码冗余,也会造成文件的速度的减慢。
解决办法:
1.条件编译
每个头文件的开头写
#ifndef __TEST_H__ #define __TEST_H__ //头文件的内容 #endif //__TEST_H__
意思就是,如果没有定义TEST.H这个头文件,那么就定义它,如果定义了,就不再次定义。
比如说这个,上面的红色框框,是test.h文件的内容,下面红色框框是test.c文件内容,在test.c文件中包含test.h文件,然后进入test.h文件,执行#ifndef,如果自己没有被定义,那就定义,如果定义过了,那就不重复定义了。
直接写下面这句话就可以了。
#pragma once
更加推荐第二种写法。
这篇文章到这里就结束了!
如果对于有帮助,不妨点赞关注吧!












