最近整理代码,发现之前曾经用到的可变参数(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_list
itself -- 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()的返回值却是标准的。
四 参考链接
我在整理这篇文章的过程中参考到的一些文章