作者ccbruce (蛍雪时代にの仆)
看板C_and_CPP
标题谈谈va_list与void *
时间Mon Jan 14 00:14:00 2008
在C/C++中,有所谓的variant argument(变动引数)这东西。讲白一点,就是可以让函数
使用数量不固定的引数。这东西也许不是每个人都知道,但我想每个人都用过。因为
printf()家族就是使用这个东西的典型函数。
定义一个函数的prototype(原型)时,若是将参数列以"..."代入,就指述了这个函数即将
使用variant argument。如:
void func(...);
这样子便可让编绎器不检验传入这种函数里的引数型态和数量,编出来的程式码在呼叫端
就能够尽可能地把各式各样引数传入。如:
void func(0, 1, 2, 3, 0.4, 0.5 "6789");
那麽,在如此的程式里,要怎麽存取variant argument呢?因为缺少引数变数,所以我们
不可能像一般程式一样直接存取它们,而是要改用stdarg.h里面所提供的三个巨集与一个
型别,分别是:
va_list,宣告一个指标,让它指向引数串列。
va_start,初始化这个指标,让它真正指向正确的引数串列开头。
va_arg,来取得va_list中的资料。
va_end,清除这个指标,把它设为NULL。范例如下:
void func(int n, ...)
{
va_list args;
va_start(args, n);
while(n>0)
{
printf("%d\n", va_arg(args, int));
n--;
}
va_end(args);
}
以上程式是假设所有参数都会是int,并且有n个,把每个参数都印出来。
我们可以看看这三个家伙都是在做些什麽事,所以把它们展开来:
In vadefs.h...
typedef char * va_list;
#define _ADDRESSOF(v) ( &reinterpret_cast<const char &>(v) )
#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
#define _crt_va_start(ap,v) ( ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v) )
#define _crt_va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
#define _crt_va_end(ap) ( ap = (va_list)0 )
In stdarg.h...
#define va_start _crt_va_start
#define va_arg _crt_va_arg
#define va_end _crt_va_end
从这里可以看出va_list其实只是一个char*,它将整串引数当成一个位元阵列;而
va_start与va_arg可能比较神奇一点,但是仔细观查的话,可以发现va_start是参考实际
引数(Actual argument)t,把它的记忆体位址(也许你对_ADDRESSOF巨集不解,也从没看
过reinterpret_cast<>,其实这就是C++的static casting,当做(const char *)&v便是。
)加上另一个巨集_INTSIZEOF(n)做为偏移量,以真正地指向串列的开头。
那麽,为什麽要大费周张地用神秘巨集_INTSIZEOF(n)呢。就合理的推断来说,应该只要加
上sizeof(t)不就好了吗?这是因为,以C/C++编绎出来的各个参数是会对齐sizeof(int)的
。也就是说,今天若你传入了char做为variant argument的前一个参数,那麽想像中,整
个参数的配置应该是1+n个byte(char, ...)。但实际上,对於那个char,编绎出来的码
会是4+n byte才是。(要注意的是,这麽做很可能只有在X86/VC上是如此,若是换成其它
CPU或编绎器,可能不会这麽做。)也就是因为这种要五毛给一块的行为,让我们不能简
单地直接加上sizeof(t)。
再看看va_arg,你应该也发现它只是把ap往下继续延伸,很简单。
所以再看看我们的func函数,因为它的引数是(int n, ...),所以做过扩展後得到的就是
char *args; //arg_list
args=((const char *)&n)+4; //va_start
*(int *)((args+=4)-4); //va_arg
又va_start总是需要variant argument的前一个变数当做参考,所以我们一开始写的
func(...)便是永远不可能实用化的,除了一个例外。
考虑一个我们想实作的printf,它有Escape char,当%1时输出字串;%2时输出数字:
int my_printf(const char *fmt, ...);
可以写成这样子:
void my_printf_helper(const char *fmt, va_list args)
{
char *ptr=(char *)fmt;
while(*ptr)
{
switch(*ptr)
{
case '\\':
if(*++ptr)
ptr++;
continue;
case '%':
switch(*++ptr)
{
case NULL:
continue;
case '1':
printf("%s", va_arg(args, char *));
break;
case '2':
printf("%d", va_arg(args, int));
break;
}
ptr++;
default:
putchar(*ptr++);
}
}
}
void my_printf(const char *fmt, ...)
{
va_list args;
va_start(args, fmt);
my_printf_helper(fmt, args);
va_end(args);
}
好了,我们现在做出一个自己的printf了,就是这麽简单。接着,我们谈谈另一个应用。
应该有不少人会遇到一些函数其argument是void *吧。这是不定型指标,也就是说当
compiler遇到它时,不会检查它的型别。换句话说,若是需要传入一个复杂资料结构时,
便需要很恶心的一长串casting。又或是想传入多种资料时,不仅要casting,还得要配置
记忆体。这实在很令人难过,如_beginthreadex或CreateThread就是需要这样的一种函数
做为引数,好做为thread的主体:
unsigned int __stdcall my_func(void *args)
{...}
...
_beginthreadex(NULL, 0, my_func, ptr, 0, NULL);
...
但若考虑va_list的特性,我们可以做出比较乾净的程式码:
unsigned int __stdcall my_func(void *_args)
{
va_list args;
while(1)
{
args=(va_list)_args;
printf("%d\n", va_arg(args, int));
printf("%s\n", va_arg(args, char *));
Sleep(500);
}
return 0;
}
void start_thread(...)
{
va_list args;
__asm lea eax, [ebp+8] //because we don't have previous argument of the
__asm mov args, eax //variant argument... So we can't use va_start...
WaitForSingleObject((HANDLE)_beginthreadex(NULL, 0, my_func, args, 0, NULL),
INFINITE);
}
...
start_thread(100, "abcdef");
这样看起来就完美许多,这是因为variant argument事实上就是将stack视为byte array
,所以可以利用一点小技巧骗过compiler,并且让OS帮我们把资料捆成一包,送给thread
。
嗯,室友开始用Foxy抓该死的A片了,所以文章打到这里为止,欢迎讨论。
--
--
※ 发信站: 批踢踢实业坊(ptt.cc)
◆ From: 59.121.125.159
1F:推 csihcs:赞耶~~推一个^^ 01/14 00:16
2F:推 PRAM:辛苦啦 推~~~ 01/14 00:18
※ 编辑: ccbruce 来自: 59.121.125.159 (01/14 00:28)
3F:推 wixter:有看有推 感恩~~ 01/14 02:18
4F:推 doomleika:有看有推 01/14 03:11
5F:推 UNARYvvv:看到 ebp+8 就懂了 推~ 01/14 05:34
6F:推 wulawu:推 01/14 15:33
7F:推 duidae:推! 01/14 17:44
8F:推 avhacker:搞thread还是boost好用. bind一个function就可以开thread 01/14 18:56
9F:推 rogerliu0916:无私的分享最令人敬佩了.... 01/15 00:02
10F:推 JLong:清楚明了 推 01/15 00:36
11F:推 vip82:有看有推 好文 01/15 13:32
12F:推 chung928:先推再看~ 04/12 16:43