Bootstrap

手把手教你实现 C 语言的函数多参默认值 「上」

以下内容为本人的学习笔记,如需要转载,请声明原文链接 微信公众号「ENG八戒」https://mp.weixin.qq.com/s/nj0C9SbAuzUOPt_J1n8B_Q

自从上一篇关于 C 语言单个参数函数的默认值实现《C语言函数也可以给形参添加默认值?》发表以来,有很多的同学反馈想知道多参数函数的默认值又该如何实现,今天特地整理相关代码和实现思路说明如下。

由于 C 语言本身是不提供函数默认值功能和相关语法的,为了实现函数参数的默认值,归根到底是对原有函数参数的自动填充,填充的值就是默认值。

那么如何填充参数值,又不会影响程序性能呢?可以使用宏定义的灵活性在代码的编译预处理期做一些替换操作,本文和《C语言函数也可以给形参添加默认值?》文章的基本思路类似,但是实现上会繁琐一些,最终的目标是实现从单参数默认值向多参数默认值迭代升级。

设想一下总体思路:

由于带有默认值的函数在使用时,形式上输入的参数个数是不定的,所以需要计算函数实际输入参数的个数,然后针对未输入的参数填充默认值。所以这里提出两个问题,一个是如何计算函数实际输入参数的个数,另一个是如何对参数填充默认值?

接下来将对提出来的问题逐一破解。

计算函数实际输入参数的个数

我们先来定义一个带有 2 个输入参数的目标函数 _fun2():

void _fun2(int val1, int val2)
{
    printf("fun inputs val1:%d, val2:%d\n", val1, val2);
}

参考《C语言函数也可以给形参添加默认值?》文章的基本思路,定义一个变长参数宏 fun2() 代表目标函数 _fun2(),原来调用目标函数 _fun2() 的语句将变成直接调用宏 fun2(),而目标函数 _fun2() 间接被调用。

但是由于我们的思路里多了一个因素—函数实际输入参数的个数,所以变长参数宏 fun2() 和目标函数 _fun2() 之间还需要多一层转换,下面统称为转换函数 _funs(),函数实际输入参数(调用宏 fun2() 时输入的参数)的个数作为参数输入到转换函数 _funs(),目标函数 _fun2() 在转换函数 _funs() 内部被调用。

#define fun2(...)           _funs(ARGC(__VA_ARGS__), 123, 456, ##__VA_ARGS__)

void _funs(int real_param_num, ...)
{
    ...
    // _fun2(val1, val2);
}

宏定义 ARGC() 用于计算函数实际输入参数的个数,123 和 456 作为目标函数 _fun2() 的参数默认值。real_param_num 是转换函数 _funs() 的第一个参数,用于接收变长参数宏 fun2() 被调用时输入的参数个数。

另外,鉴于视乎实际需求,目标函数的参数也可以是 2 之外的其它数目,所以为了便于扩展转换函数 _funs() 的应用范围,需要输入目标函数的完整参数个数,继续迭代转换函数 _funs()。

#define fun2(...)           _funs(2, ARGC(__VA_ARGS__), 123, 456, ##__VA_ARGS__)

void _funs(int param_num_max, int real_param_num, ...)
{
    ...
    // _fun2(val1, val2);
}

用变长参数宏 fun2() 代表有 2 个输入参数的目标函数 _fun2(),param_num_max 指定目标函数的完整参数个数。

ARGC 如何实现?

为什么使用宏定义来计算函数实际输入参数的个数?

由于 C 运行过程中确认参数个数会消耗系统资源,并降低程序运行效率,所以尽量采用在编译预处理时计算的宏定义。

暂定参数个数上限是 2 个,用宏定义预处理对参数的展开来统计参数个数,涉及占位符的妙用(相信会令你开眼界):

#define __ARGS(X) (X)
#define __ARGC_N(_0,_1,N,...) N
#define __ARGC(...) __ARGS(__ARGC_N(__VA_ARGS__,2,1))
#define ARGC(...)       __ARGC(__VA_ARGS__)

其中 _0, _1,… 是预处理占位符,用于在宏定义展开参数时逐个匹配参数,_0 匹配第一个参数,_1 匹配第二个参数,以此类推,最终 N 用于匹配 __ARGC_N(__VA_ARGS__,2,1) 后边的逆序数字 2 或者 1,这个 N 就是最终计算所得的参数个数。

测试一下对参数个数的计算效果:

printf("ARGC() arg num=%d\n", ARGC());
printf("ARGC(1) arg num=%d\n", ARGC(1));
printf("ARGC(1, 2) arg num=%d\n", ARGC(1, 2));

针对上面的调用,为了更好理解求值过程,让我们尝试一下手动对宏调用 ARGC(1) 展开看看:

ARGC(1)
__ARGC(1)
__ARGS(__ARGC_N(1,2,1))
(__ARGC_N(1,2,1))       ---->  (__ARGC_N(1,2,N))
(1)                     ----> N = 1

ARGC(1) 应该返回 1。

完整测试结果输出:

ARGC() arg num=1
ARGC(1) arg num=1
ARGC(1, 2) arg num=2

为什么 ARGC() 输入 0 个参数时,计算结果不为 0,而输入其它数量的参数计算结果符合预期呢?计算过程还有待改进。

你可以自己手动展开一下 ARGC() 看看,当 ARGC() 输入 0 个参数时,__ARGC_N(__VA_ARGS__,2,1) 后边的逆序数字数量不足以匹配 N,此时的 N 就强制匹配最后一个数字 1 了。为了解决这种异常匹配,当 N == 1 时,可以追加对第一个输入参数是否为空的判断:

#define __ARGC_N(_0,_1,N,...) N==1?(#_0)[0]!=0:N

#_0 用于将第一个参数转换成字符串,会占用一点空间,但不影响程序运行效率。(#_0)[0] 提取字符串的第一个字节。当 N == 1 时,如果第一个参数是空字符,(#_0)[0]!=0 返回 false,否则返回 true。false 转换成 0,true 转换成 1。

最终测试结果输出:

ARGC() arg num=0
ARGC(1) arg num=1
ARGC(1, 2) arg num=2

如果参数上限是 3 个,又怎么修改呢?

#define __ARGS(X) (X)
#define __ARGC_N(_0,_1,_2,N,...) N==1?(#_0)[0]!=0:N
#define __ARGC(...) __ARGS(__ARGC_N(__VA_ARGS__,3,2,1))
#define ARGC(...)       __ARGC(__VA_ARGS__)

__ARGC_N(__VA_ARGS__,3,2,1) 后边的逆序最大数字 3 就决定了参数的上限个数为 3。当然修改参数上限,__ARGC_N 宏定义输入参数中的占位符 _0,_1,_2 个数需要对应参数上限的个数,占位符从 _0 开始。

如果输入的参数超过了上限呢?

printf("ARGC(1, 2, 3) arg num=%d\n", ARGC(1, 2, 3));
printf("ARGC(1, 2, 3, 6) arg num=%d\n", ARGC(1, 2, 3, 6));
printf("ARGC(1, 2, 3, 6, 9) arg num=%d\n", ARGC(1, 2, 3, 6, 9));

结果输出:

ARGC(1, 2, 3) arg num=3
ARGC(1, 2, 3, 6) arg num=6
ARGC(1, 2, 3, 6, 9) arg num=6

输出的参数个数就等于超出上限的第一个参数值,不过,有言在先,参数个数有上限,而且合理设计的函数参数不应该过多,不必过度计较了。


未完待续,关注我查看更多精彩内容


;