深度探索va_start、va_arg、va_end
採用C語言程式設計的時候,函式中形式引數的數目通常是確定的,在呼叫時要依次給出與形式引數對應的所有實際引數。但在某些情況下希望函式的引數個數可以根據需要確定。典型的例子有大家熟悉的函式printf()、scanf()和系統呼叫execl()等。那麼它們是怎樣實現的呢?
C編譯器通常提供了一系列處理這種情況的巨集,以遮蔽不同的硬體平臺造成的差異,增加程式的可移植性。這些巨集包括va_start、va_arg和va_end等。在講解以上巨集之前我們先了解一下呼叫函式時傳入引數的處理過程。
一、函式傳入引數過程
一個函式包括函式名、傳入引數、返回引數以及函式體,函式體編譯後的二進位制程式碼儲存在程式程式碼區。當用戶呼叫某個函式時,系統會通過函式名(C++中會涉及到mangled命名處理)查詢函式體入口指標並壓入棧中,之後將傳入引數以從右至左的順序壓入棧中(棧空間是往低地址方向增長的,也即棧底對應高地址,棧頂對應低地址),示例如下:
#include <iostream> using namespace std; void fun(int a, ...) { int *temp = &a; temp++; for (int i = 0; i < a; ++i) { cout<< *temp << endl; temp++; } } int main() { int a = 1; int b = 2; int c = 3; int d = 4; fun(4, a, b, c, d); system("pause"); return 0; } // Output: // 1 // 2 // 3 // 4
二、va_start、va_arg和va_end巨集定義
接著我們再來看看va_start、va_arg和va_end等巨集的具體定義。
#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) ) // 此句巨集的作用是將型別n的大小向上取成4的倍數,如n為char型的話結果即為4 #ifdef __cplusplus #define _ADDRESSOF(v) ( &reinterpret_cast<const char &>(v) ) // vs2015中此句高亮,作用是將v的地址重新解釋成char*型 #else #define _ADDRESSOF(v) ( &(v) ) #endif #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 ) #ifndef _VA_LIST_DEFINED #ifdef _M_CEE_PURE typedef System::ArgIterator va_list; #else typedefchar * va_list; // vs2015中此句高亮 #endif /* _M_CEE_PURE */ #define _VA_LIST_DEFINED #endif // 以上巨集定義出現在vadef.h,通過stdio.h即可使用
#define va_start _crt_va_start #define va_arg _crt_va_arg #define va_end _crt_va_end // 以上巨集定義出現在stdarg.h中,若要使用則需加上 #include <stdarg.h>
三、va_start、va_arg和va_end使用示例
容易看出,以上巨集定義主要涉及地址操作,va_start獲取第二個傳入引數的地址給va_list型別變數(假設為arg_ptr),va_arg用於獲取當前引數值並將指標arg_ptr往後移,va_end則是將arg_ptr置為空。va_start、va_arg、va_end具體用法示例如下:
#include <stdio.h> #include<stdarg.h> int fun(int x, int y) { return x - y; } int fun(int count, ...) { va_list arg_ptr; // 等同於 char *arg_ptr; int nArgValue = count; int nArgCout = 0; va_start(arg_ptr, count); // 使arg_ptr指向第二個引數的地址 printf("The 1 th arg: %d\n", nArgValue); // 輸出第一個引數的值 int sum = 0; for (int i = 0; i < count; i++) { ++nArgCout; nArgValue= va_arg(arg_ptr, int);// 將arg_ptr所指引數返回成int並移動arg_ptr使其指向後一個引數,這裡假設傳入引數均是int型 printf("The %d th arg: %d\n", i+2, nArgValue); // 輸出各引數的值 sum += nArgValue; } va_end(arg_ptr); // 將arg_ptr置為空 return sum; } int main() { cout<< fun(0) << endl; cout<< fun(1, 1) << endl; // 優先匹配到函式int fun(int, int), 輸出0 cout << fun(2, 3, 4) << endl; system("pause"); return 0;}
// Output: // 0 // 0 // The 1 th arg: 2 // The 2 th arg: 3 // The 3 th arg: 4 // 7
注意:以上巨集操作並不提供引數個數獲取操作,這需要使用者在函式中獲取,如第二個fun函式使用count指明個數,printf通過解析第一個傳入引數來確定引數個數與型別等。