本文绝大部分内容均转自“北海石松”的博客,感谢!其博文地址如下:
http://www.cnblogs.com/gogly/articles/2416833.html
看到MINI2440的UART驱动程序中,有如下代码:
1 void Uart_Printf(char *fmt,...) 2 { 3 va_list ap; 4 char string[256]; 5 va_start(ap,fmt); 6 vsprintf(string,fmt,ap); 7 Uart_SendString(string); 8 va_end(ap); 9 }
原理解释:
VA_LIST是在C语言中解决变参问题的一组宏,所在头文件:#include <stdarg.h>,
用法如下:
(1)首先在函数里定义一具VA_LIST型的变量,这个变量是指向参数的指针;
(2)然后用VA_START宏初始化变量刚定义的VA_LIST变量,使其指向第一个可变参数的地址;
(3)然后用VA_ARG返回可变的参数,VA_ARG的第二个参数是你要返回的参数的类型(如果函数有多个可变参数的,依次调用VA_ARG获取各个参数);
(4)最后用VA_END宏结束可变参数的获取。
VA_LIST在编译器中的处理:
(1)在运行va_list(ap,v)之后,ap指向第一个可变参数在堆栈的地址。
(2)va_arg()取得类型t的可变参数值,在这步操作中,首先apt=sizeof(t类型),让ap指向下一个参数的地址。然后返回ap - sizeof(t类型) 的t类型*指针,这正是第一个可变参数在堆栈里的地址,然后用*取得这个地址的内容。
(3)va_end(),X86平台定义为ap = ( (char*)0 ),使ap不再指向堆栈,而是跟NULL一样,有些直接定义为
( (void*)0 ),这样编译器不会为va_end产生代码,例如gcc在Linux的X86平台就是这样定义的。
要注意的是:由于参数的地址用于va_start宏,所以参数不能声明为寄存器变量,或作为函数或数组类型。
使用VA_LIST应该注意的问题:
(1)因为va_start, va_arg, va_end等定义成宏,所以它显得很愚蠢,可变参数的类型和个数完全在该函数中由程序代码控制,它并不能智能地识别不同参数的个数和类型. 也就是说,你想实现智能识别可变参数的话是要通过在自己的程序里作判断来实现的.
(2)另外有一个问题,因为编译器对可变参数的函数的原型检查不够严格,对编程查错不利.不利于我们写出高质量的代码。
小结:可变参数的函数原理其实很简单,而VA系列是以宏定义来定义的,实现跟堆栈相关。我们写一个可变函数的C函数时,有利也有弊,所以在不必要的场合,我们无需用到可变参数,如果在C++里,我们应该利用C++多态性来实现可变参数的功能,尽量避免用C语言的方式来实现。
举一个相关的例子,vs2010编译通过:
1 #include "stdafx.h"
2 #include <stdarg.h>
3 #include <iostream>
4 using namespace std;
5
6 char buffer[80];
7
8 int vspf(char *fmt, ...)
9 {
10 va_list argptr;
11 int cnt;
12 va_start(argptr, fmt);
13 cnt = vsprintf(buffer, fmt, argptr);
14 va_end(argptr);
15 return(cnt);
16 }
17
18 int main(void)
19 {
20 int inumber = 30;
21 float fnumber = 90.0;
22 char string[4] = "abc";
23 int n;
24 n=vspf("
hello
%d %f %s", inumber, fnumber, string);
25 printf("%s
", buffer);
26 printf("length of buffer is %d
",n);
27 system("pause");
28 return 0;
29 }
输出结果为:
如果将第24行改为:
1 n=vspf(" hello %d !%f %s", inumber, fnumber, string);
即,%d %f %s之间再空一格,且在%f前加一个' ! ',则输出结果为:
C语言用va_start等宏来处理这些可变参数,这些宏看起来很复杂,其实原理挺简单,就是根据参数入栈的特点从最靠近第一个可变参数的固定参数开始,依次获取每个可变参数的地址。下面我们来分析这些宏。
(1)va_list型变量:
1 #ifdef _M_ALPHA 2 typedef struct{ 3 char* a0; /*pointertofirsthomedintegerargument*/ 4 int offset; /*byteoffsetofnextparameter*/ 5 }va_list; 6 #else 7 typedef char* va_list; 8 #endif
(2)_INTSIZEOF宏,获取类型占用的空间长度,最小占用长度为int的整数倍:
1 #define _INTSIZEOF(n) ((sizeof(n)+sizeof(int)-1)&~(sizeof(int)-1))
(3)va_start宏,获取可变参数列表的第一个参数的地址(ap是va_list类型指针,v是可变参数最左边的参数,亦即最后一个固定参数):
1 #define va_start(ap,v) (ap=(va_list)&v+_INTSIZEOF(v))
(4)va_arg宏,获取可变参数的当前参数,返回指定类型,并将指针指向下一参数(t参数描述了当前参数的类型)
1 #define va_arg(ap,t) (*(t*)((ap+=_INTSIZEOF(t))-_INTSIZEOF(t)))
(5)va_end宏,清空va_list可变参数列表:
1 #define va_end(ap) (ap=(va_list)0)
_INTSIZEOF(n)宏是为了考虑那些内存地址需要对齐的系统,从宏的名字来看,应该是跟sizeof(int)对齐。一般的sizeof(int)=4,也就是参数在内存中的地址都为4的倍数。比如,如果sizeof(n)在1~4之间,那么_INTSIZEOF(n)=4;如果sizeof(n)在5-8之间,那么_INTSIZEOF(n)=8。
为了能从固定参数依次得到每个可变参数,va_start和va_arg充分利用下面两点:
1、C语言在函数调用时,先将最后一个参数压入栈
2、X86平台下的内存分配顺序是从高地址内存到低地址内存,如下示意图所示:
高位地址
第N个可变参数
。。。
第二个可变参数
第一个可变参数 ? ap
固定参数 ? v
低位地址
由上图可见,v是固定参数在内存中的地址,在调用va_start后,ap指向第一个可变参数。这个宏的作用就是在v的内存地址上增加v所占的内存大小,这样就得到了第一个可变参数的地址。
接下来,可以这样设想,如果我能确定这个可变参数的类型,那么我就知道了它所占用了多少内存,依葫芦画瓢,我就能得到下一个可变参数的地址。
让我再来看看va_arg,它先ap指向下一个可变参数,然后减去当前可变参数的大小,即得到当前可变参数的内存地址,再做个类型转换,返回它的值。
要确定每个可变参数的类型,有两种做法,要么都是默认的类型,要么就在固定参数中包含足够的信息让程序可以确定每个可变参数的类型。比如,printf程序通过分析format字符串就可以确定每个可变参数的类型。
最后一个宏就简单了,va_end使得ap不再指向有效的内存地址。
其实在varargs.h头文件中定义了UNIX System V实行的va系列宏,而上面在stdarg.h头文件中定义的是ANSI C形式的宏,这两种宏是不兼容的,一般说来,我们应该使用ANSI C形式的va宏。
定义_INTSIZEOF(n)主要是为了某些需要内存对齐的系统,C语言的函数是从右向左压入堆栈的,我们看到va_list被定义为char*,在一些平台或操作系统定义为void*,再看va_start的定义,定义为&v+_INTSIZEOF(v),而&v是固定参数在堆栈的地址,所以运行va_start(ap,v)后,ap指向第一个可变参数在堆栈的地址,然后,我们用va_arg()取得类型t的可变参数值。va_end,x86平台定义为ap=(char*)0;使ap不再指向堆栈,而是跟NULL一样.有些直接定义为((void*)0),这样编译器不会为va_end产生代码,例如gcc在linux的x86平台就是这样定义的.在这里大家要注意一个问题:由于参数的地址用于va_start宏,所以参数不能声明为寄存器变量或作为函数或数组类型.关于va_start, va_arg, va_end的描述就是这些了,我们要注意的是不同的操作系统和硬件平台的定义有些不同,但原理却是相似的。
最后,要说的是vsprintf,其定义为:
返回值: 正常情况下返回生成字串的长度(除去 ),错误情况返回负值
用法:int vsprintf(char *string, char *format, va_list param);
// 将param 按格式format写入字符串string中
注: 该函数会出现内存溢出情况,建议使用vsnprintf
从前面的例子可以看出在实际应用中的作用。