在开发中,我们可能需要定义可以处理不同数量参数的函数。如果我们使用传统的方式定义多个函数,每个函数接受不同数量的参数,那么这将会导致代码冗余,不易维护。因此,使用可变参数函数可以避免这种情况的发生,提高代码的可维护性和重用性。
方法一 cstdarg头文件
我们查看printf
函数的实现,就通过这样的方式实现了可变参数,即使用了...
:
_CRT_STDIO_INLINE int __CRTDECL printf(
_In_z_ _Printf_format_string_ char const* const _Format,
...)
提示:在C++中
cstdarg
和stdarg.h
功能一致,但是stdarg.h
定义在全局命名空间中(global namespace),而cstdarg
定义在标准命名空间(std namespace)中,也就是说,需要使用using namespace std;
或者加上std::
。
下面是使用cstdarg
库的一个例子:
#include <stdarg.h>
#include <iostream>
int get_sum(int num, ...) {
int result = 0;
//第一步:初始化
va_list ap;
//第二步:va_start 宏允许访问后随具名参数 parm_n 的可变参数。
//void va_start( std::va_list ap, parm_n );
va_start(ap, num);
for (int i = 0; i < num;i++) {
//第三步:va_arg 宏展开成对应来自 std::va_list ap 的下个参数的 T 类型表达式。
result += va_arg(ap, int);
}
//最后:va_end 宏进行对为 va_start 或 va_copy 所初始化的 ap 对象的清理。 va_end 可以修改 ap 并令它不再可用。
va_end(ap);
return result;
}
int main() {
std::cout << get_sum(3,2,3,4) << std::endl; // result=9
return 0;
}
cstdarg
中定义了如下几个宏(C++11中还增加了va_copy),他们的功能分别如下:
va_start()
:初始化一个可变参数列表,并使其指向第一个可变参数。va_arg()
:获取可变参数列表中的下一个参数。va_end()
:结束一个可变参数列表的访问。
查看这几个宏的具体定义,可见这样的可变参数列表是通过指针来实现的,sizeof(t)
意味着t
需要有确定的大小。也就是说,其支持的数据类型仅包括 int、double、float、char、指针。
#define va_start __crt_va_start
#define va_arg __crt_va_arg
#define va_end __crt_va_end
#define __crt_va_start_a(ap, x) ((void)(__va_start(&ap, x)))
#define __crt_va_arg(ap, t) \
((sizeof(t) > sizeof(__int64) || (sizeof(t) & (sizeof(t) - 1)) != 0) \
? **(t**)((ap += sizeof(__int64)) - sizeof(__int64)) \
: *(t* )((ap += sizeof(__int64)) - sizeof(__int64)))
#define __crt_va_end(ap) ((void)(ap = (va_list)0))
由此可见,想要使用省略符来实现可变参数,就必须要指定后续参数的个数。因此,通过cstdarg
库实现可变参数是非常危险的,当传入的参数数量与指定的参数数量不一致时,程序可能会访问到错误的地址,尤其当传入的参数是指针时,程序可能会读取或写入未分配的内存,造成崩溃和错误。
方法二 可变参数模板参数包
在C++11标准中引入了一种新的语法特性——可变参数模板参数包(variadic template parameter pack),这使得我们可以定义任意数量参数的模板函数或类。
在之前的C++标准中,函数或类模板只能使用确定数量的模板参数,但是这样做会限制它们的灵活性。比如,如果我们想要定义一个模板函数,它可以接受任意数量的参数并对它们进行求和,我们必须使用重载或者是函数模板嵌套的方式来实现。这会使得代码变得冗长且难以维护。而可变参数模板参数包的引入,可以大大简化这个过程。
使用可变参数模板参数包,我们可以定义一个函数模板,它接受任意数量的参数,并将这些参数打包成一个参数包(parameter pack)。我们可以使用类似于展开操作符(unpacking operator)的语法,将参数包中的每一个参数解包出来,这样就可以方便地对这些参数进行处理。
下面使用例子1展示一些参数包展开的常用情景
//可变参数模板参数包展开 例子1
#include <iostream>
template<typename... Args>
auto sum(Args... args){
return (args + ...);
}
template<typename... Args>
void print_A(Args... args){
(std::cout << ... << args) << std::endl;
}
int main(){
std::cout<<sum(1,2,3,4)<<std::endl;
print_A("Hello world ",2023,"-",1,"-",1);
return 0;
}
//输出
//10
//Hello world 2023-1-1
除了参数包展开之外,我们也可以使用递归调用来实现可变参数。对于下面例子2****递归调用的情况,当传入多个参数时,print_B
函数会递归调用void print_B(const T& arg, const Args&... args)
,直至只剩一个参数时,得益于C++函数重载的特性,会调用void print_B(const T& arg)
,这样就实现了打印函数。
//例子2
#include <iostream>
// 基本情况
template<typename T>
void print_B(const T& arg) {
std::cout << arg << std::endl;
}
// 递归情况
template<typename T, typename... Args>
void print_B(const T& arg, const Args&... args) {
std::cout << arg ;
print_B(args...);
}
int main(){
// 递归调用
print_B("Hello world ",2023,"-",1,"-",1);
return 0;
}
//输出:
//Hello world 2023-1-1
方法三 初始化列表(initializer_list)
在C++11之后,还引入了initializer_list,它也可以用于实现可变参数函数。与可变参数模板参数包不同,initializer_list是一个包含任意数量元素的列表,但是所有元素的类型必须相同。
#include <iostream>
double get_avg(std::initializer_list<int> li){
int sum=0;
int count=0;
for(auto i:li){
sum+=i;
count++;
}
return (double)sum/count;
}
int main(){
std::cout<<get_avg({4,5,6,7})<<std::endl;
return 0;
}