在我们用C语言写函数时,在设置函数参数时,是否偶有不确定的时候,“这个参数…用宏…或者全局变量来代替会不会更合适?”就像用C写栈的初始化时,一开始是否要给栈开空间?要开的话开多大呢?设计C++的大佬可能也是意识到了这点,于是便有了缺省参数;用宏写简单函数时,总是会被符号优先级的问题困扰,而且宏函数不会进行类型检查,不用怕!C++推出了内联函数来帮你解决。
缺省参数的规则
缺省参数就是一个有默认值的参数,设有缺省参数的函数,在被调用时,不给缺省参数传参的话,缺省参数就会是默认值,简单举例如下:
#include <iostream>
#include <stdio.h>
void print(int a, int b = 6, int c = 9) {
std::cout << "a:" << a << std::endl; // 这里的输出语句不懂的话可以去看看上一篇笔记
std::cout << "b:" << b << std::endl;
printf("c:%d\n\n", c); // C++兼容C语言哦
}
int main() {
printf("不缺省:\n");
print(3, 4, 5); // 对全部参数都传参时,就和普通函数一样
printf("完全缺省:\n");
print(3); // 不给缺省函数,则该参数为默认值
printf("部分缺省:\n");
print(3, 4); // 缺省函数可以部分缺省,缺省后按参数位置对应传参
return 0;
}
需要注意的是缺省参数与普通参数需要各自分开,而且缺省参数只能设置在函数参数的尾部,简单思考下:传参是按参数顺序进行传参的,如果普通参数位于缺省参数之后,在参数个数只有一个的情况时,这个参数是传给缺省参数呢?还是后面的普通参数呢?二义性就此产生,所以部分缺省时,缺省参数只能设置在函数参数的尾部,简单举例如下:
// 错误写法:
// 普通参数与缺省参数交错使用
//void print(int a = 1, int b, int c = 3) {
//}
// 虽然将缺省参数与普通参数分开,但是缺省参数未在尾部
//void print(int a = 1, int b = 2, int c) {
//}
// 正确写法:
// 各自分开且缺省参数位于尾部
void print1(int a , int b = 2, int c = 3) {
}
// 当完全缺省时,可任意摆放参数位置
void print2(int b = 2, int a = 1, int c = 3) {
}
当含缺省参数的函数的声明和定义分开时,需要注意的是修改缺省参数时,声明处和定义处都要进行修改,这不免就会有出现失误的时候,而且后续要增添缺省参数时也会很麻烦,其实有一种办法可以解决,就是把声明放头文件里放,只在声明处设置缺省参数,定义处不写缺省参数,全是普通参数,在编译器编译后的链接步骤中,由于函数名相同,且参数的类型及其顺序一样,编译器就会将该声明和定义匹配在一起,变成一个带缺省参数的函数,只需要在声明处修改、增添缺省参数即可,避免声明与定义冲突的情况发生了。
简单应用场景举例
// 初始化栈(C语言版)
#define INITSIZE 4 // 为了避免调用时,需要刻意传参,用宏来定义初始化栈空间的大小
void StackInit(Stack* ps) {
assert(ps);
ps->arr = (StDataType*)malloc(sizeof(StDataType) * INITSIZE);
ps->top = 0;
ps->capacity = INITSIZE;
}
上述代码在一般场景下是可以过关的,但是在需要频繁扩容时,就不够灵活了,如下:
int main()
{
Stack st;
StackInit(&st);
for (int i = 0; i < 10; i++) { // 这里入栈10个数据
StackPush(&st, i); // 入栈函数,假设已经实现
}
for (int i = 0; i < 188; i++) { // 这里入栈188个数据
StackPush(&st, i);
}
// ......后续都是这种知道要入栈数据的个数的循环入栈
return 0;
}
由于各次入栈的数目不同,如果规定死初始化空间的大小的话,会出现没必要的扩容带来的开销,或许在数据量小的情况下是可以忽略不计的,但是当数据量大起来,异地扩容带来的开销就会挺难受的。
如果使用缺省参数的话,就能完美解决这一问题,同时在正常使用时如果不确定具体入栈数量时,也可以选择缺省参数,开默认值大小的空间,避免了一些精神内耗,哈哈;初始化栈函数如下:
// 初始化栈(缺省参数版)
void StackInit(Stack* ps, int initcapacity = 4) {
assert(ps);
ps->arr = (StDataType*)malloc(sizeof(StDataType) * initcapacity);
ps->top = 0;
ps->capacity = INITSIZE;
}
这样在使用时便可以随机应变,提高效率的同时也增加了函数的可读性。
内联函数
提问:现在能写一个Add的宏函数吗?
笔者大概可以猜到一些读者的答案:
#define Add(a,b) a+b; // 预处理时会将;一起替换上去
#define Add(a,b) a+b // 如果前面有优先级比+更高的运算符的话,会出现错误
#define Add(a,b) (a+b) // 比上一行好了一些,但还不够,如果a是(2|3)这种比+号优先级低的运算符就会出问题
#define Add(a,b) ((a)+(b)) // 正确答案
宏函数有些时候确实方便一些,能避免一些函数调用带来的花销,但是在实现的时候,要比写普通函数多想一些反逻辑的地方,也不好进行函数的调试,这使得函数的可维护性下降,那么inline函数就很好地解决了这一痛点。
内联函数就是在正常的函数前面加上关键字inline,然后通过将该函数的汇编指令直接在调用处展开的方式来避免call指令带来的花销,达到与宏函数的一样的替换效果,不仅如此,内联函数和正常函数有一样的属性,会进行类型检查以及方便调试等,有一点不同的是内联函数不支持声明和定义分离。
需要注意的是当内联函数过于复杂时,编译器不会展开该内联函数的汇编指令,如内联函数是一个递归函数、内联函数有百八十行等情况,具体多少行后就不展开要看对应编译器,因为inline其实是给编译器发送一个展开的请求,请求是否会被编译器处理就要看设计编译器的大佬是怎么设计的了,要确定你用的编译器是否展开了内联函数,我们可以先调至release版本(debug版本下需要设置一些参数,比较麻烦)然后逐步运行,在内联函数调用处进入反汇编代码查看,如果没有通过call寻址调用,那就是展开了,一般六七十行以下的内联函数是没问题的,所以是有能力替代宏函数的。
虽然使用内联函数能使计算机更加快速地运行代码,不过当大量调用内联函数时,会让程序的空间花销大起来,就是对应的安装包会大一些,因为每次展开的指令都是需要重复存储的,所以在需要大量使用内联函数时,需要斟酌一下是否要将该内联函数设为普通函数使用。
总结
本篇介绍了缺省参数的使用以及内联函数和宏的辨析,含缺省参数的函数的声明和定义分离时需要注意保持一致,也可以采用多文件的方式解决,内联函数有好处但在使用时要注意展开指令带来的空间花销,以及不要将内联函数的声明和定义分离了哦。
给感兴趣的读者出道题:学习缺省参数后,宏函数是否可以被替代呢?用宏定义的常量呢?欢迎在评论区中留下你的答案。