带副作用的宏参数
当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。
例如:
x+1;不带副作用
x++; 带有副作用
求两个数较大值
- 宏代码实例
MAX宏可以证明具有副作用的参数所引起的问题。
#define MAX(X,Y) ((X)>(Y)?(X):(Y)) int main() { int a = 3; int b = 5; int c = MAX(a++, b++); //int c = ((a++)>(b++)?(a++):(b++)); // 3 5 6 printf("%d\n", c);//6 printf("%d\n", a);//4 printf("%d\n", b);//7 return 0; }
最终于输出结果
6
4
7
- 函数代码实例
换成函数实现这个功能,后果不至于那么严重。因为函数的参数是计算之后传进去的,不是替换进去的
int Max(int x, int y) { return x>y?x:y; } int main() { int a = 3; int b = 5; int c = Max(a++, b++); printf("%d\n", c); printf("%d\n", a); printf("%d\n", b); return 0; }
最终输出结果
5
4
6
注: 后置++ 是先使用后++,Max使用完才++
宏和函数对比
命名约定:一般来讲函数的宏的使用语法很相似。所以语言本身没法帮我们区分二者。那我们平时的一个习惯是:
- 把宏名全部大写
- 函数名不要全部大写
这只是我们的一个约束,但有的编译器甚至把宏全部小写。
宏通常被应用于执行简单的运算。
比如在两个数中找出较大的一个。
函数同样也可以完成同样的问题,那两者哪一个更好呢?
#include<stdio.h> #include<stdlib.h> //函数的实现 - 1 int Max(int x, int y) { return x > y ? x : y; } //宏的实现 - 2 #define MAX(x,y) ((x)>(y)?(x):(y)) int main() { int a = 0; int b = 0; //输入 scanf("%d %d", &a, &b); //较大值 int m1 = Max(a, b); printf("%d\n", m1); int m2 = MAX(a, b); //int m2 = ((a)>(b)?(a):(b)); printf("%d\n", m2); system("pause"); return 0; }
实现该功能宏更加好,原因有2:
1.用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。
函数调用的时间花费:
- 函数调用前准备 (传参、L函数栈帧空间的维护)
- 函数内部(主要运算)
- 函数返回,返回值的处理,函数栈帧的销毁
所以宏比函数在程序的规模和速度方面更胜一筹。
2.更为重要的是函数的参数必须声明为特定的类型。(比如一定要定义int char类型的)
所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以用来比较的类型。
宏是与类型无关的。
当然和函数相比宏也有劣势的地方:
1.每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
2.宏是没法调试的。
3.宏由于类型无关,也就不够严谨。
4.宏可能会带来运算符优先级的问题,导致程容易出现错。
以后功能比较简单的时候,可以采用宏来实现,如果功能比较复杂,建议使用函数来实现
宏有时候可以做函数做不到的事情。比如:宏的参数可以只出现类型,但是函数做不到。
- 代码实例:
#include<stdio.h> #include<stdlib.h> #define MALLOC(num,Type) (Type*)malloc(sizeof(Type)*num) int main() { int* p = MALLOC(10, int); if (p == NULL) perror("malloc:"); //业务处理 //内存释放 free(p); p = NULL; system("pause"); return 0; }
宏和函数的一个对比
属性 | #define定义宏 | 函数 |
代码长度 | 每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅度增长 | 函数代码只出现于一个地方;每次使用这个函数时,都调用那个地方的同一份代码 |
执行速度 | 更快 | 存在函数的调用和返回的额外开销,所以相对慢一些 |
操作符优先级 | 宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多些括号。 | 函数参数只在函数调用的时候求值一次,它的结果值传递给函数。表达式的求值结果更容易预测。 |
带有副作用的参数 | 参数可能被替换到宏体中的多个位置,所以带有副作用的参数求值可能会产生不可预料的结果。 | 函数参数只在传参的时候求值一次,结果更容易控制。 |
参数类型 | 宏的参数与类型无关,只要对参数的操作是合法的,它就可以使用于任何参数类型。 | 函数的参数是与类型有关的,如果参数的类型不同,就需要不同的函数,即使他们执行的任务是相同的。 |
调试 | 宏是不方便调试的 | 函数是可以逐语句调试的 |
递归 | 宏是不可以递归 | 函数是可以递归 |
#undef
这条指令用于移除一个宏定义。
- 代码实例
此时后面的MAX已经出现未定义了,因为给我们#undef取消了.
命令行定义
许多C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。
执行程序时,可以从命令行传值给 C 程序。这些值被称为命令行参数,它们对程序很重要,特别是当您想从外部控制程序,而不是在代码内对这些值进行硬编码时,就显得尤为重要了。
例如:当我们根据同一个源文件要编译出一个程序的不同版本的时候,这个特性有点用处。(假定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大些,我们需要一个数组能够大些。)
那应该怎么操作呢?
- 接下来我们使用Vscode gcc编译器演示
此时SZ还没有定于,我们用命令行定义
//演示命令行定义 int main() { int a[SZ]; int i = 0; for (i = 0; i < SZ; i++) { a[i] = i; } for (i = 0; i < SZ; i++) { printf("%d ", a[i]); } return 0; }
我们输入: gcc .\test-vscode.c -D SZ=10 (把SZ定义成10)
命令行定义是真实存在的。这个只能在gcc编译器才能演示,Vs是无法查看的.
条件编译
在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令。
比如说:
调试性的代码,删除可惜,保留又碍事,所以我们可以选择性的编译。
我们继续在VScode里给大家演示:
从预处理可以看见
#ifdef DEBUG
#endif //DEBUG
确实把printf那一句屏蔽掉了,我们只要把__DEBUG__定义了就可以解除屏蔽了
当然这里还有更多的条件编译指令,大家可以自行去了解。这里不一 一陈述。
文件包含
#include 指令可以使另外一个文件被编译。就像它实际出现于 #include 指令的地方一样.
这样一个源文件被包含10次,那就实际被编译10次。
头文件被包含的方式:
- 本地文件包含
#include “filename”
查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。如果找不到就提示编译错误.
- 库文件包含
#include <filename.h>
查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。
这样是不是可以说,对于库文件也可以使用 “ ” 的形式包含?
答案是肯定的,可以。
他先去本地文件查找,在去库文件查找,这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。
自己定义的原文件不能用<>,因为我们自己定义的是放在本地文件的。
嵌套文件包含
如果出现这样的场景:
comm.h和comm.c是公共模块。
test1.h和test1.c使用了公共模块。
test2.h和test2.c使用了公共模块。
test.h和test.c使用了test1模块和test2模块。
这样最终程序中就会出现两份comm.h的内容。这样就造成了文件内容的重复。
- 如何解决这个问题?
- 条件编译。
每个头文件的开头写:
#ifndef __TEST_H__ #define __TEST_H__ //头文件的内容 #endif //__TEST_H__
- 也是最常用的一种
#pragma once
可以避免头文件的重复引入