目录
[1. 前言:为什么需要可变参数?](#1. 前言:为什么需要可变参数?)
[2. 可变参数核心原理(stdarg.h)](#2. 可变参数核心原理(stdarg.h))
[3. 实战:实现 logger_print 风格的日志函数](#3. 实战:实现 logger_print 风格的日志函数)
[3.1 需求分析](#3.1 需求分析)
[3.2 完整实现(代码同前文示例)](#3.2 完整实现(代码同前文示例))
[3.3 核心细节解析](#3.3 核心细节解析)
[(2)v 系列函数的使用](#(2)v 系列函数的使用)
[(3)调用示例(同前文 main 函数示例)](#(3)调用示例(同前文 main 函数示例))
[4.1 常见坑点](#4.1 常见坑点)
[4.2 避坑总结](#4.2 避坑总结)
1. 前言:为什么需要可变参数?
在嵌入式开发、日志系统、通用工具函数中,我们经常需要处理 "参数个数不确定" 的场景:比如日志打印函数,有时只打印一句话,有时需要拼接整数、字符串、浮点数等。如果为每种参数组合写一个函数,会导致代码冗余且难以维护。
C 语言的可变参数(...)正是为解决这个问题而生,比如标准库的 printf(const char* fmt, ...)、本文提到的 logger_print,都是可变参数的经典应用。
2. 可变参数核心原理(stdarg.h)
C 语言本身不直接支持可变参数,而是通过 <stdarg.h> 提供的宏封装实现,核心宏说明:
| 宏 | 作用 |
|---|---|
va_list |
定义可变参数列表类型的变量(本质是字符指针,指向栈上的可变参数起始地址) |
va_start |
初始化 va_list,绑定最后一个固定参数(锚点),定位可变参数的起始位置 |
va_arg |
按指定类型提取下一个可变参数,同时移动指针到下一个参数 |
va_end |
清理 va_list,释放资源(部分编译器中是空宏,但必须调用) |
注意:可变参数必须跟在固定参数之后,且函数参数列表中至少有一个固定参数(如 logger_print 中的 fmt),否则 va_start 无法定位。
3. 实战:实现 logger_print 风格的日志函数
3.1 需求分析
我们需要实现一个通用日志函数,满足:
- 支持自定义日志级别(DEBUG/INFO/WARN/ERROR);
- 支持模块名标识;
- 支持任意格式的参数拼接(整数、字符串、浮点数等);
- 接口风格匹配
logger_print(logger_t* logger, int level, const char* fmt, ...)。
3.2 完整实现(代码同前文示例)
(此处粘贴前文的完整示例代码,重点标注关键步骤)
3.3 核心细节解析
(1)参数合法性校验
必须先校验固定参数(如 logger、fmt)是否为空,避免空指针访问崩溃:
(2)v 系列函数的使用
普通的 printf 无法直接处理 va_list,需使用对应的 v 系列函数:
| 普通函数 | 可变参数版本 | 用途 |
|---|---|---|
| printf | vprintf | 标准输出 |
| sprintf | vsprintf | 字符串拼接(有缓冲区溢出风险) |
| snprintf | vsnprintf | 安全的字符串拼接(指定缓冲区大 |
(3)调用示例(同前文 main 函数示例)
4.1 常见坑点
(1)类型不匹配
va_arg(args, 类型) 必须严格匹配实际参数类型,比如:
// 错误:传入float,但va_arg取int(float会被提升为double) logger_print(&logger, LOG_INFO, "温度:%f", 25.5); // 实际传入double,va_arg需用double
避坑:float 会自动提升为 double,char/short 会提升为 int,需按提升后的类型取值。
(2)缓冲区溢出
使用 vsprintf 时,若缓冲区大小不足,会导致内存越界:避坑 :优先使用 vsnprintf,指定缓冲区最大长度。
(3)无固定参数锚点
错误写法:int bad_func(...) { ... }避坑:可变参数必须跟在固定参数之后,且 va_start 必须绑定最后一个固定参数。
4.2 避坑总结
- 始终校验固定参数的合法性;
- 优先使用安全的 v 系列函数(如 vsnprintf);
- 严格匹配 va_arg 的参数类型;
- 可变参数列表必须以 va_end 收尾。
代码示例
cpp
#include <stdio.h>
#include <stdarg.h> // 必须包含的可变参数头文件
// 日志级别定义
typedef enum {
LOG_DEBUG = 0,
LOG_INFO,
LOG_WARN,
LOG_ERROR
} log_level_t;
// 自定义logger结构体(模拟logger_t)
typedef struct {
int enable; // 日志使能标志
const char* module; // 日志所属模块
} logger_t;
/**
* @brief 模仿logger_print实现可变参数日志打印
* @param logger 日志器对象
* @param level 日志级别
* @param fmt 格式化字符串(固定参数,作为va_start的锚点)
* @param ... 可变参数(匹配fmt中的占位符)
* @return 打印的字符数
*/
int logger_print(logger_t* logger, int level, const char* fmt, ...) {
// 1. 入参合法性校验
if (logger == NULL || !logger->enable || fmt == NULL) {
return -1;
}
// 2. 定义可变参数列表变量
va_list args;
// 3. 初始化参数列表:绑定最后一个固定参数fmt
va_start(args, fmt);
// 4. 拼接日志前缀(级别+模块)
const char* level_str[] = {"DEBUG", "INFO", "WARN", "ERROR"};
printf("[%s][%s] ", level_str[level], logger->module);
// 5. 处理可变参数:vprintf接收va_list(替代printf)
int ret = vprintf(fmt, args);
// 6. 释放参数列表资源(必须调用)
va_end(args);
// 补换行
printf("\n");
return ret;
}
// 应用示例
int main() {
// 初始化日志器
logger_t app_logger = {
.enable = 1,
.module = "APP_MAIN"
};
// 场景1:打印简单信息(无可变参数)
logger_print(&app_logger, LOG_INFO, "程序启动成功");
// 场景2:打印带整数的日志(1个可变参数)
int port = 8080;
logger_print(&app_logger, LOG_DEBUG, "服务监听端口:%d", port);
// 场景3:打印多类型参数(多个可变参数)
const char* ip = "192.168.1.100";
float temp = 25.5f;
logger_print(&app_logger, LOG_WARN, "设备[%s]温度异常:%.1f℃", ip, temp);
// 场景4:打印错误信息(配合errno)
int err_code = -1;
logger_print(&app_logger, LOG_ERROR, "文件读取失败,错误码:%d,描述:%s",
err_code, "权限不足");
return 0;
}