欢迎光临
我们一直在努力

C语言可变参数

目录

  • 1. 什么是可变参数
    • 1.1.printf
    • 1.2 scanf
    • 1.3 sprintf
      • 补充 snprintf
  • 2. 核心头文件与标准宏
    • 2.1 可变参数的类型
    • 2.2 核心宏
  • 3. 实现原理
    • 3.1 参数压栈规则(cdecl调用约定)
    • 3.2 宏的底层实现逻辑
  • 4. 使用方法与示例
    • 4.1 使用步骤
      • 示例:固定类型的求和函数
  • 5. 关键特性与进阶用法
    • 5.1 默认参数提升
    • 5.2 标准v系列格式化函数
    • 5.3 多次遍历参数:va_copy

1. 什么是可变参数

可变参数函数(Variadic Function)是指参数的数量、类型可以在调用时动态变化的函数。最典型的例子是标准库中的 printf、scanf、sprintf 等格式化函数,以及 execl 等系统调用。

1.1.printf

int printf(const char *format, ...);

1.2 scanf

int scanf(const char *format, ...);

1.3 sprintf

int sprintf(char *str, const char *format, ...);

  • 功能:把格式化字符串输出到字符缓冲区 str(内存字符串,非屏幕)
  • 返回值:生成字符串的字符数(不含'\\0');出错返回负数

不做缓冲区越界检查,优先用 snprintf

补充 snprintf

int snprintf(char *str, size_t size, const char *format, ...);

  • size:缓冲区总大小,最多写入 size‑1 个字符,末尾自动补\\0 用法。

2. 核心头文件与标准宏

C标准规定,可变参数的实现依赖 <stdarg.h> 头文件,其中定义了一个专用类型和4个核心宏(C99新增va_copy)。

2.1 可变参数的类型

va_list 是用于保存可变参数列表状态的类型,本质上是一个指向栈内存的指针,用来遍历栈上的可变参数。

2.2 核心宏

宏作用参数说明
va_start(ap, last) 初始化可变参数列表 ap:va_list 类型变量;last:函数最后一个固定参数的名称
va_arg(ap, type) 获取下一个可变参数 ap:va_list 变量;type:当前参数的类型(如 int、double、char*)
va_end(ap) 清理可变参数列表 ap:va_list 变量,使用完毕必须调用
va_copy(dest, src) 复制可变参数列表状态(C99新增) dest:目标 va_list;src:源 va_list

3. 实现原理

要理解可变参数的本质,需要先了解C语言函数的栈帧结构和调用约定。

3.1 参数压栈规则(cdecl调用约定)

在x86等主流架构下,C语言默认使用 cdecl 调用约定:

  • 函数参数从右往左依次压入栈中;
  • 栈内存从高地址向低地址生长(栈顶指向低地址);
  • 由调用者负责栈空间的清理(被调用者不需要知道参数总数)。

举个例子,调用 func(1, 2.5, "hello") 时,栈上的参数布局(从低地址到高地址):

低地址 ← 栈顶方向 高地址
+——–+——–+——–+
| 1 | 2.5 | "hello"|
+——–+——–+——–+
第一个参数 最后一个参数

最左侧的固定参数地址最低,越靠右的参数地址越高,可变参数全部位于固定参数的右侧(高地址方向)。

3.2 宏的底层实现逻辑

基于栈布局,<stdarg.h> 的宏本质就是通过最后一个固定参数的地址,逐步偏移遍历后续参数。

// 按int大小向上对齐,保证栈地址对齐
#define _INTSIZEOF(n) ((sizeof(n) + sizeof(int) 1) & ~(sizeof(int) 1))

typedef char* va_list;

// 初始化:指向最后一个固定参数的下一个位置(第一个可变参数)
#define va_start(ap, last) (ap = (va_list)&last + _INTSIZEOF(last))

// 取参数:先偏移到下一个参数,再返回当前参数的值
#define va_arg(ap, type) (*(type*)((ap += _INTSIZEOF(type)) _INTSIZEOF(type)))

// 清理:置空指针
#define va_end(ap) (ap = (va_list)0)

// 复制:直接拷贝指针状态
#define va_copy(dest, src) (dest = src)

注意:实际标准库的实现会因架构、编译器不同而有所差异,切勿手动计算地址来遍历参数,务必使用标准宏以保证代码的可移植性。

4. 使用方法与示例

使用可变参数函数必须遵循固定的步骤,且函数必须满足两个前提:

  • 至少有一个固定参数(用于 va_start 定位可变参数起始位置);
  • … 必须放在参数列表的最后。
  • 4.1 使用步骤

  • 定义函数:固定参数 + … 结尾;
  • 函数内部定义 va_list 类型变量;
  • 调用 va_start 初始化参数列表;
  • 循环调用 va_arg 依次获取每个参数;
  • 调用 va_end 清理参数列表。
  • 示例:固定类型的求和函数

    第一个参数为参数个数,后续为若干个 int 类型参数,返回它们的和。

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

    // count:可变参数的个数,后续为n个int
    int sum(int count, ...) {
    va_list ap; // 定义参数列表变量
    int total = 0;

    va_start(ap, count); // 初始化,以count为基准定位可变参数

    for (int i = 0; i < count; i++) {
    // 依次取出int类型的参数
    total += va_arg(ap, int);
    }

    va_end(ap); // 清理,必须调用
    return total;
    }

    int main() {
    printf("3个数求和:%d\\n", sum(3, 10, 20, 30)); // 输出60
    printf("5个数求和:%d\\n", sum(5, 1, 2, 3, 4, 5)); // 输出15
    return 0;
    }

    5. 关键特性与进阶用法

    5.1 默认参数提升

    在可变参数列表中,会发生默认参数提升

    • 所有小于 int 的整型类型(char、signed char、unsigned char、short、unsigned short)都会被提升为 int;
    • float 类型会被提升为 double。

    因此,绝对不能用 va_arg 直接获取 char、short、float 类型,必须使用提升后的类型 int、double

    错误示例:

    char c = va_arg(ap, char); // 错误!char被提升为int
    float f = va_arg(ap, float); // 错误!float被提升为double

    正确示例:

    char c = (char)va_arg(ap, int); // 用int接收后强转
    double f = va_arg(ap, double); // 直接用double

    这也是为什么 printf 中 %f 对应 double,传入 float 也能正常工作的原因。

    5.2 标准v系列格式化函数

    C标准库提供了一组以 v 开头的格式化函数,专门接收 va_list 类型参数,用于封装自定义可变参数函数,避免重复实现格式解析逻辑:

    • vprintf:输出到标准输出
    • vfprintf:输出到指定文件流
    • vsprintf:输出到字符串缓冲区
    • vsnprintf:安全版字符串输出(带长度限制)

    示例:封装一个带时间戳的日志函数

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

    void log_info(const char *fmt, ...) {
    va_list ap;
    va_start(ap, fmt);

    // 先打印时间戳
    time_t t = time(NULL);
    struct tm *tm_info = localtime(&t);
    printf("[%04d-%02d-%02d %02d:%02d:%02d] [INFO] ",
    tm_info->tm_year + 1900, tm_info->tm_mon + 1, tm_info->tm_mday,
    tm_info->tm_hour, tm_info->tm_min, tm_info->tm_sec);

    // 直接调用vprintf输出格式化内容
    vprintf(fmt, ap);

    va_end(ap);
    }

    int main() {
    log_info("用户%s登录成功,ID:%d\\n", "admin", 1001);
    return 0;
    }

    5.3 多次遍历参数:va_copy

    如果需要多次遍历同一个可变参数列表,不能重复使用同一个 va_list 直接从头遍历,也不能多次调用 va_start。正确做法是用 va_copy 复制一份状态。

    示例:先打印所有参数,再求平均值

    double avg_and_print(int count, ...) {
    va_list ap_read, ap_copy;
    double sum = 0;

    va_start(ap_read, count);
    va_copy(ap_copy, ap_read); // 复制当前状态到ap_copy

    // 第一次遍历:打印
    printf("参数列表:");
    for (int i = 0; i < count; i++) {
    int val = va_arg(ap_read, int);
    printf("%d ", val);
    }
    printf("\\n");

    // 第二次遍历:求和
    for (int i = 0; i < count; i++) {
    sum += va_arg(ap_copy, int);
    }

    va_end(ap_read);
    va_end(ap_copy); // 复制的va_list也必须调用va_end

    return sum / count;
    }

    赞(0)
    未经允许不得转载:171主机测评 » C语言可变参数
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址