1. 程式人生 > >va_list、va_start、va_arg、va_end巨集的使用(轉)

va_list、va_start、va_arg、va_end巨集的使用(轉)

當你的函式的引數個數不確定時,就可以使用上述巨集進行動態處理,這無疑為你的程式增加了靈活性。

Example:

CString AppendString(CString str1,...)//一個連線字串的函式,引數個數可以動態變化
{
      LPCTSTR str=str1;//str需為指標型別,因為va_arg巨集返回的是你的引數的指標,但是如果你的引數為int等簡                       //單型別,則不必為指標,因為變數名實際上即是指標。
      CString res;
      va_list marker;     //你的型別連結串列
      va_start(marker,str1);//初始化你的marker連結串列

      while(str!="ListEnd")//ListEnd:引數的結束標誌,十分重要,在實際中需自行指定
      {
          res+=str;
          str=va_arg(marker,CString);//取得下一個指標
      }
      va_end(marker);//結束,與va_start合用
      return res;
}

int main()
{
      CString    str=AppendString("xu","zhi","hong","ListEnd");
      cout<<str.GetBuffer(str.GetLength())<<endl;
      return 0;
}

輸出 xuzhihong
CString AppendString(CString str1,...),因為連線字串的引數可以動態變化,你不知使用者要進行連線的字串個數是多少,所以你可以用來代替。但是要注意的是你的函式要有一個引數作為標誌來表示結束,否則會出錯。在上例中用ListEnd作為結束符。還有va_arg返回的是你引數內容的指標。上例在支援MFC程式的console下執行通過。

可變引數函式的原型宣告格式為:

type VAFunction(type arg1, type arg2, … );

引數可以分為兩部分:個數確定的固定引數和個數可變的可選引數。函式至少需要一個固定引數,固定引數的宣告和普通函式一樣;可選引數由於個數不確定,宣告時用"…"表示。固定引數和可選引數公同構成一個函式的引數列表。

藉助上面這個簡單的例2,來看看各個va_xxx的作用。

va_list arg_ptr:定義一個指向個數可變的引數列表指標;

va_start(arg_ptr, argN):使引數列表指標arg_ptr指向函式引數列表中的第一個可選引數,說明:argN是位於第一個可選引數之前的固定引數,(或者說,最後一個 固定引數;…之前的一個引數),函式引數列表中引數在記憶體中的順序與函式宣告時的順序是一致的。如果有一va函式的宣告是void va_test(char a, char b, char c, …),則它的固定引數依次是a,b,c,最後一個固定引數argN為c,因此就是va_start(arg_ptr, c)。

va_arg(arg_ptr, type):返回引數列表中指標arg_ptr所指的引數,返回型別為type,並使指標arg_ptr指向引數列表中下一個引數。

va_copy(dest, src):dest,src的型別都是va_list,va_copy()用於複製引數列表指標,將dest初始化為src。

va_end(arg_ptr):清空引數列表,並置引數指標arg_ptr無效。說明:指標arg_ptr被置無效後,可以通過呼叫va_start ()、va_copy()恢復arg_ptr。每次呼叫va_start() / va_copy()後,必須得有相應的va_end()與之匹配。引數指標可以在引數列表中隨意地來回移動,但必須在va_start() … va_end()之內。

va函式的實現就是對引數指標的使用和控制。


typedef char *   va_list;   // x86平臺下va_list的定義


函式的固定引數部分,可以直接從函式定義時的引數名獲得;對於可選引數部分,先將指標指向第一個可選引數,然後依次後移指標,根據與結束標誌的比較來判斷是否已經獲得全部引數。因此,va函式中結束標誌必須事先約定好,否則,指標會指向無效的記憶體地址,導致出錯。

這裡,移動指標使其指向下一個引數,那麼移動指標時的偏移量是多少呢,沒有具體答案,因為這裡涉及到記憶體對齊(alignment)問題,記憶體對齊跟具體 使用的硬體平臺有密切關係,比如大家熟知的32位x86平臺規定所有的變數地址必須是4的倍數(sizeof(int) = 4)。va機制中用巨集_INTSIZEOF(n)來解決這個問題,沒有這些巨集,va的可移植性無從談起。

首先介紹巨集_INTSIZEOF(n),它求出變數佔用記憶體空間的大小,是va的實現的基礎。


#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 )                            // 將指標置為無效


下表是針對函式int TestFunc(int n1, int n2, int n3, …)

引數傳遞時的記憶體堆疊情況。(C編譯器預設的引數傳遞方式是__cdecl。)

對該函式的呼叫為int result = TestFunc(a, b, c, d. e); 其中e為結束標誌。


從上圖中可以很清楚地看出va_xxx巨集如此編寫的原因。

1. va_start。為了得到第一個可選引數的地址,我們有三種辦法可以做到:

A) = &n3 + _INTSIZEOF(n3)

// 最後一個固定引數的地址 + 該引數佔用記憶體的大小

B) = &n2 + _INTSIZEOF(n3) + _INTSIZEOF(n2)

// 中間某個固定引數的地址 + 該引數之後所有固定引數佔用的記憶體大小之和

C) = &n1 + _INTSIZEOF(n3) + _INTSIZEOF(n2) + _INTSIZEOF(n1)

// 第一個固定引數的地址 + 所有固定引數佔用的記憶體大小之和

從編譯器實現角度來看,方法B),方法C)為了求出地址,編譯器還需知道有多少個固定引數,以及它們的大小,沒有把問題分解到最簡單,所以不是很聰明的途 徑,不予採納;相對來說,方法A)中運算的兩個值則完全可以確定。va_start()正是採用A)方法,接受最後一個固定引數。呼叫va_start ()的結果總是使指標指向下一個引數的地址,並把它作為第一個可選引數。在含多個固定引數的函式中,呼叫va_start()時,如果不是用最後一個固定 引數,對於編譯器來說,可選引數的個數已經增加,將給程式帶來一些意想不到的錯誤。(當然如果你認為自己對指標已經知根知底,遊刃有餘,那麼,怎麼用就隨 你,你甚至可以用它完成一些很優秀(高效)的程式碼,但是,這樣會大大降低程式碼的可讀性。)

注意:巨集va_start是對引數的地址進行操作的,要求引數地址必須是有效的。一些地址無效的型別不能當作固定引數型別。比如:暫存器型別,它的地址不是有效的記憶體地址值;陣列和函式也不允許,他們的長度是個問題。因此,這些型別時不能作為va函式的引數的。

2. va_arg身兼二職:返回當前引數,並使引數指標指向下一個引數。

初看va_arg巨集定義很彆扭,如果把它拆成兩個語句,可以很清楚地看出它完成的兩個職責。


#define va_arg(ap,t)    ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) ) //下一個引數地址
// 將( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )拆成:
/* 指標ap指向下一個引數的地址 */
1. ap += _INTSIZEOF(t);         // 當前,ap已經指向下一個引數了
/* ap減去當前引數的大小得到當前引數的地址,再強制型別轉換後返回它的值 */
2. return *(t *)( ap - _INTSIZEOF(t))


回想到printf/scanf系列函式的%d %s之類的格式化指令,我們不難理解這些它們的用途了- 明示引數強制轉換的型別。

(注:printf/scanf沒有使用va_xxx來實現,但原理是一致的。)

3.va_end很簡單,僅僅是把指標作廢而已。

#define va_end(ap) (ap = (va_list)0) // x86平臺

四、 簡潔、靈活,也有危險

從va的實現可以看出,指標的合理運用,把C語言簡潔、靈活的特性表現得淋漓盡致,叫人不得不佩服C的強大和高效。不可否認的是,給程式設計人員太多自由空間必然使程式的安全性降低。va中,為了得到所有傳遞給函式的引數,需要用va_arg依次遍歷。其中存在兩個隱患:

1)如何確定引數的型別。

va_arg在型別檢查方面與其說非常靈活,不如說是很不負責,因為是強制型別轉換,va_arg都把當前指標所指向的內容強制轉換到指定型別;

2)結束標誌。如果沒有結束標誌的判斷,va將按預設型別依次返回記憶體中的內容,直到訪問到非法記憶體而出錯退出。例2中SqSum()求的是自然數的平方 和,所以我把負數和0作為它的結束標誌。例如scanf把接收到的回車符作為結束標誌,大家熟知的printf()對字串的處理用'/0'作為結束標 志,無法想象C中的字串如果沒有'/0', 程式碼將會是怎樣一番情景,估計那時最流行的可能是字元陣列,或者是malloc/free。

允許對記憶體的隨意訪問,會留給不懷好意者留下攻擊的可能。當處理cracker精心設計好的一串字串後,程式將跳轉到一些惡意程式碼區域執行,以使cracker達到其攻擊目的。(常見的exploit攻擊)所以,必需禁止對記憶體的隨意訪問和嚴格控制記憶體訪問邊界。