C语言:va_start、va_end、va_arg 实现可变长参数
C语言:va_start、va_end、va_arg 实现可变长参数
1、可变长参数
即参数的个数不确定,个数可变。例如printf函数的定义:
int printf( const char* format, ...);
2、C语言实现
C语言可变参数通过三个宏(va_start、va_end、va_arg)和一个类型(va_list)实现:
void va_start ( va_list ap, paramN );
参数:
ap: 可变参数列表地址
paramN: 确定的参数
功能:初始化可变参数列表(把函数在 paramN 之后的参数地址放到 ap 中)。
void va_end ( va_list ap );
功能:关闭初始化列表(将 ap 置空)。
type va_arg ( va_list ap, type );
功能:返回下一个参数的值。
va_list :存储参数的类型信息。
3、用法
(1)首先在函数里定义一具va_list型的变量,这个变量是指向参数的指针;
(2)然后用va_start宏初始化变量刚定义的va_list变量;
(3)然后用va_arg返回可变的参数,va_arg的第二个参数是你要返回的参数的类型(如果函数有多个可变参数的,依次调用va_arg获取各个参数);
(4)最后用va_end宏结束可变参数的获取。
4、注意问题
(1)宏定义在 stdarg.h 中,所以使用时,不要忘了添加头文件。
(2)设定一个参数结束标志(cplusplus 上说,va_arg 并不能确定哪个参数是最后一个参数)。
(3)类型的匹配
(4)可变参数的类型和个数完全由程序代码控制,它并不能智能地识别不同参数的个数和类型;
(5)如果我们不需要一一详解每个参数,只需要将可变列表拷贝至某个缓冲,可用vsprintf函数;
(6)因为编译器对可变参数的函数的原型检查不够严格,对编程查错不利.不利于我们写出高质量的代码;
5、实例
#include <stdio.h> #include <stdarg.h> #define END -1 int va_sum (int first_num, ...) { // (1) 定义参数列表 va_list ap; // (2) 初始化参数列表 va_start(ap, first_num); int result = first_num; int temp = 0; // 获取参数值 while ((temp = va_arg(ap, int)) != END) { result += temp; } // 关闭参数列表 va_end(ap); return result; } int main () { int sum_val = va_sum(1, 2, 3, 4, 5, END); printf ("%d", sum_val); return 0; }
6、源码分析
typedef char * va_list;
#define _crt_va_start(ap,v) ( ap = (va_list)&(v) + _INTSIZEOF(v) )//获取可变参数列表的第一个参数的地址(ap是类型为va_list的指针,v是可变参数最左边的参数)
#define _crt_va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )//获取可变参数的当前参数,返回指定类型并将指针指向下一参数(t参数描述了当前参数的类型)
#define _crt_va_end(ap) ( ap = (va_list)0 ) //清空va_list可变参数列表
#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )//获取类型占用的空间长度,最小占用长度为int的整数倍
(1)定义_INTSIZEOF(n)主要是为了某些需要内存对齐的系统.C语言的函数是从右向左压入堆栈的,图(1)是函数的参数在堆栈中的分布位置.我们看到va_list被定义成char*,有一些平台或操作系统定义为void*.再看va_start的定义,定义为&v+_INTSIZEOF(v),而&v是固定参数在堆栈的地址,所以我们运行va_start(ap, v)以后,ap指向第一个可变参数在堆栈的地址。
高地址|-----------------------------|
|函数返回地址 |
|-----------------------------|
|....... |
|-----------------------------|
|第n个参数(第一个可变参数) |
|-----------------------------|<--va_start后ap指向
|第n-1个参数(最后一个固定参数)|
低地址|-----------------------------|<-- &v
图( 1 )
然后,我们用va_arg()取得类型t的可变参数值,以上例为int型为例,我们看一下va_arg取int型的返回值:
j= ( *(int*)((ap += _INTSIZEOF(int))-_INTSIZEOF(int)) );
首先ap+=sizeof(int),已经指向下一个参数的地址了.然后返回ap-sizeof(int)的int*指针,这正是第一个可变参数在堆栈里的地址(图2).然后用*取得这个地址的内容(参数值)赋给j.
高地址|-----------------------------|
|函数返回地址 |
|-----------------------------|
|....... |
|-----------------------------|<--va_arg后ap指向
|第n个参数(第一个可变参数) |
|-----------------------------|<--va_start后ap指向
|第n-1个参数(最后一个固定参数)|
低地址|-----------------------------|<-- &v
图( 2 )
最后要说的是va_end宏的意思,x86平台定义为ap=(char*)0;使ap不再指向堆栈,而是跟NULL一样.有些直接定义为((void*)0),这样编译器不会为va_end产生代码,例如gcc在linux的x86平台就是这样定义的.
在这里大家要注意一个问题:由于参数的地址用于va_start宏,所以参数不能声明为寄存器变量或作为函数或数组类型.
补充:软件开发 , C语言 ,