談談va_list與void *
在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
推
01/14 00:16, , 1F
01/14 00:16, 1F
推
01/14 00:18, , 2F
01/14 00:18, 2F
※ 編輯: ccbruce 來自: 59.121.125.159 (01/14 00:28)
推
01/14 02:18, , 3F
01/14 02:18, 3F
推
01/14 03:11, , 4F
01/14 03:11, 4F
推
01/14 05:34, , 5F
01/14 05:34, 5F
推
01/14 15:33, , 6F
01/14 15:33, 6F
推
01/14 17:44, , 7F
01/14 17:44, 7F
推
01/14 18:56, , 8F
01/14 18:56, 8F
推
01/15 00:02, , 9F
01/15 00:02, 9F
推
01/15 00:36, , 10F
01/15 00:36, 10F
推
01/15 13:32, , 11F
01/15 13:32, 11F
推
04/12 16:43, , 12F
04/12 16:43, 12F
C_and_CPP 近期熱門文章
PTT數位生活區 即時熱門文章