c/c++怎样编写可变参数函数.


author: hjjdebug

date: 2025年 05月 25日 星期日 16:04:57 CST

descrip: c/c++怎样编写可变参数函数.


文章目录

  • [1. c语言可变参数函数实现原理.](#1. c语言可变参数函数实现原理.)
    • [1.1 c语言的可变参数函数书写规范](#1.1 c语言的可变参数函数书写规范)
    • [1.2. printf 为什么可以编译期检查参数类型是否错误.](#1.2. printf 为什么可以编译期检查参数类型是否错误.)
    • [1.3. 我们可否定义在编译期能够检查参数类型的函数?](#1.3. 我们可否定义在编译期能够检查参数类型的函数?)
  • [2. c++对变参函数的改良](#2. c++对变参函数的改良)

函数的参数是可变的,这个功能很强大!

1. c语言可变参数函数实现原理.

1.1 c语言的可变参数函数书写规范

你可以使用stdarg.h头文件中定义的宏来创建可变参数的函数

  • va_list: 是一种类型,变量args必需声明为va_list 类型
  • va_start()宏用于指定可变参数列表args的开始位置。函数参数必须要有一个固定参数
  • va_arg()宏用于获取可变参数列表中的值。
  • va_end()宏在完成操作后调用,以释放内存。
    举例:
cpp 复制代码
$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

这是最简单的例子了,要求的框框多咱不说,还有很多其它问题.

  1. 必须要传递一个固定参数. 例如此例的count, 否则va_start就没法为args
    找到起始位置.
  2. count 还不能搞错,如果个数传错了,结果肯定不对.
  3. 它要求传递的参数类型都是整数, 你传个浮点数, 编译的时候还不报错.计算结果却错了.
    总之就是不完美, 太多的注意事项,出错概率大大增加, 估计很少人使用这种编程方法.

而且你可能也会抱怨接口,凭啥要我传递参数个数, 你自己不会数数个数吗?

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表示参数结尾,就不用传参数个数了.

cpp 复制代码
$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 任意个参数的函数汇编码

cpp 复制代码
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)也可以

}

测试代码:

cpp 复制代码
$ 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个参数.

看看它把模板函数编译成了什么?

cpp 复制代码
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   

可变参数的模板函数, 竟被翻译成直接返回一个常数!!

cpp 复制代码
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)

以常数的方式给出的.

我们用变参函数模板重写上边的累加函数,这次是完美的.

cpp 复制代码
#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

正确! 而且接口调用中没有多余的东西!

看一下其汇编码的实现:

cpp 复制代码
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)得到编译器返给你的参数个数,是个常数.

你可以通过折叠表达式描述你的代码.

通过...来展开操作.

相关推荐
dd向上4 小时前
8位单通道数据保存为JPG
c++·图像处理
fpcc6 小时前
跟我学c++中级篇——动态库的资源处理
开发语言·c++
泽02026 小时前
C++之string的模拟实现
开发语言·数据结构·c++·算法
姬公子5217 小时前
leetcode hot100刷题日记——29.合并两个有序链表
c++·leetcode·链表
菜一头包7 小时前
CPP中CAS std::chrono 信号量与Any类的手动实现
开发语言·c++
new出对象7 小时前
C++ 中的函数包装:std::bind()、std::function<>、函数指针与 Lambda
开发语言·c++·算法
宁静祥和----------7 小时前
编码总结如下
c++
理论最高的吻8 小时前
面试题 08.08. 有重复字符串的排列组合【 力扣(LeetCode) 】
c++·算法·leetcode·深度优先·回溯法
whoarethenext8 小时前
c/c++的opencv霍夫变换
c语言·c++·opencv
ontheway-xx10 小时前
Windows MongoDB C++驱动安装
数据库·c++·mongodb