Skip to content

「C/C++」 可变参数函数的实现方式

Published at:

在开发中,我们可能需要定义可以处理不同数量参数的函数。如果我们使用传统的方式定义多个函数,每个函数接受不同数量的参数,那么这将会导致代码冗余,不易维护。因此,使用可变参数函数可以避免这种情况的发生,提高代码的可维护性和重用性。

方法一 cstdarg头文件

我们查看printf函数的实现,就通过这样的方式实现了可变参数,即使用了...

C
_CRT_STDIO_INLINE int __CRTDECL printf(
    _In_z_ _Printf_format_string_ char const* const _Format,
    ...)

提示:在C++中cstdargstdarg.h功能一致,但是stdarg.h定义在全局命名空间中(global namespace),而cstdarg定义在标准命名空间(std namespace)中,也就是说,需要使用using namespace std;或者加上std::

下面是使用cstdarg库的一个例子:

c++
#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),他们的功能分别如下:

  1. va_start():初始化一个可变参数列表,并使其指向第一个可变参数。
  2. va_arg():获取可变参数列表中的下一个参数。
  3. va_end():结束一个可变参数列表的访问。

查看这几个宏的具体定义,可见这样的可变参数列表是通过指针来实现的,sizeof(t)意味着t需要有确定的大小。也就是说,其支持的数据类型仅包括 int、double、float、char、指针。

C
#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展示一些参数包展开的常用情景

cpp
//可变参数模板参数包展开 例子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),这样就实现了打印函数。

cpp
//例子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是一个包含任意数量元素的列表,但是所有元素的类型必须相同。

cpp
#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;
}