va_list在win32/vc++6.0下的讨論
1. 簡介
va_list、va_arg、va_end是為了處理變參數的函數而做的宏定義,這些定義會因為平台(cpu、作業系統)和環境(編譯系統)的不同而有所不同。
簡單原理:編譯系統編譯時,會将函數的參數依次放到棧中,這樣根據固定參數的位址以及固定參數給出的相關資訊很容易得到可變參數的個數、類型、值。注意一點,這些或者是固定參數給出的資訊,雖然不是直接給出的;或者是程式寫作者自我約定。得到了可變參數,剩下的處理和普通函數一樣了。
以下的了解和執行個體均在win32&vc++6.0環境中的情況。
2. 結合一個執行個體看簡單原理
執行個體如下(win2kserver vc++6.0編譯通過):
//============================
//求若幹個整數的平均值
#include
#include
int AveInt(int,...);
void main()
{
printf("%d/t",AveInt(2,2,3));
printf("%d/t",AveInt(4,2,4,6,8));
return;
}
int AveInt(int num,...)
{
int ReturnValue=0;
int i=num;
va_list myvalist;
va_start(myvalist,num);
while(i>0)
{
ReturnValue+=va_arg(myvalist,int);
i--;
}
return ReturnValue/=num;
}
//===============================
(1) 配置設定情況:跟第二次調用AveInt(4,2,4,6,8),看下參數在記憶體中的配置設定,見下圖。
圖1
明顯,參數依次連續配置設定;
(2) 參數的個數:由第一個參數給定;
(3) 參數類型:我約定為整數;
(4) 參數值:明顯了。
3. 兩個小知識(自我整理下)
(1) win32 vc++編譯
記憶體配置設定由高址向低址進行;參數調用時,參數入棧順序:從最後一個開始,直到第一個。通過上圖可以看到這兩點。
(2) 記憶體對齊
分為結構成員記憶體對齊和棧記憶體對齊。
前者要求:字、雙字和四字在自然邊界上不需要在記憶體中對齊。(對字、雙字和四字來說,自然邊界分别是偶數位址、可以被4整除的位址、和可以被8整除的位址。)一個字或雙字操作數跨越了4位元組邊界,或者一個四字操作數跨越了8位元組邊界,被認為未對齊的。
後者要求:總保持對齊,而且對齊在4位元組邊界上。
兩者差別:前者是可以通過工具控制對齊邊界的,如在vc++中就可以通過控制選項控制邊界;而後者是不能控制的,必須對齊的,畢竟棧的效率太影響到程式性能了。
4. 宏定義
以下是vc++6.0定義在stdarg.h檔案中的關于x86平台的部分宏定義:
typedef char * va_list;
#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) )
#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
#define va_end(ap) ( ap = (va_list)0 )
結合上例,調用AveInt(4,2,4,6,8),當運作了va_start(myvalist,num);之後,根據定義myvalist =(va_list)&num+_INTSIZEOF(i),這樣myvalist指向第一個可變參數2;第一次運作va_arg(myvalist,int);後,先讓myvalist+= _INTSIZEOF(int)指向下一個可變參數4,然後在取出前面的可變參數2進行處理;依此類推。
以下是跟蹤AveInt(4,2,4,6,8)的運作記憶體截圖:
圖2:va_start(myvalist,num);之後
圖3:第一次運作va_arg(myvalist,int);後
圖4:第二次運作va_arg(myvalist,int);後
圖5:第三次運作va_arg(myvalist,int);後
5. 注意:易出錯
由于是可變參數,對編譯器來說,檢查類型一般比較困難,是以,編譯器對類型檢查不嚴格,容易出現由于程式寫作者的疏忽導緻的錯誤。例如,可變參數中有個int型的,然而在函數中把這個int型參數當作char *進行處理,就可能導緻記憶體越界等錯誤。