author: hjjdebug
date: 2025年 05月 25日 星期日 16:04:57 CST
descrip: c/c++怎样编写可变参数函数.
文章目录
函数的参数是可变的,这个功能很强大!
1. c语言可变参数函数实现原理.
1.1 c语言的可变参数函数书写规范
你可以使用stdarg.h头文件中定义的宏来创建可变参数的函数
- va_list: 是一种类型,变量args必需声明为va_list 类型
- va_start()宏用于指定可变参数列表args的开始位置。函数参数必须要有一个固定参数
- va_arg()宏用于获取可变参数列表中的值。
- va_end()宏在完成操作后调用,以释放内存。
举例:
$cat main.cpp
#include <stdio.h>
#include <stdarg.h>
int add(int count, ...)
{
int sum = 0;
va_list args;
va_start(args, count);
for(int i = 0; i < count; i++)
{
sum += va_arg(args, int);
}
va_end(args);
return sum;
}
int main() {
int i=add(7,1,2,3,4,5,6,7);
printf("i:%d\n",i);
return 0;
}
执行:
$ ./tt
i:28
这是最简单的例子了,要求的框框多咱不说,还有很多其它问题.
- 必须要传递一个固定参数. 例如此例的count, 否则va_start就没法为args
找到起始位置. - count 还不能搞错,如果个数传错了,结果肯定不对.
- 它要求传递的参数类型都是整数, 你传个浮点数, 编译的时候还不报错.计算结果却错了.
总之就是不完美, 太多的注意事项,出错概率大大增加, 估计很少人使用这种编程方法.
而且你可能也会抱怨接口,凭啥要我传递参数个数, 你自己不会数数个数吗?
c 还真的数不了, 除非约定最后一个参数是某个特殊数,例如0.
即va_arg(arg,int)=0; 为结尾. 则还可以用下面代码计算个数
#include <stdarg.h>
int count_args(const char *fmt, …) {
va_list ap;
int count = 0;
va_start(ap, fmt);
while (va_arg(ap, int) != 0) count++; // 以0作为结束标记
va_end(ap);
return count;
}
那第一个是固定参数能去掉吗? 去不了,反我去不了. 因为var_start要用它. 尽管函数可能不用它.
想一想,c的main函数传递的都是int main(int argc,char *argv[]); argc是参数个数, 就知道,
单凭函数自己,它就不知道参数有多少个.
关于stdarg.h中的几个宏, 从汇编代码上还能看出点什么吗?
测试代码:
我们以参数为0表示参数结尾,就不用传参数个数了.
$cat main.cpp
#include <stdio.h>
#include <stdarg.h>
int add(const char *notUse, ...)
{
va_list args;
va_start(args, notUse);
int sum = 0;
int i;
while((i=va_arg(args,int))!=0)
{
sum += i;
}
va_end(args);
return sum;
}
int main() {
int i=add("not use",1,2,3,4,5,6,7,0);
printf("i:%d\n",i);
return 0;
}
汇编码:
add 任意个参数的函数汇编码
0000000000400557 <_Z3addPKcz>:
#include <stdio.h>
#include <stdarg.h>
int add(const char *notUse, ...)
{
400557: 55 push %rbp
400558: 48 89 e5 mov %rsp,%rbp
40055b: 48 81 ec f0 00 00 00 sub $0xf0,%rsp #开辟栈帧,此栈能存30个参数,一般来讲是够了.
400562: 48 89 bd 18 ff ff ff mov %rdi,-0xe8(%rbp) # 保存6个寄存器中参数到堆栈
400569: 48 89 b5 58 ff ff ff mov %rsi,-0xa8(%rbp)
400570: 48 89 95 60 ff ff ff mov %rdx,-0xa0(%rbp)
400577: 48 89 8d 68 ff ff ff mov %rcx,-0x98(%rbp)
40057e: 4c 89 85 70 ff ff ff mov %r8,-0x90(%rbp)
400585: 4c 89 8d 78 ff ff ff mov %r9,-0x88(%rbp)
40058c: 84 c0 test %al,%al #判断一下浮点数个数
40058e: 74 20 je 4005b0 <_Z3addPKcz+0x59>
400590: 0f 29 45 80 movaps %xmm0,-0x80(%rbp) #保存浮点数参数到栈,本例没有使用浮点数.
400594: 0f 29 4d 90 movaps %xmm1,-0x70(%rbp)
400598: 0f 29 55 a0 movaps %xmm2,-0x60(%rbp)
40059c: 0f 29 5d b0 movaps %xmm3,-0x50(%rbp)
4005a0: 0f 29 65 c0 movaps %xmm4,-0x40(%rbp)
4005a4: 0f 29 6d d0 movaps %xmm5,-0x30(%rbp)
4005a8: 0f 29 75 e0 movaps %xmm6,-0x20(%rbp)
4005ac: 0f 29 7d f0 movaps %xmm7,-0x10(%rbp)
4005b0: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax # 取一个数保存到栈,用来做堆栈保护测试
4005b7: 00 00
4005b9: 48 89 85 48 ff ff ff mov %rax,-0xb8(%rbp)
4005c0: 31 c0 xor %eax,%eax
va_list args;
va_start(args, notUse);
4005c2: c7 85 30 ff ff ff 08 movl $0x8,-0xd0(%rbp) # 把第一个可变参数地址赋值给-0xd0(%rbp)
4005c9: 00 00 00
4005cc: c7 85 34 ff ff ff 30 movl $0x30,-0xcc(%rbp) #下面可能与浮点数传递有关,对本例无用.
4005d3: 00 00 00
4005d6: 48 8d 45 10 lea 0x10(%rbp),%rax # 无用
4005da: 48 89 85 38 ff ff ff mov %rax,-0xc8(%rbp) # 无用
4005e1: 48 8d 85 50 ff ff ff lea -0xb0(%rbp),%rax # 无用
4005e8: 48 89 85 40 ff ff ff mov %rax,-0xc0(%rbp) # 无用
int sum = 0;
4005ef: c7 85 28 ff ff ff 00 movl $0x0,-0xd8(%rbp) # -0xd8(%rbp) == sum
4005f6: 00 00 00
int i;
while((i=va_arg(args,int))!=0)
4005f9: 8b 85 30 ff ff ff mov -0xd0(%rbp),%eax
4005ff: 83 f8 2f cmp $0x2f,%eax
400602: 77 23 ja 400627 <_Z3addPKcz+0xd0> # >0x2f转下面,即超过6个参数转下面
400604: 48 8b 85 40 ff ff ff mov -0xc0(%rbp),%rax #可优化掉
40060b: 8b 95 30 ff ff ff mov -0xd0(%rbp),%edx #取地址,下面几句没有用,可优化调.
400611: 89 d2 mov %edx,%edx #可优化掉
400613: 48 01 d0 add %rdx,%rax #可优化掉
400616: 8b 95 30 ff ff ff mov -0xd0(%rbp),%edx # 取地址
40061c: 83 c2 08 add $0x8,%edx # 加8
40061f: 89 95 30 ff ff ff mov %edx,-0xd0(%rbp) # 保存地址.
400625: eb 12 jmp 400639 <_Z3addPKcz+0xe2>
400627: 48 8b 85 38 ff ff ff mov -0xc8(%rbp),%rax # -0xc8(%rbp) == addr
40062e: 48 8d 50 08 lea 0x8(%rax),%rdx # 得到下一个addr (+8)
400632: 48 89 95 38 ff ff ff mov %rdx,-0xc8(%rbp) # 保存
400639: 8b 00 mov (%rax),%eax # 从地址取数
40063b: 89 85 2c ff ff ff mov %eax,-0xd4(%rbp) # 保存到i -0xd4(%rbp)
400641: 83 bd 2c ff ff ff 00 cmpl $0x0,-0xd4(%rbp) # i==0?
400648: 0f 95 c0 setne %al
40064b: 84 c0 test %al,%al
40064d: 74 0e je 40065d <_Z3addPKcz+0x106>
{
sum += i;
40064f: 8b 85 2c ff ff ff mov -0xd4(%rbp),%eax # -0xd4(%rbp) == i
400655: 01 85 28 ff ff ff add %eax,-0xd8(%rbp) # -0xd8(%rbp) == sum
while((i=va_arg(args,int))!=0)
40065b: eb 9c jmp 4005f9 <_Z3addPKcz+0xa2>
}
va_end(args);
return sum;
40065d: 8b 85 28 ff ff ff mov -0xd8(%rbp),%eax
}
从汇编码上我们看出,尽管x86_64是通过6个寄存器加堆栈来传递参数的.
但对于不知道参数个数的函数来说,其内部实现还是通过把参数放入堆栈来计算的.
堆栈中保存了一系列参数,一个参数占8个字节,就像一个数组,你可以访问其中的项,
但是数组项的个数却是不知道的!
不过现成的可变参数的函数倒是可以调用的,
例如printf 函数, 它的参数项个数是通过解析format字符串得到的,
变参函数printf 用着确实很方便.
1.2. printf 为什么可以编译期检查参数类型是否错误.
gcc对printf 函数进行了特殊处理,通过解析格式字符串中的占位符(如%d、%s),
与后续参数类型进行静态匹配,从而判定传递的类型是否正确.
1.3. 我们可否定义在编译期能够检查参数类型的函数?
attribute((format))是GCC的扩展属性,
它可以在编译时检查变参函数的格式化字符串与参数类型是否匹配,
常用于自定义的printf风格的函数
举例:
// 声明带格式检查的日志函数
void debug_log(const char* file, int line, const char* fmt, …)
attribute((format(printf, 3, 4)));
attribute((format(printf,3,4))) 中的3表示格式化字符串是第3个参数.
4 表示变参从第4个参数开始.
// 函数实现 ,stdarg.h中的几个宏调用的也挺顺手,主要是vprintf支持可变参数va_list
void debug_log(const char* file, int line, const char* fmt, …) {
va_list args;
va_start(args, fmt);
printf("[%s:%d] ", file, line);
vprintf(fmt, args);
va_end(args);
}
//调用
int main() {
debug_log(FILE, LINE, “Value: %d\n”, 100); // 正确
debug_log(FILE, LINE, “Value: %s\n”, 100); // 编译警告:类型不匹配
return 0;
}
会给出编译警告, 恰同你直接调用printf 类型不匹配一样.
2. c++对变参函数的改良
C++11可使用模板参数包和sizeof…运算符直接获取参数个数
<typename… Args>
int arg_count(Args… args) {
return sizeof…(Args); //sizeof…(args)也可以
}
测试代码:
$ cat main.cpp
#include <stdio.h>
template <typename... Args>
int arg_count(Args... args) {
return sizeof...(Args); //sizeof...(args) 也可以.
}
int main() {
int i=arg_count("not use",1,2,3,4,5,6,7,0);
printf("i:%d\n",i);
return 0;
}
编译时首先收到了9条警告.
In instantiation of ‘int arg_count(Args …) [with Args = {const char*, int, int, int, int, int, int, int, int}]’:
int arg_count(Args… args) {
^~~~
main.cpp:3:23: warning: unused parameter ‘args#0’ [-Wunused-parameter]
main.cpp:3:23: warning: unused parameter ‘args#1’ [-Wunused-parameter]
main.cpp:3:23: warning: unused parameter ‘args#2’ [-Wunused-parameter]
main.cpp:3:23: warning: unused parameter ‘args#3’ [-Wunused-parameter]
main.cpp:3:23: warning: unused parameter ‘args#4’ [-Wunused-parameter]
main.cpp:3:23: warning: unused parameter ‘args#5’ [-Wunused-parameter]
main.cpp:3:23: warning: unused parameter ‘args#6’ [-Wunused-parameter]
main.cpp:3:23: warning: unused parameter ‘args#7’ [-Wunused-parameter]
main.cpp:3:23: warning: unused parameter ‘args#8’ [-Wunused-parameter]
从这个警告上我们知道它很牛, 它能够在编译期区分清args#0… args#8
看一下运行:
$ ./tt
i:9
正确,9个参数.
看看它把模板函数编译成了什么?
00000000004004e7 <main>:
int main() {
4004e7: 55 push %rbp
4004e8: 48 89 e5 mov %rsp,%rbp
4004eb: 48 83 ec 10 sub $0x10,%rsp # 开辟栈帧
int i=arg_count("not use",1,2,3,4,5,6,7,0);
4004ef: 48 83 ec 08 sub $0x8,%rsp # 这等价于加了一个参数,可能时对齐需要.
4004f3: 6a 00 pushq $0x0 # 从右向左如栈
4004f5: 6a 07 pushq $0x7
4004f7: 6a 06 pushq $0x6
4004f9: 41 b9 05 00 00 00 mov $0x5,%r9d # 前6个参数用寄存器
4004ff: 41 b8 04 00 00 00 mov $0x4,%r8d
400505: b9 03 00 00 00 mov $0x3,%ecx
40050a: ba 02 00 00 00 mov $0x2,%edx
40050f: be 01 00 00 00 mov $0x1,%esi
400514: 48 8d 3d d9 00 00 00 lea 0xd9(%rip),%rdi # 4005f4 字符串地址给第1参数
40051b: e8 24 00 00 00 callq 400544 <_Z9arg_countIJPKciiiiiiiiEEiDpT_> # 调用函数
400520: 48 83 c4 20 add $0x20,%rsp #调用着维持栈平衡. 入栈等价于4个,这里恢复
400524: 89 45 fc mov %eax,-0x4(%rbp)
printf("i:%d\n",i);
400527: 8b 45 fc mov -0x4(%rbp),%eax
40052a: 89 c6 mov %eax,%esi # 返回值给第2参数
40052c: 48 8d 3d c9 00 00 00 lea 0xc9(%rip),%rdi # 4005fc 字符串给第1参数
400533: b8 00 00 00 00 mov $0x0,%eax
400538: e8 b3 fe ff ff callq 4003f0 <printf@plt> #调用printf函数
return 0;
40053d: b8 00 00 00 00 mov $0x0,%eax
}
400542: c9 leaveq
400543: c3 retq
可变参数的模板函数, 竟被翻译成直接返回一个常数!!
0000000000400544 <_Z9arg_countIJPKciiiiiiiiEEiDpT_>:
int arg_count(Args... args) {
400544: 55 push %rbp
400545: 48 89 e5 mov %rsp,%rbp
400548: 48 89 7d f8 mov %rdi,-0x8(%rbp)
40054c: 89 75 f4 mov %esi,-0xc(%rbp)
40054f: 89 55 f0 mov %edx,-0x10(%rbp)
400552: 89 4d ec mov %ecx,-0x14(%rbp)
400555: 44 89 45 e8 mov %r8d,-0x18(%rbp)
400559: 44 89 4d e4 mov %r9d,-0x1c(%rbp)
return sizeof...(Args);
40055d: b8 09 00 00 00 mov $0x9,%eax
}
400562: 5d pop %rbp
400563: c3 retq
可见参数个数是编译器gcc 通过关键字sizeof…(xxx)
以常数的方式给出的.
我们用变参函数模板重写上边的累加函数,这次是完美的.
#include <stdio.h>
template <typename... Args>
int add(Args... args)
{
int sum=0;
((sum += args),...); //折叠表达式,逗号表示重复,...表示参数展开.
return sum;
}
int main() {
int i=add(1,2,3,4,5,6,7);
printf("i:%d\n",i);
return 0;
}
执行结果:
$ ./tt
i:28
正确! 而且接口调用中没有多余的东西!
看一下其汇编码的实现:
000000000040053e <_Z3addIJiiiiiiiEEiDpT_>: //int add<int, int, int, int, int, int, int>(int, int, int, int, int, int, int)
int add(Args... args)
40053e: 55 push %rbp
40053f: 48 89 e5 mov %rsp,%rbp
400542: 89 7d ec mov %edi,-0x14(%rbp) # 保存6个寄存器
400545: 89 75 e8 mov %esi,-0x18(%rbp)
400548: 89 55 e4 mov %edx,-0x1c(%rbp)
40054b: 89 4d e0 mov %ecx,-0x20(%rbp)
40054e: 44 89 45 dc mov %r8d,-0x24(%rbp)
400552: 44 89 4d d8 mov %r9d,-0x28(%rbp)
int sum=0;
400556: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp) -0x4(%rbp)==sum
((sum += args),...);
40055d: 8b 45 ec mov -0x14(%rbp),%eax
400560: 01 45 fc add %eax,-0x4(%rbp)
400563: 8b 45 e8 mov -0x18(%rbp),%eax
400566: 01 45 fc add %eax,-0x4(%rbp)
400569: 8b 45 e4 mov -0x1c(%rbp),%eax
40056c: 01 45 fc add %eax,-0x4(%rbp)
40056f: 8b 45 e0 mov -0x20(%rbp),%eax
400572: 01 45 fc add %eax,-0x4(%rbp)
400575: 8b 45 dc mov -0x24(%rbp),%eax
400578: 01 45 fc add %eax,-0x4(%rbp)
40057b: 8b 45 d8 mov -0x28(%rbp),%eax #前面6个参数内容相加到sum
40057e: 01 45 fc add %eax,-0x4(%rbp)
400581: 8b 45 10 mov 0x10(%rbp),%eax #0x10(%rbp)是堆栈中保留的第7个参数
400584: 01 45 fc add %eax,-0x4(%rbp)
return sum;
400587: 8b 45 fc mov -0x4(%rbp),%eax
}
40058a: 5d pop %rbp #恢复rbp
40058b: c3 retq # 返回
可见c++编译器,很好的解决了变参函数问题, 比c好多了.
你可以通过sizeof…(args)得到编译器返给你的参数个数,是个常数.
你可以通过折叠表达式描述你的代码.
通过…来展开操作.