在 c/c++ 中,函数是支持可变参数的,最典型的就是 printf()
函数,为了支持可变参数,函数参数的入栈顺序默认是从右往左的,即最后一个参数位于高地址,第一个参数位于低地址。
一、相关宏
1、va_list 类型变量
该变量类型是一个宏定义,本质上是一个char*
指针类型的变量,这个指针指向下一个参数的地址。
typedef char* va_list;
2、va_start() 宏
void va_start(va_list ap, last_arg)
作用
初始化参数列表 ap,确定第一个不可变参数的位置(也就是第二个可变参数地址)
参数
- ap:参数列表。
- last_arg:传入最后一个入栈的参数(也就是第一个参数)。
3、va_arg() 宏
type va_arg(va_list ap, type)
作用
检索函数参数列表中类型为 type 的下一个参数。它无法判断检索到的参数是否是传给函数的最后一个参数。
参数
- ap:参数列表。
- type:下一个参数的类型
4、va_end() 宏
void va_end(va_list ap)
作用
取完所有参数并从函数返回之前,必须调用 va_end(),由此确保堆栈的正确恢复。如果未正确使用 va_end(),程序可能瘫痪。
参数
扫描二维码关注公众号,回复:
14811206 查看本文章
- ap:参数列表。
5、如何获取可变参数的个数/类型
因为 va_arg()
宏不能获知获取到的下一个参数是否为可变参数中的最后一个,所以如果想要获取到,可以通过传入的参数中获得。
- 事先约定(过于死板,差不多变成普通函数了,不推荐)
- 对于可变参数不为同一类型的:可以将第一个参数设置成
char*
,在里面约定好后续变量的个数以及类型。比如my_printf(“cdfs”, 'a', 10, 9.5, "hello world");
,其中c
代表第二个参数为 char 类型,d
表示第三个参数为 int 类型,f
表示第四个参数为 float 类型,s
表示第五个参数为 char* 类型。 - 对于可变参数为同一类型的:一种是在第一个参数中传入后续参数的个数。比如
add(3, 5, 8, -2);
,第一个参数 3 代表后面的可变参数有 3 个。另一种是在最后一个参数中传入一个截止元素。比如combine("hello", "world", NULL);
,当va_arg()
宏读取到的元素为 NULL 时,就知道该元素是最后一个了,无需继续进行读取。
6、例子
#include <stdio.h>
#include <stdarg.h>
int adds(char *a, ...)
{
va_list args;
va_start(args, a);
printf("第 1 个参数 a 的值:%s\n", a);
printf("第 1 个参数 a 的地址:%p\n", &a);
printf("此时 args 的地址:%p\n", args);
printf("args 所指向的内容:%d\n", *((int*)args));
printf("=======================================\n");
for (int i = 0; i < 2; i++)
{
int j = va_arg(args, int);
printf("第 %d 个参数:%d\n", i + 2, j);
printf("此时 args 的地址:%p\n", args);
printf("args 所指向的内容:%d\n", *((int*)args));
printf("=======================================\n");
}
va_end(args);
}
int main()
{
adds("hello world", 25, 32);
}
输出结果:
第 1 个参数 a 的值:hello world
第 1 个参数 a 的地址:0000005911D1F9E0
此时 args 的地址:0000005911D1F9E8
args 所指向的内容:25
=======================================
第 2 个参数:25
此时 args 的地址:0000005911D1F9F0
args 所指向的内容:32
=======================================
第 3 个参数:32
此时 args 的地址:0000005911D1F9F8
args 所指向的内容:298973088
=======================================
二、已有的可变参数底层函数
1、vprintf() 函数
是 printf() 底层函数。
/*
*描述:将可变参数列表的格式化数据打印到 stdout
*
*参数:
* format 包含格式字符串的 C 字符串,其格式字符串与 printf 中的格式相同。
*
* arg 标识使用 va_start 初始化的变量参数列表的值。
*
*返回值:
* 成功后,返回写入的字符总数。
* 如果发生写入错误,则会设置错误指示符(ferror)并返回负数。
* 如果在编写宽字符时发生多字节字符编码错误,则将 errno 设置为 EILSEQ,并返回负数;
*/
int vprintf(const char * format, va_list arg);
vprintf() 函数可以实现对 printf() 函数的封装。
#include <stdio.h>
#include <stdarg.h>
void my_printf(char *format, ...)
{
va_list args;
va_start(args, format);
vprintf(format, args);
va_end(args);
}
2、vsprintf() 函数
是 int sprintf(char *str, const char *format, ...)
底层函数。
/*
*描述:向一个字符串缓冲区打印格式化字符串
*
*参数:
* str 字符串数组指针,该数组用于存储格式化后的 C 字符串。
* format 格式化模式参数。
* arg 可变参数列表对象。
*
*返回值:
* 如果成功,则返回写入的字符总数,不包括末尾追加的\0,否则返回一个负值。
*/
int vsprintf(char *str, const char *format, va_list arg);
#include <stdio.h>
#include <stdarg.h>
char buffer[80];
int vspfunc(char *format, ...)
{
va_list aptr;
int ret;
va_start(aptr, format);
ret = vsprintf(buffer, format, aptr);
va_end(aptr);
return(ret);
}
3、vsnprintf() 函数
是 int snprintf(char *str, size_t size, const char *format, ...)
底层函数。
/*
*描述:向一个字符串缓冲区打印格式化字符串
*
*参数:
* str 用于缓存格式化字符串结果的字符数组。
* n 限定最多打印到 str 缓冲区的字符的个数为 n-1 个,因为 vsnprintf()
* 还要在结果的末尾追加 '\0'。如果格式化字符串长度大于 n-1,则多出的部分被丢弃。
* 一般这里传递的值就是 str 缓冲区的长度。
* format 格式化模式。
* arg 可变参数列表对象。
*
*返回值:
* 如果成功,则返回成功保存到缓冲区中的字符的个数,不包括末尾追加的\0,否则返回一个负值。
*/
int vsnprintf(char *str, size_t n, const char *format, va_list arg);
#include <stdio.h>
#include <stdarg.h>
#define MAX_SIZE 256
char buf[MAX_SIZE];
// 该函数也可以用于封装 Arduino 的 Serial.printf() 函数,只需将里面的 printf() 进行替换即可
void my_printf(const char *format, ... )
{
va_list args;
va_start(args, format);
vsnprintf(buf, SBUF_SIZE, format, args);
va_end(args);
printf("%s", buf);
}
4、__VA_ARGS__
该宏也可以实现对 printf()
函数的封装。
关于宏中的 #、## 符号使用问题,可以看我的另一篇文章:【C/C++】define的用法(高级用法)
#include <stdio.h>
#define SHOWLIST(...) printf(#__VA_ARGS__)
#define PRINT(format, ...) printf(format, ##__VA_ARGS__)
int main(void)
{
SHOWLIST(Hello, 520, 3.14\n);
PRINT("num = %d\n", 520);
PRINT("Hello World!\n");
return 0;
}