【嵌入式C语言】可变参数 va_start、va_arg、va_end、va_list、stdarg.h 库详解
可变参数详解printf( )是我们在编程中避不开的函数,之前我们研究了printf( )的实现原理,初步了解了printf( )函数在库中是借用putchar( )来进行实现输出的,但是还有一个问题我们上一篇文章没有解决,那就是printf( )中的可变参数是怎么实现的呢,如何去使用可变参数完成我们自己的输出函数呢?让我们再来看一眼printf( )的长相,在stdio.h文件中他是这样声明的i
可变参数详解
printf( )
是我们在编程中避不开的函数,之前我们研究了printf( )
的实现原理,初步了解了printf( )
函数在库中是借用putchar( )
来进行实现输出的,但是还有一个问题我们上一篇文章没有解决,那就是printf( )
中的可变参数是怎么实现的呢,如何去使用可变参数完成我们自己的输出函数呢?
让我们再来看一眼printf( )
的长相,在stdio.h
文件中他是这样声明的
int printf(const char *format, ...)
printf( )
中分为两种参数,一种是const char *format
代表的固定参数,一种是...
代表的就是可变参数
/* *****************************************************
* Name: my_vprintf
* fuction: Implement formatting string function
* Input: const char *fmt -- A pointer to a formatted string
va_list ap -- Parameters of the pointer
* Output: None
* Return: None
* ****************************************************** */
static void my_vprintf(const char *fmt, va_list ap)
{
char lead = ' ';
int maxwidth = 0;
for (; *fmt != '\0'; fmt++)
{
if (*fmt != '%')
{ //顺序查找判断,遇到%就推出,否则继续循环输出
outc(*fmt);
continue;
}
fmt++;
if (*fmt == '0')
{ //遇到‘0’说明前导码是0
lead = '0';
fmt++;
}
while (*fmt >= '0' && *fmt <= '9')
{ //紧接着的数字是长度,算出指定长度
maxwidth *= 10;
maxwidth += (*fmt - '0');
fmt++;
}
switch (*fmt)
{ //判断格式输出
case 'd':
out_num(va_arg(ap, int), 10, lead, maxwidth);
break;
case 'o':
out_num(va_arg(ap, unsigned int), 8, lead, maxwidth);
break;
case 'u':
out_num(va_arg(ap, unsigned int), 10, lead, maxwidth);
break;
case 'x':
out_num(va_arg(ap, unsigned int), 16, lead, maxwidth);
break;
case 'c':
outc(va_arg(ap, int));
break;
case 's':
outs(va_arg(ap, char *));
break;
default:
outc(*fmt);
break;
}
}
}
/* *****************************************************
* Name: printf
* fuction: None
* Input: const char *fmt -- A pointer to a formatted string
... -- Variable number of arguments
* Output: None
* Return: None
* ****************************************************** */
void printf(const char *fmt, ...)
{
va_list ap; /* 获取输入的参数指针 */
va_start(ap, fmt); /* 获取 */
my_vprintf(fmt, ap);
va_end(ap);
}
可变参数与 stdarg.h 库
首先我们来看看stdarg.h
库中有那些内容
库变量
下面是头文件 stdarg.h
中定义的变量类型:
序号 | 变量 & 描述 |
---|---|
1 | va_list 这是一个适用于 va_start()、va_arg() 和 va_end() 这三个宏存储信息的类型。 |
库宏
下面是头文件 stdarg.h 中定义的宏:
序号 | 宏 & 描述 |
---|---|
1 | void va_start(va_list ap, last_arg) 这个宏初始化 ap 变量,它与 va_arg 和 va_end 宏是一起使用的。last_arg 是最后一个传递给函数的已知的固定参数,即省略号之前的参数。 |
2 | **type va_arg(va_list ap, type)**这个宏检索函数参数列表中类型为 type 的下一个参数。 |
3 | **void va_end(va_list ap)**这个宏允许使用了 va_start 宏的带有可变参数的函数返回。如果在从函数返回之前没有调用 va_end,则结果为未定义。 |
注意:以上的所有操作,只能从头到尾顺序访问后面的可变参数,可以暂停,但不能反向读取
库实现
/* VC++ 6.0 */
#define _INTSIZEOF(n) ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1))
#define va_start(ap, v) (ap = (va_list)&v + _INTSIZEOF(v)) //第一个可选参数地址
#define va_arg(ap, t) (*(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t))) //下一个参数的值
#define va_end(ap) (ap = (va_list)0) // 将指针置为无效
如果将其转换成函数则是
void va_start(va_list ap, xxx v) /* 其中的 xxx 为任意类型变量 */
{
ap = (va_list)&v + sizeof(v);
}
xxx va_arg(va_list ap, xxx t) /* 其中的 xxx 为任意类型变量 */
{
// (*(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)))
ap += sizeof(t);
return *((t *)(ap - sizeof(t)))
}
void va_end(va_list ap)
{
ap = (va_list)0; //强制转换
}
可变参数的存储与实现
以下文字引用自博客 可变参数函数详解
C调用约定下可使用va_list系列变参宏实现变参函数,此处va意为variable-argument(可变参数)。
典型用法如下:
#include <stdarg.h>
int VarArgFunc(int dwFixedArg, ...){ //以固定参数的地址为起点依次确定各变参的内存起始地址
va_list pArgs = NULL; //定义va_list类型的指针pArgs,用于存储参数地址
va_start(pArgs, dwFixedArg); //初始化pArgs指针,使其指向第一个可变参数。该宏第二个参数是变参列表的前一个参数,即最后一个固定参数
int dwVarArg = va_arg(pArgs, int); //该宏返回变参列表中的当前变参值并使pArgs指向列表中的下个变参。该宏第二个参数是要返回的当前变参类型
//若函数有多个可变参数,则依次调用va_arg宏获取各个变参
va_end(pArgs); //将指针pArgs置为无效,结束变参的获取
/* Code Block using variable arguments */
}
//可在头文件中声明函数为extern int VarArgFunc(int dwFixedArg, ...);,调用时用VarArgFunc(FixedArg, VarArg);
变参宏根据堆栈生长方向和参数入栈特点,从最靠近第一个可变参数的固定参数开始,依次获取每个可变参数的地址。
变参宏的定义和实现因操作系统、硬件平台及编译器而异(但原理相似)。
下面这四小段引用自 C语言中函数参数入栈的顺序
C程序栈底为高地址,栈顶为低地址,函数参数入栈顺序的确是从右至左的。可到底为什么呢?查了一直些文献得知,参数入栈顺序是和具体编译器实现相关的。比如,Pascal语言中参数就是从左到右入栈的,有些语言中还可以通过修饰符进行指定,如Visual C++.即然两种方式都可以,为什么C语言要选择从右至左呢?
进一步发现,Pascal语言不支持可变长参数,而C语言支持这种特色,正是这个原因使得C语言函数参数入栈顺序为从右至左。具体原因为:C方式参数入栈顺序(从右至左)的好处就是可以动态变化参数个数。通过栈堆分析可知,自左向右的入栈方式,最前面的参数被压在栈底。除非知道参数个数,否则是无法通过栈指针的相对位移求得最左边的参数。这样就变成了左边参数的个数不确定,正好和动态参数个数的方向相反。
因此,C语言函数参数采用自右向左的入栈顺序,主要原因是为了支持可变长参数形式。换句话说,如果不支持这个特色,C语言完全和Pascal一样,采用自左向右的参数入栈方式。
System V Unix在varargs.h
头文件中定义va_start
宏为va_start(va_list arg_ptr)
,而ANSI C则在stdarg.h头文件中定义va_start
宏为va_start(va_list arg_ptr, prev_param)
。
两种宏并不兼容,为便于程序移植通常采用ANSI C定义。
gcc编译器使用内置宏间接实现变参宏,如#define va_start(v,l) __builtin_va_start(v,l)
。因为gcc编译器需要考虑跨平台处理,而其实现因平台而异。例如x86-64或PowerPC处理器下,参数不全都通过堆栈传递,变参宏的实现相比x86处理器更为复杂。
x86平台VC6.0编译器中,stdarg.h
头文件内变参宏定义如下:
typedef char * va_list;
#define _INTSIZEOF(n) ( (sizeof(n)+sizeof(int)-1) & ~(sizeof(int)-1) )
#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) )
#define va_arg(ap, type) ( *(type *)((ap += _INTSIZEOF(type)) - _INTSIZEOF(type)) )
#define va_end(ap) ( ap = (va_list)0 )
各宏的含义如下:
- _INTSIZEOF宏考虑到某些系统需要内存地址对齐。从宏名看应按照sizeof(int)即堆栈粒度对齐,即参数在内存中的地址均为sizeof(int)=4的倍数。例如,若在1≤sizeof(n)≤4,则_INTSIZEOF(n)=4;若5≤sizeof(n)≤8,则_INTSIZEOF(n)=8。为便于理解,简化该宏为
#define _INTSIZEOF(n) ((sizeof(n) + x) & ~(x))x = sizeof(int) - 1 = 3 = 0b’0000 0000 0000 0011~x = 0b’1111 1111 1111 1100
一个数与(~x)相与的结果是sizeof(int)的倍数,即_INTSIZEOF(n)将n圆整为sizeof(int)的倍数。
-
va_start宏根据(va_list)&v得到第一个可变参数前的一个固定参数在堆栈中的内存地址,加上_INTSIZEOF(v)即v所占内存大小后,使ap指向固定参数后下个参数(第一个可变参数地址)。
固定参数的地址用于va_start宏,因此不能声明为寄存器变量(地址无效)或作为数组类型(长度难定)。
-
va_arg宏取得type类型的可变参数值。首先ap+=_INTSIZEOF(type),即ap跳过当前可变参数而指向下个变参的地址;然后ap-_INTSIZEOF(type)得到当前变参的内存地址,类型转换后返回当前变参值。
va_arg宏的等效实现,如下将指针移动至下个变参,并返回左移的值[-1](数组下标表示偏移量),即当前变参值
#define va_arg(ap,type) ((type *)((ap) += _INTSIZEOF(type)))
-
va_end宏使ap不再指向有效的内存地址。该宏的某些实现定义为((void*)0),编译时不会为其产生代码,调用与否并无区别。但某些实现中va_end宏用于函数返回前完成一些必要的清理工作:如va_start宏可能以某种方式修改堆栈,导致返回操作无法完成,va_end宏可将有关修改复原;又如va_start宏可能对参数列表动态分配内存以便于遍历va_list,va_end宏可释放此前动态分配的内存。因此,从使用va_start宏的函数中退出之前,必须调用一次va_end宏。函数内可多次遍历可变参数,但每次必须以va_start宏开始,因为遍历后ap指针不再指向首个变参。
下图给出基于变参宏的可变参数在堆栈中的分布:
变参宏无法智能识别可变参数的数目和类型,因此实现变参函数时需自行判断可变参数的数目和类型。
前者可显式提供变参数目或设定遍历结束条件(如-1、'\0’或回车符等)。
后者可显式提供变参类型枚举值,或在固定参数中包含足够的类型信息(如printf函数通过分析format字符串即可确定各变参类型),甚至主调函数和被调函数可约定变参的类型组织等。
都看到这里了,如果有帮助,点个赞👍👍👍再走呗!
点个赞,代码没有Bug呦~
更多推荐
所有评论(0)