关于 C/C++ 可变参数函数的学习笔记

在编程的世界里,灵活性和效率往往是我们追求的目标。当涉及到函数设计时,能够处理不同数量参数的能力极大地增加了代码的复用性和适应性。C/C++ 语言通过提供可变参数函数(variadic functions)这一特性,允许开发者创建能够接收不定数量参数的函数,从而满足了这一需求。

在计算机程序设计,一个可变参数函数是指一个函数拥有不定引数,即是它接受一个可变数目的参数。简单来说,就是函数的参数个数可变,参数类型不定的函数。

不同的编程语言对可变参数函数的支持有很大差异。

——引用:《可变参数函数_百度百科

那么,为什么需要可变参数函数?

在实际开发中,我们经常会遇到一些场景,其中函数调用所需的参数数量不是固定的。例如,格式化输出函数如 printf 可以根据提供的格式字符串来决定需要多少个额外参数。为了应对这种不确定性,C/C++ 提供了专门的机制来定义和使用可变参数函数,使得编写更加通用和灵活的代码成为可能。

本文将详细讲解每个宏的作用,演示一个简单的可变参数函数的实现,并讨论在使用过程中应当注意的问题。此外,还将比较 C 和 C++ 在这方面的差异,以及探索更现代的方法,比如模板和重载,在某些情况下可以作为传统可变参数函数的替代方案。

C/C++ 中的可变参数函数基础

C/C++ 语言中的可变参数函数主要依赖于几个宏和类型定义,它们共同构成了处理可变参数的核心。这些工具包括 va_listva_startva_arg 以及 va_end,全部定义在 <cstdarg>(对于 C++)或 <stdarg.h>(对于 C)头文件中。通过合理地运用这些宏,我们可以安全有效地遍历传递给函数的所有参数,无论其数量如何。

  • va_list:用于声明一个指向可变参数列表的类型。
  • va_start:初始化 va_list 变量,使其指向第一个可变参数。
  • va_arg:从可变参数列表中提取下一个参数,并指定其类型。
  • va_end:清理 va_list 变量,结束对可变参数列表的操作。

示例一

下面是一个简单的例子,展示了如何使用这些宏来实现一个求平均值的函数:

扫描二维码关注公众号,回复: 17582928 查看本文章
#include <stdio.h>
#include <stdarg.h>    // 引入可变参数函数相关头文件

double average(int n_values, ...)
{
    
    
    va_list var_arg;	// 声明一个指向可变参数列表的变量
    int count;
    double sum = 0;

    va_start(var_arg, n_values);    // 初始化可变参数列表,并指向第一个可变参数

    for (count = 0; count < n_values; count++)
        sum += va_arg(var_arg, int);    // 从可变参数列表中提取下一个参数,并指定为int类型

    va_end(var_arg);    // 清理可变参数列表,并结束对可变参数列表的操作

    return n_values > 0 ? sum / n_values : 0;
}

int main()
{
    
    
    printf("Average: %f\n", average(4, 1, 2, 3, 4));
    return 0;
}

执行结果如下:

grayson@Zheng:~$ vim average.c
grayson@Zheng:~$ gcc average.c -o average
grayson@Zheng:~$ ./average 
Average: 2.500000
grayson@Zheng:~$

[!NOTE]

虽然可变参数函数提供了很大的灵活性,但也有一些问题需要注意。首先,由于编译器不会检查传递给可变参数函数的实际参数类型,因此容易导致运行时错误。其次,可变参数函数的类型检查比较困难,这也可能导致未定义行为。因此,在设计和调用可变参数函数时,务必小心谨慎,确保参数的类型和数量匹配。

示例二

该例子定义了一个名为 print_all 的函数,它可以接受不同类型的参数,并根据提供的信息打印出这些参数的值。为了简化问题,我们假设所有传递给 print_all 的参数都是整数、浮点数或字符之一,并且每个参数前面都有一个字符来表示其类型(‘i’ 代表整数,‘f’ 代表浮点数,‘c’ 代表字符)。

#include <stdio.h>
#include <stdarg.h>

void print_all(const char *fmt, ...)
{
    
    
    va_list args;
    va_start(args, fmt);

    for (const char *p = fmt; *p != '\0'; ++p) {
    
    
        switch (*p) {
    
    
            case 'i': // Integer
                printf("Integer: %d\n", va_arg(args, int));
                break;
            case 'f': // Float
                printf("Float: %f\n", va_arg(args, double)); // float 参数会被提升为 double
                break;
            case 'c': // Character
                printf("Character: %c\n", (char)va_arg(args, int)); // char 参数也会被提升为 int
                break;
            default:
                printf("Unknown type: %c\n", *p);
                break;
        }
    }

    va_end(args);
}

int main()
{
    
    
    print_all("ifiic", 10, 3.14, 20, 'A', 'B');
    return 0;
}

在该例子中需要注意的是:

  1. print_all 函数的第一个参数是一个字符串 fmt,它描述了随后传入的每一个参数的类型。
  2. 使用 va_list 类型的变量 args 来存储和遍历可变参数列表。
  3. va_start 宏初始化 args,使其指向第一个可变参数。
  4. 在循环体内,通过检查 fmt 字符串中的字符来确定下一个参数的类型,并使用 va_arg 宏提取相应的参数。注意,对于 float 类型的参数,它们实际上是以 double 的形式存储在栈上,因此这里使用 va_arg(args, double) 来获取正确的值。同样地,char 类型的参数也会被提升为 int,所以我们用 (char)va_arg(args, int) 来正确地读取字符。
  5. 最后,va_end 宏用于清理 args,结束对可变参数列表的操作。

执行结果如下:

Integer: 10
Float: 3.140000
Integer: 20
Character: A
Character: B

C 与 C++ 的差异

在 C 语言中,可变参数函数是通过宏来实现的,而在 C++ 中除了可以继续使用这些宏之外,还引入了更现代的技术,如 initializer_list 和可变模板参数。特别是 C++11 引入的可变模板参数,它允许函数模板接受任意数量和类型的参数,进一步增强了代码的灵活性和安全性。

  • initializer_list:可以用来处理同类型的多个参数。
  • 可变模板参数:可以处理不同类型的多个参数,提供了更高的泛化能力。

例如,使用可变模板参数可以实现一个递归展开的打印函数:

#include <iostream>

template<typename T>
void print(T value)
{
    
    
    std::cout << value << std::endl;
}

template<typename T, typename... Args>
void print(T value, Args... args)
{
    
    
    std::cout << value << " ";
    print(args...);
}

int main()
{
    
    
    print(1, "two", 3.0); // 输出: 1 two 3
    return 0;
}

执行结果如下:

grayson@Zheng:~$ vim temp.cpp
grayson@Zheng:~$ g++ temp.cpp -o temp
grayson@Zheng:~$ ./temp 
1 two 3
grayson@Zheng:~$