在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
2016年3月22日 星期二
[轉] va_list與void *
訂閱:
張貼留言 (Atom)
沒有留言:
張貼留言