c语言va_list snprintf 的实现

转载 2015年11月19日 09:05:33

首先列出我自己实际遇到的一个例子:

在串口向 PC 发送数据时为了实现可变参数的功能,这是工程中遇到的一段代码:

int SerialDbgPrintf(uint8 type, char *fmt, ...)
{
if(type == ATCMD)
{
int cnt;
char string[MAX_PRINTF_STR_SIZE] = {'\0'};
va_list ap;
va_start(ap,fmt);
//cnt = vsprintf(string, fmt, ap);
cnt = vsnprintf(string,MAX_PRINTF_STR_SIZE ,fmt, ap);
if(cnt > 0)
{
//PutStrToUart1Dbg(string,strlen((char *)string));
if(cnt < MAX_PRINTF_STR_SIZE)
PutStrToUart1Dbg(string,cnt);
else
PutStrToUart1Dbg(string,MAX_PRINTF_STR_SIZE);
}
va_end(ap);
  return (cnt);
}
else if(type == NRCMD)
{
if(gDeviceConfig.DbgCtl.NormalInfoEn == TRUE)
{
int cnt;
char string[MAX_PRINTF_STR_SIZE] = {'\0'};
va_list ap;
va_start(ap,fmt);
//cnt = vsprintf(string, fmt, ap);
cnt = vsnprintf(string,MAX_PRINTF_STR_SIZE ,fmt, ap);
if(cnt > 0)
{
//PutStrToUart1Dbg(string,strlen((char *)string));
if(cnt < MAX_PRINTF_STR_SIZE)
PutStrToUart1Dbg(string,cnt);
else
PutStrToUart1Dbg(string,MAX_PRINTF_STR_SIZE);
}
va_end(ap);
  return (cnt);
}
else
{
return 0;
}
}
return -1;
}


C语言va_list_vsnprintf的使用

先举一个例子:

#define bufsize 80
char buffer[bufsize];

/* 这个函数用来格式化带参数的字符串*/
int vspf(char *fmt, ...)

    va_list argptr; //声明一个转换参数的变量
    int cnt; 
    va_start(argptr, fmt); //初始化变量   
    cnt = vsnprintf(buffer,bufsize ,fmt, argptr);

//将带参数的字符串按照参数列表格式化到buffer中
    va_end(argptr); //结束变量列表,和va_start成对使用   
    return(cnt);
}

int main(int argc, char* argv[])
{
    int inumber = 30; 
    float fnumber = 90.0; 
    char string[4] = "abc"; 

vspf("%d %f %s", inumber, fnumber, string);
{
    printf("%s\n", buffer); 
    return 0;
}

下面我们来探讨如何写一个简单的可变参数的C函数.

 

写可变参数的C函数要在程序中用到以下这些宏:

使用可变参数应该有以下步骤: 
1)首先在函数里定义一个va_list型的变量,这里是arg_ptr,这个变量是指向参数的指针. 
2)然后用va_start宏初始化变量arg_ptr,这个宏的第二个参数是第一个可变参数的前一个参数,是一个固定的参数. 
3)然后用va_arg返回可变的参数,并赋值给整数j. va_arg的第二个参数是你要返回的参数的类型,这里是int型. 
4)最后用va_end宏结束可变参数的获取.然后你就可以在函数里使用第二个参数了.如果函数有多个可变参数的,依次调用va_arg获取各个参数. 
如果我们用下面三种方法调用的话,都是合法的,但结果却不一样: 

可变参数在编译器中的处理

我们知道va_start,va_arg,va_end是在stdarg.h中被定义成宏的,由于:

1)硬件平台的不同

2)编译器的不同

Microsoft Visual Studio\VC98\Include\stdarg.h中,
typedef char *  va_list;
   /*把va_list被定义成char*,这是因为在我们目前所用的PC机上,字符指针类型可以用来存储内存单元地址。而在有的机器上va_list是被定义成void*的*/
#define  _INTSIZEOF(n)   ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )

/*_INTSIZEOF (n)宏是为了考虑那些内存地址需要对齐的系统,从宏的名字来应该是跟sizeof(int)对齐。一般的sizeof(int)=4,也就是参数在内存中的地址都为4的倍数。比如,如果sizeof(n)在1-4之间,那么_INTSIZEOF(n)=4;如果sizeof(n)在5-8之间,那么 _INTSIZEOF(n)=8。*/
#define  va_start(ap,v)     ( ap = (va_list)&v + _INTSIZEOF(v) )

/*va_start 的定义为 &v+_INTSIZEOF(v) ,这里&v是最后一个固定参数的起始地址,再加上其实际占用大小后,就得到了第一个可变参数的起始内存地址。所以我们运行va_start (ap, v)以后,ap指向第一个可变参数在的内存地址*/
#define  va_arg(ap,t)       ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )

/*这个宏做了两个事情,

①用用户输入的类型名对参数地址进行强制类型转换,得到用户所需要的值

②计算出本参数的实际大小,将指针调到本参数的结尾,也就是下一个参数的首地址,以便后续处理。*/
#define  va_end(ap)         ( ap = (va_list)0 )
   /*x86 平台定义为ap=(char*)0;使ap不再指向堆栈,而是跟NULL一样.有些直接定义为((void*)0),这样编译器不会为va_end产生代码,例如gcc在linux的x86平台就是这样定义的. 在这里大家要注意一个问题:由于参数的地址用于va_start宏,所以参数不能声明为寄存器变量或作为函数或数组类型. */


这里有两个地方需要深入挖掘一下:
1、#define  _INTSIZEOF(n)   ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
我们这里简化一下这个宏:
#define  _INTSIZEOF(n)  ((sizeof(n) + x) & ~(x))
x = sizeof(int) - 1 = 3 = 0000 0000 0000 0011(b)
~x = 1111 1111 1111 1100(b)

当一个数 & (-x)时,得到的值始终是sizeof(int)的倍数,也就是说_INTSIZEOF(n)的功能是将n圆整到sizeof(int)的倍数上去。sizeof(n) >= 1, sizeof(n)+sizeof(int)-1经过圆整后,一定会是>=4的整数;在其他系统平台上,圆整的目标值有的是4,有的则是8,视具体系统而定。

2、#define  va_arg(ap,t)    ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
其实有了var_args_func的实现,这里也就不难理解了。不过这里有一个trick,很多人一开始肯定对先加上_INTSIZEOF(t),又减去_INTSIZEOF(t)很不理解,其实这里是一点就透的:整个表达式((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) 返回的值其实和最初的ap所指向的地址是一致的,关键就是在整个表达式被evaluated后,ap却指向了下一个参数的地址了,就这么简单。

 

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宏,所以参数不能声明为寄存器变量或作为函数或数组类型. 
   关于va_start, va_arg, va_end的描述就是这些了,我们要注意的是不同的操作系统和硬件平台的定义有些不同,但原理却是相似的.

可变参数在编程中要注意的问题

因为va_start, va_arg, va_end等定义成宏,所以它显得很愚蠢,可变参数的类型和个数完全在该函数中由程序代码控制,它并不能智能地识别不同参数的个数和类型. 
有人会问:那么printf中不是实现了智能识别参数吗?那是因为函数 
printf是从固定参数format字符串来分析出参数的类型,再调用va_arg的来获取可变参数的.也就是说,你想实现智能识别可变参数的话是要通过在自己的程序里作判断来实现的. 
另外有一个问题,因为编译器对可变参数的函数的原型检查不够严格,对编程查错不利.如果simple_va_fun()改为:
void simple_va_fun(int i, ...) 

    va_list arg_ptr; 
    char *s=NULL;

va_start(arg_ptr, i); 
    s=va_arg(arg_ptr, char*); 
    va_end(arg_ptr); 
    printf("%d %s\n", i, s); 
    return 0; 

可变参数为char*型,当我们忘记用两个参数来调用该函数时,就会出现core dump(Unix) 或者页面非法的错误(window平台).但也有可能不出错,但错误却是难以发现,不利于我们写出高质量的程序. 
以下提一下va系列宏的兼容性. 
System V Unix把va_start定义为只有一个参数的宏:    va_start(va_list arg_ptr); 
而ANSI C则定义为:    va_start(va_list arg_ptr, prev_param); 
如果我们要用system V的定义,应该用vararg.h头文件中所定义的宏,ANSI C的宏跟system V的宏是不兼容的,我们一般都用ANSI C,所以用ANSI C的定义就够了,也便于程序的移植.
小结:

可变参数的函数原理其实很简单,而va系列是以宏定义来定义的,实现跟堆栈相关.我们写一个可变函数的C函数时,有利也有弊,所以在不必要的场合,我们无需用到可变参数.如果在C++里,我们应该利用C++的多态性来实现可变参数的功能,尽量避免用C语言的方式来实现.

相关文章推荐

Keil编程环境下STM32内存管理研究 (Code、 RO-data、 RW-data、ZI-data)

非常简单的一个工程,没有用到任何IO操作,与STM32有关的仅仅只有芯片的选择,即其SRAM大小有区别。图1是工程示意图,从图中可以看出,除了自己编写的代码外,仅仅增加了2个文件,即system_st...

Keil MDK编译器下查看占用Flash和SRAM空间大小

在Keil MDK编译器编译后,信息栏都会显示编译情况,这其中就包含了FLASH跟SRAM的信息。如下图所示, /**************************************...

详解_C语言可变参数_va_list和_vsnprintf及printf实现

C语言的变长参数在平时做开发时很少会在自己设计的接口中用到,但我们最常用的接口printf就是使用的变长参数接口,在感受到printf强大的魅力的同时,是否想挖据一下到底printf是...
  • cjsycyl
  • cjsycyl
  • 2012年01月03日 12:39
  • 661

详解C语言可变参数va_list和vsnprintf及printf实现

C语言的变长参数在平时做开发时很少会在自己设计的接口中用到,但我们最常用的接口printf就是使用的变长参数接口,在感受到printf强大的魅力的同时,是否想挖据一下到底printf是如何实现的呢?这...
  • cbNotes
  • cbNotes
  • 2014年08月22日 17:50
  • 5763

详解C语言可变参数va_list和vsnprintf及printf实现

C语言的变长参数在平时做开发时很少会在自己设计的接口中用到,但我们最常用的接口printf就是使用的变长参数接口,在感受到printf强大的魅力的同时,是否想挖据一下到底printf是如何实现的呢?这...

详解 C语言可变参数 va_list和_vsnprintf及printf实现

C语言的变长参数在平时做开发时很少会在自己设计的接口中用到,但我们最常用的接口printf就是使用的变长参数接口,在感受到printf强大的魅力的同时,是否想挖据一下到底printf是如何实现的呢?这...
  • ilvu999
  • ilvu999
  • 2012年09月07日 21:59
  • 581

VA_LIST 是在C语言中解决变参问题的一组宏

VA_LIST 是在C语言中解决变参问题的一组宏 VA_LIST的用法:              (1)首先在函数里定义一具VA_LIST型的变量,这个变量是指向参数的指针        ...

C语言库函数之 int vsnprintf(char *str, size_t size, const char *format, va_list ap);

_vsnprintf,C语言库函数之一,属于可变参数。用于向字符串中打印数据、数据格式用户自定义。 头文件: #include 函数声明: int vsnprintf(char *str, ...
  • yhhwatl
  • yhhwatl
  • 2013年08月19日 22:47
  • 1633

C语言中可变参数的处理va_list

今天在看UNPv2的时候看到C语言中可变参数的操作,有必要深究一下,恩。整理下。。。     va_list是在C语言中解决可变参数问题的一组宏,他有这么几个成员:     1) va_list型变量...
  • ddppqq
  • ddppqq
  • 2013年12月15日 10:57
  • 1569
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:c语言va_list snprintf 的实现
举报原因:
原因补充:

(最多只允许输入30个字)