C 语言可变参数(...)实战:从 logger_print 到通用日志函数

目录

[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 核心细节解析)

(1)参数合法性校验

[(2)v 系列函数的使用](#(2)v 系列函数的使用)

[(3)调用示例(同前文 main 函数示例)](#(3)调用示例(同前文 main 函数示例))

[4.1 常见坑点](#4.1 常见坑点)

(1)类型不匹配

(2)缓冲区溢出

(3)无固定参数锚点

[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 避坑总结

  1. 始终校验固定参数的合法性;
  2. 优先使用安全的 v 系列函数(如 vsnprintf);
  3. 严格匹配 va_arg 的参数类型;
  4. 可变参数列表必须以 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;
}
相关推荐
Larry_Yanan2 小时前
Qt多进程(一)进程间通信概括
开发语言·c++·qt·学习
superman超哥2 小时前
仓颉语言中基本数据类型的深度剖析与工程实践
c语言·开发语言·python·算法·仓颉
不爱吃糖的程序媛2 小时前
Ascend C开发工具包(asc-devkit)技术解读
c语言·开发语言
bu_shuo2 小时前
MATLAB奔溃记录
开发语言·matlab
你的冰西瓜3 小时前
C++标准模板库(STL)全面解析
开发语言·c++·stl
李绍熹3 小时前
c语言字符串操作示例
c语言
徐先生 @_@|||3 小时前
(Wheel 格式) Python 的标准分发格式的生成规则规范
开发语言·python
利剑 -~3 小时前
jdk源码解析
java·开发语言
Damon_X4 小时前
extern “C“
c语言