进阶:可变参数函数

在C语言中,像 printfscanf 这样的函数可以接受可变数量的参数,这得益于可变参数函数(Variadic Functions)机制。 通过 <stdarg.h> 头文件中提供的宏,我们可以定义自己的可变参数函数。 可变参数函数在日志系统、格式化输出、通用计算器等场景中非常有用。本章将详细讲解可变参数函数的实现方法及注意事项。

📚 可变参数函数概述

可变参数函数是指参数个数和类型在编译时不确定的函数。在函数原型中,用省略号 ... 表示可变参数部分。

int printf(const char *format, ...);
int my_sum(int count, ...);   // 自定义可变参数函数
注意: 可变参数函数至少需要一个固定参数(如 printfformat),用于确定后续参数的数量和类型。

🔧 stdarg.h 中的宏

<stdarg.h> 定义了以下类型和宏来处理可变参数:

  • va_list:用于存储可变参数信息的类型。
  • va_start(ap, last_fixed):初始化 va_list 变量,last_fixed 是最后一个固定参数的名称。
  • va_arg(ap, type):获取下一个参数,type 是参数的类型。
  • va_end(ap):清理工作,结束可变参数的访问。
  • va_copy(dest, src)(C99):复制 va_list 对象。

📝 示例:计算多个整数的和

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

// 计算 n 个整数的和
int sum(int count, ...) {
    int total = 0;
    va_list args;
    va_start(args, count);   // 初始化,count是最后一个固定参数

    for (int i = 0; i < count; i++) {
        total += va_arg(args, int);   // 每次获取一个int参数
    }

    va_end(args);   // 清理
    return total;
}

int main() {
    printf("1+2+3 = %d\n", sum(3, 1, 2, 3));        // 6
    printf("1+2+3+4+5 = %d\n", sum(5, 1, 2, 3, 4, 5)); // 15
    return 0;
}
注意: 调用可变参数函数时,必须传递正确的参数个数和类型,否则行为未定义。编译器不会检查可变参数的类型匹配。

🎯 处理不同类型的可变参数

可变参数可以是不同类型,通常通过一个固定参数(如格式字符串)来指示后续参数的类型。

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

// 自定义格式化输出(简化版)
void my_printf(const char *format, ...) {
    va_list args;
    va_start(args, format);

    for (const char *p = format; *p != '\0'; p++) {
        if (*p == '%') {
            p++;
            switch (*p) {
                case 'd': {
                    int i = va_arg(args, int);
                    printf("%d", i);
                    break;
                }
                case 'f': {
                    double d = va_arg(args, double);
                    printf("%f", d);
                    break;
                }
                case 'c': {
                    // char 会被提升为 int
                    int c = va_arg(args, int);
                    putchar(c);
                    break;
                }
                case 's': {
                    char *s = va_arg(args, char*);
                    printf("%s", s);
                    break;
                }
                default:
                    putchar(*p);
            }
        } else {
            putchar(*p);
        }
    }

    va_end(args);
}

int main() {
    my_printf("整数: %d, 浮点数: %f, 字符串: %s\n", 42, 3.14159, "Hello");
    return 0;
}
重要: 在可变参数中,整数类型会进行默认提升:charshort 提升为 intfloat 提升为 double。 因此,va_arg(args, char) 是错误的,应使用 va_arg(args, int)

🔄 遍历所有参数的方法

除了通过固定参数指定数量,还可以使用哨兵值(如 NULL 或特殊值)来标记参数列表的结束。

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

// 以 -1 作为结束标记的求和函数
int sum_until_minus_one(int first, ...) {
    int total = first;
    va_list args;
    va_start(args, first);

    int n;
    while ((n = va_arg(args, int)) != -1) {
        total += n;
    }

    va_end(args);
    return total;
}

int main() {
    printf("总和: %d\n", sum_until_minus_one(10, 20, 30, -1));  // 60
    return 0;
}

⚠️ 可变参数函数的限制

  • 类型安全缺失:编译器不检查可变参数的类型,传错类型会导致未定义行为。
  • 无法直接获取参数数量:必须通过固定参数或哨兵值传递个数。
  • 参数默认提升:调用时 float 自动提升为 doublechar/short 提升为 int
  • 不适用于对象:C语言中不能传递结构体给可变参数(但可以传递指针)。
  • 性能开销:可变参数访问比普通参数慢。

📋 综合示例:简易日志系统

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

typedef enum {
    LOG_DEBUG,
    LOG_INFO,
    LOG_WARN,
    LOG_ERROR
} LogLevel;

const char* level_strings[] = {
    "DEBUG", "INFO", "WARN", "ERROR"
};

void log_message(LogLevel level, const char *file, int line, const char *format, ...) {
    // 获取当前时间
    time_t now = time(NULL);
    struct tm *tm_info = localtime(&now);
    char time_buf[20];
    strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S", tm_info);

    // 输出时间和日志级别
    fprintf(stderr, "[%s] [%s] [%s:%d] ", time_buf, level_strings[level], file, line);

    // 处理可变参数
    va_list args;
    va_start(args, format);
    vfprintf(stderr, format, args);
    va_end(args);

    fprintf(stderr, "\n");
}

// 宏简化调用,自动传入文件名和行号
#define LOG_DEBUG(...) log_message(LOG_DEBUG, __FILE__, __LINE__, __VA_ARGS__)
#define LOG_INFO(...)  log_message(LOG_INFO,  __FILE__, __LINE__, __VA_ARGS__)
#define LOG_WARN(...)  log_message(LOG_WARN,  __FILE__, __LINE__, __VA_ARGS__)
#define LOG_ERROR(...) log_message(LOG_ERROR, __FILE__, __LINE__, __VA_ARGS__)

int main() {
    int x = 42;
    double pi = 3.14159;

    LOG_DEBUG("调试信息: x = %d", x);
    LOG_INFO("程序启动成功");
    LOG_WARN("警告: pi 值 = %.2f", pi);
    LOG_ERROR("发生严重错误!");

    return 0;
}

⚠️ 常见错误

  • 忘记调用 va_end:可能导致资源泄漏或未定义行为。
  • 参数类型不匹配va_arg 指定的类型与实际参数类型不一致。
  • 未正确处理参数提升:例如尝试用 va_arg(args, char) 获取 char 参数。
  • 超出参数范围:调用 va_arg 次数超过实际参数数量。
  • 在无固定参数的函数中使用可变参数:这是C语言标准不允许的(C23之前)。

可变参数函数为C语言提供了极大的灵活性,但也带来了类型安全的风险。 在实际开发中,应谨慎使用,并确保调用者遵守约定的参数格式。掌握这一特性后,你可以编写出类似 printf 的强大函数。