可变参函数底层解析

一、可变参函数的构成

一个可变参函数必须满足两个基本结构:

  1. 固定参数 :至少有一个确定的参数(比如 printf 的第一个参数 const char *format),作为参数列表的 "锚点";
  2. 可变参数 :固定参数后用 ... 表示(C 语言标准语法),代表后续参数数量 / 类型不确定。

二、实现原理

C 语言通过 <stdarg.h> 头文件提供的来解析可变参数列表,这些宏是对编译器底层参数传递规则(栈布局)的封装。

先明确一个前提:函数调用时,参数会按照从右到左 的顺序压入栈中(大部分架构,如 ARM/STM32),栈是 "向下生长" 的(高地址→低地址)。比如调用 func(1, 2.5, "abc"),栈布局大概是:

plaintext

复制代码
高地址 →  "abc" (char*)
          2.5   (double)
          1     (int)
低地址 →  函数返回地址

<stdarg.h> 提供了 3 个核心宏,是实现可变参的关键:

作用
va_list 定义一个 "参数指针",用于遍历栈中的可变参数
va_start 初始化 va_list,让它指向第一个可变参数的地址
va_arg va_list 指向的位置读取一个参数(需指定类型),并移动指针到下一个
va_end 结束可变参数的遍历(释放 / 重置 va_list,部分架构是空宏,但必须写)

三、自定义可变参函数

先从简单的例子入手,帮你理解每个宏的用法,再结合 MCU 场景(比如自定义日志函数)。

示例 1:基础版 ------ 求和函数(多个 int 参数)
复制代码
#include <stdio.h>
#include <stdarg.h>  // 必须包含这个头文件

/**
 * @brief 可变参求和函数
 * @param n: 固定参数,指定后续可变参数的个数
 * @param ...: 可变参数(int类型)
 * @return 所有参数的和
 */
int sum(int n, ...)
{
    int total = 0;
    va_list args;  // 1. 定义参数指针
    
    // 2. 初始化:让args指向第一个可变参数(n是固定参数,作为锚点)
    va_start(args, n);
    
    // 3. 遍历所有可变参数
    for (int i = 0; i < n; i++)
    {
        // va_arg(参数指针, 参数类型):读取当前参数,并移动指针
        total += va_arg(args, int);
    }
    
    // 4. 结束遍历(必须调用)
    va_end(args);
    
    return total;
}

// 测试
int main(void)
{
    // 调用:n=3,后续3个int参数
    int s1 = sum(3, 10, 20, 30);  // 结果:60
    // n=2,后续2个int参数
    int s2 = sum(2, 5, 8);        // 结果:13
    
    printf("sum(3,10,20,30)=%d\n", s1);
    printf("sum(2,5,8)=%d\n", s2);
    
    return 0;
}
示例 2:进阶版 ------ 自定义 MCU 日志函数(模拟 printf 核心逻辑)
复制代码
#include <stdio.h>
#include <stdarg.h>
#include "stm32f1xx_hal.h"

// 假设已完成UART初始化,huart1是全局句柄
extern UART_HandleTypeDef huart1;

// 底层字符发送函数(之前重定向fputc用的)
void uart_putchar(char ch)
{
    while(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_TXE) == RESET);
    huart1.Instance->DR = (uint8_t)ch;
    if(ch == '\n')
    {
        while(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_TXE) == RESET);
        huart1.Instance->DR = '\r';
    }
}

// 自定义可变参日志函数(支持%d、%s)
void my_printf(const char *format, ...)
{
    va_list args;
    va_start(args, format);  // 以format为锚点,初始化参数指针
    
    // 遍历格式化字符串
    while (*format != '\0')
    {
        if (*format == '%')  // 遇到格式符
        {
            format++;  // 跳过%,读取后续的类型符
            switch (*format)
            {
                case 'd':  // 处理整数
                {
                    int num = va_arg(args, int);
                    // 简单实现:将整数转为字符串并发送(仅处理正数,新手版)
                    char buf[16] = {0};
                    sprintf(buf, "%d", num);  // 临时用sprintf转字符串
                    for (int i=0; buf[i]!='\0'; i++)
                    {
                        uart_putchar(buf[i]);
                    }
                    break;
                }
                case 's':  // 处理字符串
                {
                    char *str = va_arg(args, char*);
                    for (int i=0; str[i]!='\0'; i++)
                    {
                        uart_putchar(str[i]);
                    }
                    break;
                }
                default:  // 其他字符直接输出
                    uart_putchar(*format);
                    break;
            }
        }
        else  // 普通字符直接发送
        {
            uart_putchar(*format);
        }
        format++;  // 移动到下一个字符
    }
    
    va_end(args);
}

// 测试(MCU中使用)
int main(void)
{
    HAL_Init();
    MX_USART1_UART_Init();
    
    // 调用自定义可变参函数
    my_printf("自定义日志测试:num=%d, str=%s\n", 123, "hello MCU");
    
    while(1)
    {
        my_printf("循环输出:count=%d\n", 666);
        HAL_Delay(1000);
    }
}
相关推荐
superkcl20222 小时前
指针常量有什么用呢?
开发语言·c++·算法
v先v关v住v获v取2 小时前
ZG-6右箱体双面钻专用机床右主轴箱设计1张总装图+零件图cad+设计说明书
科技·单片机·51单片机
Yungoal2 小时前
C++基础语法3
开发语言·c++
6+h2 小时前
【java IO】缓冲流详解
java·开发语言
爱丽_2 小时前
方法区 / 元空间:JDK 1.7 到 JDK 1.8 到底变了什么?
java·开发语言
xjdkxnhcoskxbco2 小时前
Java 多线程“八锁”问题深度解析
java·开发语言·多线程
人还是要有梦想的2 小时前
QT的起源
开发语言·qt
柏箱2 小时前
文件上传漏洞入门:(upload-labs Pass-1 & Pass-2)
开发语言·前端·javascript
人道领域2 小时前
Day | 07 【苍穹外卖:菜品套餐的缓存】
java·开发语言·redis·缓存击穿·springcache