可变参数(varialbe arguments)

  最近整理代码,发现之前曾经用到的可变参数(variable arguments),当时也是工作需要,没有进一步的理解。开这个blog,本为着整理积累的目的,于是就查阅了一些相关资料,整理成这篇文章。

  需要说明的是,可变参数是一个底层,而且陈旧的机制,缺点很多(下面会详细描述)。实际工作中,有很多良好的替代方法,比如传递结构体,也可以构造参数表类来解决。除非有充分的理由(比如你的上司拍了脑袋), variable arguments这种机制仅适用于了解底层实现,阅读一些底层代码。

 一 可变参数的使用

   可变参数使用起来非常简单,只需要知道四个宏就可以了,它门分别是:va_list、va_start、va_arg、va_end。如果仅仅是使用,完全可以这样来理解:va_list声明了一个迭代器,va_start初始化了这个迭代器,然后va_arg相当于迭代访问,va_end结束对可变参数列表的遍历。当然,实际的机制完全不是这样(本想在这篇文章中详细分析一下可变参数列表的实现,考虑到可变参数列表的实现都不是标准的,语言本身只不过定义了其标准的使用方式,而且现在也没太多时间,这篇文章中就不分析具体的实现方式了),而且在实际的使用中,也有一些需要注意的地方。

   下面是一个简单的示例代码,我们可以通过分析代码来了解variable arguments这种机制的使用方式。   

 

#include <stdio.h>
#include <stdarg.h>

#define ARG_TYPE_INTEGER	0
#define ARG_TYPE_FLOAT		1
#define ARG_TYPE_STRING		2

void PrintArgList(int nArgType, int nArgCount, ...);

int main (int argc, char * argv[]) {

	PrintArgList(ARG_TYPE_INTEGER, 4, 0, 1, 2, 3);
	printf("\n");
	
	PrintArgList(ARG_TYPE_FLOAT, 5, 0.0, 0.1, 0.2, 0.3, 0.4);
	printf("\n");
	
	PrintArgList(ARG_TYPE_STRING, 3, "one", "two", "three");
	printf("\n");

	return 0;
}

void PrintArgList_valist(int nArgType, int nArgCount, va_list valist) {
	int i = 0;
	for (; i < nArgCount; i ++) {
		
		printf("%d:", i);
		
		switch(nArgType) {
		case ARG_TYPE_INTEGER:
			{
				int nArg = va_arg(valist, int);  //3)读取可变参数列表中的值
				printf("%d\n", nArg);
			}
			break;
		case ARG_TYPE_FLOAT:
			{
				float fArg = (float)va_arg(valist, double);  //(#)为什么传入double类型?换成float试试?
				printf("%1.1f\n", fArg);
			}
			break;
		case ARG_TYPE_STRING:
			{
				char * pArg = va_arg(valist, char*);
				printf("%s\n", pArg);
			}			
			break;
		default:
			return;
		}
	}
}

void PrintArgList(int nArgType, int nArgCount, ...) {
	va_list valist;  // (1)声明一个valist变量
	va_start(valist, nArgCount);  // (2)用前面声明的valist变量和最后一个固定参数来初始化
	PrintArgList_valist(nArgType, nArgCount, valist);
	va_end(valist);  //  (4)结束
}

从使用角度,你只需要遵循以下模式就可以了:

	va_list valist;
	va_start(valist, nArgCount);

	// ... 在这个地方用va_arg宏从valist中获取参数的值,通常这部分会实现为一个独立的函数。

	va_end(valist);

需要注意以下几点:

1)可变参数列表不是类型安全的,va_arg(valist, int);这个语句的作用是从valist这个指针中读取一个int类型的量,同时将指针向后移动sizeof(int)个字节。这期间没有,也不可能做任何的类型检查。如果你指定了错误的类型,那么其结果是未定义的。想想当初学习C语言时,在printf函数上翻了多少跟头就明白了。

2)在实际实现中,一个带有可变参数的函数通常包括若干个固定参数,和一个可变参数列表,例如:

void foo(arg1, arg2, ...);  // 应该不会有只含有可变参数的函数:-)

通常前面的固定参数用来描述后面可变参数的信息,可以这么理解,前面的固定参数描述后面可变参数的格式,作为解析的约定。遗憾的是你从valist中看到的只是一个指针,因此,你的函数能否得到一个符合约定的参数列表,完全依赖于函数调用者的人品,编译器帮不了任何的忙。

3)这是我在写示例代码的过程中遇到的一个问题,当我传入float参数,而在函数实现中用va_arg(valist, float)读取到的却是不正常的值,后来查询资料才知道:在IA32平台上,浮点数参数压栈时会自动转换成double类型。

上面的示例代码演示来使用可变参数机制的方法。下面详细分析一下可变参数机制的原理。

二 stackoverflow上面关于可变参数的一些讨论

  在检索资料的过程中,看到stackoverflow上有一篇相关的讨论,其中一些观点很有道理,摘录并简单翻译了一下。

  The va_list maintains pointers to temporary memory on the stack (so-called "automatic storage" in the C standard). After the function with variable args has returned,   this automatic storage is gone and the contents are no longer usable. Because of this, you cannot simply keep a copy of the va_listitself -- the memory it references will   contain unpredictable content.”

  va_list维护了一个指向临时内存的指针,这个临时内存位于栈空间(在C标准中被称为“automatic storage").在这个包含可变参数的函数返回后,这个automatic storage就会收回,其内容也不再有效。因此,你无法简单的拷贝va_list本身——它所引用的内存的内容是不可知的。


  The implementation of va_list is not standard, but the value returned from va_arg() is.

  va _list的实现不是标准的,但是va_arg()的返回值却是标准的。

 


 

四 参考链接

  我在整理这篇文章的过程中参考到的一些文章

  GNU lib format output

  Stackoverflow上面关于可变参数的一个讨论

posted on 2011-02-26 20:36  kamala  阅读(491)  评论(0编辑  收藏  举报