一、可变参函数的构成
一个可变参函数必须满足两个基本结构:
- 固定参数 :至少有一个确定的参数(比如
printf的第一个参数const char *format),作为参数列表的 "锚点"; - 可变参数 :固定参数后用
...表示(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);
}
}