C语言可变参数

C语言的可变参函数

  • C语言中可变参数函数实现原理
    • C函数调用的栈结构
        • 但是对于变长参数的函数,我们就没有这么顺利了。还好,按照C标准的说明,支持变长参数的函数在原型声明中,必须有至少一个最左固定参数(这一点与传统C有区别,传统C允许不带任何固定参数的纯变长参数函数),这样我们可以得到其中固定参数的地址,但是依然无法从声明中得到其他变长参数的地址,比如:
        • 从这个结果来看,显然参数是从右到左,逐一压入栈中的(栈的延伸方向是从高地址到低地址,栈底的占领着最高内存地址,先入栈的参数,其地理位置也就最高了)。
    • C语言可变参数函数详解示例

C语言中可变参数函数实现原理

C函数调用的栈结构

可变参数函数的实现与函数调用的栈结构密切相关,正常情况下C的函数参数入栈规则为__stdcall, 它是从右到左的,即函数中的最右边的参数最先入栈。例如,对于函数:

1.	void fun(int a, int b, int c)  
2.	 {  
3.	       int d;  
4.	       ...  
5.	 }  

其栈结构为:

0x1ffc-->d  
0x2000-->a  
0x2004-->b  
0x2008-->c	

对于在32位系统的多数编译器,每个栈单元的大小都是sizeof(int), 而函数的每个参数都至少要占一个栈单元大小,如函数 void fun1(char a, int b, double c, short d) 对一个32的系统其栈的结构就是:

0x1ffc-->a  (4字节)(为了字对齐)  
0x2000-->b  (4字节)  
0x2004-->c  (8字节) 
0x200c-->d  (4字节)

因此,函数的所有参数是存储在线性连续的栈空间中的,基于这种存储结构,这样就可以从可变参数函数中必须有的第一个普通参数来寻址后续的所有可变参数的类型及其值。
先看看固定参数列表函数:

void fixed_args_func(int a, double b, char *c)  
{  
	printf("a = 0x%p\n", &a);  
	printf("b = 0x%p\n", &b);  
    printf("c = 0x%p\n", &c);  
}  

对于固定参数列表的函数,每个参数的名称、类型都是直接可见的,他们的地址也都是可以直接得到的,比如:通过&a我们可以得到a的地址,并通过函数原型声明了解到a是int类型的。

但是对于变长参数的函数,我们就没有这么顺利了。还好,按照C标准的说明,支持变长参数的函数在原型声明中,必须有至少一个最左固定参数(这一点与传统C有区别,传统C允许不带任何固定参数的纯变长参数函数),这样我们可以得到其中固定参数的地址,但是依然无法从声明中得到其他变长参数的地址,比如:

void var_args_func(const char * fmt, ...)   
{  
	   ... ...   
}  

这里我们只能得到fmt这固定参数的地址,仅从函数原型我们是无法确定"…“中有几个参数、参数都是什么类型的。回想一下函数传参的过程,无论”…"中有多少个参数、每个参数是什么类型的,它们都和固定参数的传参过程是一样的,简单来讲都是栈操作,而栈这个东西对我们是开放的。这样一来,一旦我们知道某函数帧的栈上的一个固定参数的位置,我们完全有可能推导出其他变长参数的位置。
我们先用上面的那个fixed_args_func函数确定一下入栈顺序。

int main()   
{  
	fixed_args_func(17, 5.40, "hello world");  
	return 0;  
}  
a = 0x0022FF50 
b = 0x0022FF54
c = 0x0022FF5C

从这个结果来看,显然参数是从右到左,逐一压入栈中的(栈的延伸方向是从高地址到低地址,栈底的占领着最高内存地址,先入栈的参数,其地理位置也就最高了)。

我们基本可以得出这样一个结论:

c.addr = b.addr + x_sizeof(b);  /*注意:  x_sizeof !=sizeof */ 
b.addr = a.addr + x_sizeof(a);

有了以上的"等式",我们似乎可以推导出 void var_args_func(const char * fmt, … ) 函数中,可变参数的位置了。起码第一个可变参数的位置应该是:first_vararg.addr = fmt.addr + x_sizeof(fmt); 根据这一结论我们试着实现一个支持可变参数的函数:

#include   
#include   
  
void var_args_func(const char * fmt, ...)   
{  
	char    *ap;  

	ap = ((char*)&fmt) + sizeof(fmt);  
	printf("%d\n", *(int*)ap);    
        
	ap =  ap + sizeof(int);  
	printf("%d\n", *(int*)ap);  
	
	ap =  ap + sizeof(int);  
	printf("%s\n", *((char**)ap));  
}  
  
int main()  
{  
	var_args_func("%d %d %s\n", 4, 5, "hello world");  
	return 0;  
}  

期待输出的内容:
4
5
hello world
先来解释一下这个程序。我们用ap获取第一个变参的地址,我们知道第一个变参是4,一个int 型,所以我们用(int*)ap以告诉编译器,以ap为首地址的那块内存我们要将之视为一个整型来使用,(int)ap获得该参数的值;接下来的变参是5,又一个int型,其地址是ap + sizeof(第一个变参),也就是ap + sizeof(int),同样我们使用*(int*)ap获得该参数的值;最后的一个参数是一个字符串,也就是char*,与前两个int型参数不同的是,经过ap + sizeof(int)后,ap指向栈上一个char类型的内存块(我们暂且称之tmp_ptr, char tmp_ptr)的首地址,即ap -> &tmp_ptr,而我们要输出的不是printf("%s\n", ap),而是printf("%s\n", tmp_ptr); printf("%s\n", ap)是意图将ap所指的内存块作为字符串输出了,但是ap -> &tmp_ptr,tmp_ptr所占据的4个字节显然不是字符串,而是一个地址。如何让&tmp_ptr是char 类型的,我们将ap进行强制转换(char)ap <=> &tmp_ptr,这样我们访问tmp_ptr只需要在(char*)ap前面加上一个即可,即printf("%s\n", (char*)ap);

一切似乎很完美,编译也很顺利通过,但运行上面的代码后,不但得不到预期的结果,反而整个编译器会强行关闭(大家可以尝试着运行一下),原来是ap指针在后来并没有按照预期的要求指向第二个变参数,即并没有指向5所在的首地址,而是指向了未知内存区域,所以编译器会强行关闭。其实错误开始于:ap = ap + sizeof(int);由于内存对齐,编译器在栈上压入参数时,不是一个紧挨着另一个的,编译器会根据变参的类型将其放到满足类型对齐的地址上的,这样栈上参数之间实际上可能会是有空隙的。(C语言内存对齐详解(1) C语言内存对齐详解(2) C语言内存对齐详解(3))所以此时的ap计算应该改为:ap = (char *)ap + __va_rounded_size(int);
改正后的代码如下:

#include  

#define __va_rounded_size(TYPE)      (((sizeof (TYPE) + sizeof (int) - 1) / sizeof (int)) * sizeof (int))  

void var_args_func(const char * fmt, ...)   
{  
	char *ap;  
	ap = ((char*)&fmt) + sizeof(fmt);  
	printf("%d\n", *(int*)ap);    

	ap = (char *)ap +  + __va_rounded_size(int);  
	printf("%d\n", *(int*)ap);  

	ap = ap +  __va_rounded_size(int);  
	printf("%s\n", *((char**)ap));  
}  

int main()  
{  
	var_args_func("%d %d %s\n", 4, 5, "hello world");   
	return 0;  
}  

var_args_func只是为了演示,并未根据fmt消息中的格式字符串来判断变参的个数和类型,而是直接在实现中写死了。
为了满足代码的可移植性,C标准库在stdarg.h中提供了诸多便利以供实现变长长度参数时使用。这里也列出一个简单的例子,看看利用标准库是如何支持变长参数的:

#include   
#include   

void std_vararg_func(const char *fmt,...)   
{  
	va_list ap;  
	va_start(ap, fmt);  

	printf("%d\n", va_arg(ap, int));  
	printf("%f\n", va_arg(ap, double));  
	printf("%s\n", va_arg(ap, char*));  

	va_end(ap);  
}  

int main() {  16.	 int main() 
{  
	td_vararg_func("%d %f %s\n", 4, 5.4, "hello world");    
	return 0;
}

对比一下 std_vararg_func和var_args_func的实现,va_list似乎就是char*, va_start似乎就是 ((char*)&fmt) + sizeof(fmt),va_arg似乎就是得到下一个参数的首地址。没错,多数平台下stdarg.h中va_list, va_start和var_arg的实现就是类似这样的。一般stdarg.h会包含很多宏,看起来比较复杂。
下面我们来探讨如何写一个简单的可变参数的C 函数.
使用可变参数应该有以下步骤:
1)首先在函数里定义一个va_list型的变量,这里是arg_ptr,这个变量是指向参数的指针.
2)然后用va_start宏初始化变量arg_ptr,这个宏的第二个参数是第一个可变参数的前一个参数,是一个固定的参数.
3)然后用va_arg返回可变的参数,并赋值给整数j. va_arg的第二个参数是你要返回的参数的类型,这里是int型.
4)最后用va_end宏结束可变参数的获取.然后你就可以在函数里使用第二个参数了.如果函数有多个可变参数的,依次调用va_arg获取各个参数.
在《C程序设计语言》中,Ritchie提供了一个简易版printf函数:

#include  

void minprintf(char *fmt, ...)  
{  
	va_list ap;  
	char *p, *sval;  
	 int ival;  
	double dval;  

	va_start(ap, fmt);  
	for (p = fmt; *p; p++) 
	{  
		if(*p != '%')
		{  
			putchar(*p);  
			continue;  
		}  
		switch(*++p) 
		{  
			case 'd':  
				ival = va_arg(ap, int);  
				printf("%d", ival);  
				break;  
			case 'f':  
				dval = va_arg(ap, double);  
				printf("%f", dval);  
				break;  
			case 's':  
				for (sval = va_arg(ap, char *); *sval; sval++)  
				putchar(*sval); 
	 			break;  
			default:  
				putchar(*p);  
				break;  
		}  
	}  
	va_end(ap);  
} 

C语言可变参数函数详解示例

一般我们编程的时候,函数中形式参数的数目通常是确定的,在调用时要依次给出与形式参数对应的实际参数。但在某些情况下我们希望函数的参数个数可以根据需要确定,因此c语言引入可变参数函数。典型的可变参数函数的例子有printf()、scanf()等,下面我就开始讲解
先看代码
复制代码代码如下:

printf(“hello,world!”);其参数个数为1个。
printf(“a=%d,b=%s,c=%c”,a,b,c);其参数个数为4个。

如何编写可变参数函数呢?我们首先来看看printf函数原型是如何定义的。
在linux下,输入man 3 printf,可以看到prinf函数原型如下:
复制代码代码如下:

SYNOPSIS
#include 
int printf(const char *format, ...);

后面的三个点…表示printf参数个数是不定的.
如何实现可变参数函数?
2. 编写可变函数准备
为了编写可变参数函数,我们通常需要用到头文件下定义的以下函数:
复制代码代码如下:

void va_start(va_list ap, last);
type va_arg(va_list ap, type);
void va_end(va_list ap);
void va_copy(va_list dest, va_list src);

其中:
va_list是用于存放参数列表的数据结构。
va_start函数根据初始化last来初始化参数列表。
va_arg函数用于从参数列表中取出一个参数,参数类型由type指定。
va_copy函数用于复制参数列表。
va_end函数执行清理参数列表的工作。
上述函数通常用宏来实现,例如标准ANSI形式下,这些宏的定义是:
复制代码代码如下:

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,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
#define va_end(ap) ( ap = (va_list)0 )

使用宏_INTSIZEOF是为了按照整数字节对齐指针,因为c调用协议下面,参数入栈都是整数字节(指针或者值)。
函数官方说明,如果你看到英文就烦,可以自行忽略以下说明。
va_start()
The va_start() macro initializes ap for subsequent use by va_arg() and
va_end(), and must be called first.
The argument last is the name of the last argument before the variable
argument list, that is, the last argument of which the calling function
knows the type.
Because the address of this argument may be used in the va_start()
macro, it should not be declared as a register variable, or as a func‐
tion or an array type.
va_arg()
The va_arg() macro expands to an expression that has the type and value
of the next argument in the call. The argument ap is the va_list ap
initialized by va_start(). Each call to va_arg() modifies ap so that
the next call returns the next argument. The argument type is a type
name specified so that the type of a pointer to an object that has the
specified type can be obtained simply by adding a * to type.
The first use of the va_arg() macro after that of the va_start() macro
returns the argument after last. Successive invocations return the
values of the remaining arguments.
If there is no next argument, or if type is not compatible with the
type of the actual next argument (as promoted according to the default
argument promotions), random errors will occur.
If ap is passed to a function that uses va_arg(ap,type) then the value
of ap is undefined after the return of that function.
va_end()
Each invocation of va_start() must be matched by a corresponding invo‐
cation of va_end() in the same function. After the call va_end(ap) the
variable ap is undefined. Multiple traversals of the list, each brack‐
eted by va_start() and va_end() are possible. va_end() may be a macro
or a function.
GNU给出的一个实例:
复制代码代码如下:

#include 
#include 
void
foo(char *fmt, ...)
{
  va_list ap;
  int d;
  char c, *s;
 va_start(ap, fmt);
 while (*fmt)
     switch (*fmt++) {
     case 's': /* string */
     s = va_arg(ap, char *);
         printf("string %s\n", s);
         break;
     case 'd': /* int */
         d = va_arg(ap, int);
         printf("int %d\n", d);
         break;
     case 'c': /* char */
/* need a cast here since va_arg only takes fully promoted types */
        c = (char) va_arg(ap, int);
        printf("char %c\n", c);
        break;
   }
   va_end(ap);
}

说明:
va_start(ap, fmt);用于根据fmt初始化可变参数列表。
va_arg(ap, char *);用于从参数列表中取出一个参数,其中的char *用于指定所取的参数的类型为字符串。每次调用va_arg后,参数列表ap都会被更改,以使得下次调用时能得到下一个参数。
va_end(ap);用于对参数列表进行一些清理工作。调用完va_end后,ap便不再有效。
以上程序给了我们一个实现printf函数的是思路,即:通过调用va_start函数,来得到参数列表,然后我们一个个取出参数来进行输出即可。
3.实例
例如:对于printf(“a=%d,b=%s,c=%c”,a,b,c)语句;fmt的值为a=%d,b=%s,c=%c,调用va_start函数将参数a,b,c存入了ap中。注意到:fmt中的%为特殊字符,紧跟%后的参数指明了参数类型.
因此我们的简易printf函数如下:
复制代码代码如下:

#include 
#include 
void
myprintf(char *fmt, ...)
{
  va_list ap;
  int d;
  double f;
  char c; 
  char *s;
  char flag;
  va_start(ap,fmt);
  while (*fmt){
   flag=*fmt++;
   if(flag!='%'){
 putchar(flag);
 continue;
  }
  flag=*fmt++;//记得后移一位
    switch (flag) 
  { 
   case 's':
 s=va_arg(ap,char*);
 printf("%s",s);
 break;
   case 'd': /* int */         
 d = va_arg(ap, int);         
 printf("%d", d);         
 break;     
   case 'f': /* double*/         
 d = va_arg(ap,double);         
 printf("%d", d);         
 break;
   case 'c': /* char*/   
 c = (char)va_arg(ap,int);        
 printf("%c", c);        
 break;
   default:
 putchar(flag);
 break;
  }   
  }
  va_end(ap);
}
int main(){
  char str[10]="linuxcode";
  int i=1024;
  double f=3.1415926;
  char c='V';
  myprintf("string is:%s,int is:%d,double is:%f,char is :%c",str,i,f,c);
}

从上面我们可以知道可变参数函数的编写,必须要传入一个参数fmt,用来告诉我们的函数怎样去确定参数的个数。我们的可变参数函数是通过自己解析这个参数来确定函数参数个数的。
比如,我们编写一个求和函数,其函数实现如下:
复制代码代码如下:

int sum(int cnt,...){
    int sum=0;
int i;
    va_list ap;
    va_start(ap,cnt);
for(i=0;i

总结一下就是:通过va_start初始化参数列表(也就能得到具体的参数个数了),然后使用va_arg函数从参数列表中取出你想要的参数,最后调用va_end执行清理工作。

你可能感兴趣的:(C语言可变参数)