前置知识: C

可变参数函数

4 minIntermediate2026/6/14

stdarg.h与可变参数

概述

可变参数函数是C语言中一特殊的函数,它可以接受不确定数量的参数。最典型的例子就是标准库中的 printfscanf,它们的参数个数和型由格式字符串决定。C语言通过 <stdarg.h> 头文件提供了一套宏机制来支持可变参数的处理,使开发者能够自定义可变参数函数。

基础概念

可变参数的原理

在C语言中,函数参数按照从右到左的顺序压入栈中(取决于调用约定),因此只要知道第一个可变参数的地址,就可以依次访问后续参数。<stdarg.h> 中的宏正是基于这一原理实现的。

核心宏与

名称说明
va_list可变参数列表的型,本质上是一个指针
va_start初始化 va_list,使其指向第一个可变参数
va_arg获取当前参数并移动到下一个参数
va_end结束可变参数的遍历,执行清理工作
va_copy复制 va_list 的当前状态(C99新增)

函数声明语法

可变参数函数的声明中,固定参数至少要有一个,省略号 ... 必须放在参数列表的最后:

// 正确:至少一个固定参数,省略号在末尾
返回类型 函数名(固定参数, ...);

// 错误:省略号不能单独出现
返回类型 函数名(...);

快速上手

下面是一个最简单的可变参数函数,计算任意个数整数的和:

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

// count 表示后续可变参数的个数
int sum(int count, ...) {
    va_list args;          // 声明可变参数列表
    va_start(args, count); // 初始化,从 count 之后开始

    int total = 0;
    for (int i = 0; i < count; i++) {
        total += va_arg(args, int); // 逐个获取 int 类型的参数
    }

    va_end(args); // 结束遍历
    return total;
}

int main(void) {
    printf("%d\n", sum(3, 1, 2, 3));   // 输出: 6
    printf("%d\n", sum(5, 10, 20, 30, 40, 50)); // 输出: 150
    return 0;
}

详细用法

va_start 的使用

va_start 接受两个参数:va_list 变量和最后一个固定参数的名称。它将 va_list 定位到第一个可变参数的位置:

va_list ap;
va_start(ap, last_fixed_param); // last_fixed_param 是最后一个固定参数

va_arg 的使用

va_arg 接受两个参数:va_list 变量和期望的参数型。它返回当前参数的值,并将指针移动到下一个参数:

int val = va_arg(ap, int);       // 获取一个 int 类型的参数
double d = va_arg(ap, double);   // 获取一个 double 类型的参数
char *str = va_arg(ap, char *);  // 获取一个 char* 类型的参数

注意:va_arg 的第二个参数必须是实际传入参数的型,否则会导致未定义行为。

va_end 的使用

每次使用 va_start 之后,必须调用 va_end 进行清理。不调用 va_end 会导致未定义行为:

va_end(ap); // 必须在函数返回前调用

va_copy 的使用

va_copy 用于复制当前 va_list 的状态,适用于需要多次遍历可变参数的场景:

va_list ap, ap_copy;
va_start(ap, count);
va_copy(ap_copy, ap); // 复制当前状态

// 用 ap 遍历一次
for (int i = 0; i < count; i++) {
    int val = va_arg(ap, int);
}

// 用 ap_copy 再遍历一次
for (int i = 0; i < count; i++) {
    int val = va_arg(ap_copy, int);
}

va_end(ap);
va_end(ap_copy); // 每个 va_copy 都需要对应的 va_end

常见场景

场景一:实现自定义日志函数

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

// 自定义日志函数,支持格式化输出
void log_message(const char *level, const char *fmt, ...) {
    printf("[%s] ", level);

    va_list args;
    va_start(args, fmt);
    vprintf(fmt, args); // vprintf 接受 va_list 参数
    va_end(args);

    printf("\n");
}

int main(void) {
    log_message("INFO", "用户 %s 登录成功", "张三");
    log_message("WARN", "磁盘使用率达到 %d%%", 85);
    log_message("ERROR", "文件 %s 打开失败,错误码: %d", "data.txt", 2);
    return 0;
}

场景二:实现可变参数的求平均值函数

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

// 计算任意数量 double 的平均值
double average(int count, ...) {
    if (count <= 0) return 0.0;

    va_list args;
    va_start(args, count);

    double sum = 0.0;
    for (int i = 0; i < count; i++) {
        sum += va_arg(args, double); // 注意:float 会自动提升为 double
    }

    va_end(args);
    return sum / count;
}

int main(void) {
    printf("平均值: %.2f\n", average(4, 90.5, 85.0, 92.5, 78.0));
    return 0;
}

场景三:封装格式化字符串函数

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

// 将格式化字符串写入动态分配的内存
char *format_string(const char *fmt, ...) {
    va_list args;

    // 第一次调用:计算所需长度
    va_start(args, fmt);
    int len = vsnprintf(NULL, 0, fmt, args);
    va_end(args);

    if (len < 0) return NULL;

    // 分配内存
    char *buf = malloc(len + 1);
    if (!buf) return NULL;

    // 第二次调用:实际写入
    va_start(args, fmt);
    vsnprintf(buf, len + 1, fmt, args);
    va_end(args);

    return buf;
}

int main(void) {
    char *result = format_string("姓名: %s, 年龄: %d", "李四", 25);
    if (result) {
        printf("%s\n", result);
        free(result);
    }
    return 0;
}

注意事项

默认参数提升

在可变参数中,小于 int 的整型会提升为 intfloat 会提升为 double。因此 va_arg 中不能使用 charshortfloat

// 错误:char 和 short 会提升为 int
// char c = va_arg(ap, char);   // 未定义行为
// short s = va_arg(ap, short); // 未定义行为

// 正确:使用提升后的类型
int c = va_arg(ap, int);       // 传入的 char 会被提升为 int
double f = va_arg(ap, double); // 传入的 float 会被提升为 double

缺少参数个数信息

可变参数函数本身无法知道传入了多少个参数,必须通过其他方式告知:

  • 使用固定参数指定个数(如 printf 的格式字符串中 %d 的数量)
  • 使用哨兵值标记结束(如以 NULL 结尾的指针列表)
// 方式一:用固定参数指定个数
void func(int count, ...);

// 方式二:用哨兵值标记结束
void print_strings(const char *first, ...) {
    va_list ap;
    va_start(ap, first);
    const char *str = first;
    while (str != NULL) { // NULL 作为结束标记
        printf("%s\n", str);
        str = va_arg(ap, const char *);
    }
    va_end(ap);
}

型不匹配的危险

如果 va_arg 指定的型与实际传入的型不一致,会导致未定义行为,编译器通常不会报错:

// 危险:传入 int 但按 double 读取
int sum(int count, ...) {
    va_list ap;
    va_start(ap, count);
    // 如果调用者传入 int,但这里按 double 读取,结果不可预测
    double val = va_arg(ap, double); // 错误!
    va_end(ap);
    return (int)val;
}

va_start 和 va_end 必须成对出现

每次 va_start 必须有对应的 va_end,否则可能导致栈损坏或内存泄漏

进阶用法

实现简易 printf

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

// 简易版 printf,仅支持 %d、%s、%c、%f
void my_printf(const char *fmt, ...) {
    va_list ap;
    va_start(ap, fmt);

    while (*fmt) {
        if (*fmt == '%') {
            fmt++; // 跳过 %
            switch (*fmt) {
                case 'd': {
                    int val = va_arg(ap, int);
                    printf("%d", val);
                    break;
                }
                case 's': {
                    const char *str = va_arg(ap, const char *);
                    if (str) printf("%s", str);
                    else printf("(null)");
                    break;
                }
                case 'c': {
                    // char 提升为 int
                    int ch = va_arg(ap, int);
                    putchar(ch);
                    break;
                }
                case 'f': {
                    double val = va_arg(ap, double);
                    printf("%f", val);
                    break;
                }
                case '%':
                    putchar('%');
                    break;
                default:
                    putchar('%');
                    putchar(*fmt);
                    break;
            }
        } else {
            putchar(*fmt);
        }
        fmt++;
    }

    va_end(ap);
}

int main(void) {
    my_printf("姓名: %s, 年龄: %d, 成绩: %f\n", "王五", 20, 95.5);
    return 0;
}

可变参数与函数指针结合

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

// 定义可变参数的函数指针类型
typedef void (*log_func_t)(const char *fmt, ...);

// 日志实现
void debug_log(const char *fmt, ...) {
    va_list ap;
    va_start(ap, fmt);
    printf("[DEBUG] ");
    vprintf(fmt, ap);
    printf("\n");
    va_end(ap);
}

void error_log(const char *fmt, ...) {
    va_list ap;
    va_start(ap, fmt);
    printf("[ERROR] ");
    vprintf(fmt, ap);
    printf("\n");
    va_end(ap);
}

int main(void) {
    // 通过函数指针调用不同的日志函数
    log_func_t logger = debug_log;
    logger("变量 x = %d", 42);

    logger = error_log;
    logger("文件打开失败: %s", "config.txt");
    return 0;
}

使用 va_copy 实现多次遍历

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

// 先求和再求平均值,需要遍历两次
double sum_and_avg(int count, double *out_sum, ...) {
    va_list ap, ap_copy;
    double sum = 0.0;

    // 第一次遍历:求和
    va_start(ap, count);
    va_copy(ap_copy, ap);

    for (int i = 0; i < count; i++) {
        sum += va_arg(ap, double);
    }
    va_end(ap);

    // 第二次遍历:使用副本
    // 这里演示 va_copy 的用法,实际场景中只需用 sum/count 即可
    double sum2 = 0.0;
    for (int i = 0; i < count; i++) {
        sum2 += va_arg(ap_copy, double);
    }
    va_end(ap_copy);

    *out_sum = sum;
    return sum / count;
}

int main(void) {
    double total;
    double avg = sum_and_avg(&total, 3, 80.0, 90.0, 100.0);
    printf("总分: %.1f, 平均: %.1f\n", total, avg);
    return 0;
}