C的可變參數

1 使用:
一直以來習慣了使用printf函數,但是對於可變參數沒有深入研究過,覺得可變參數是一個神奇的技術^0^。。。
工作閒下來的時候,想研究研究看可變參數的使用和原理。
目前C提供的可變參數的申明爲
void function(const char *format, ...);
這樣就可以在function中使用可變參數
C提供了幾個宏用於使用可變參數
va_list
va_start
va_arg
va_end
其中
va_list用於定義一個變量獲取可變參數指針
va_start用於將va_list定義的指針進行初始化
va_arg用於獲取對應指針的真實類型數據
va_end用於清空va_list定義的指針

好了,光說不練假把式,來一個例子吧。嗯,什麼樣的例子比較好呢?
對了,在c的printf中不支持c++的std::string,就自己實現一個支持std::string的printf吧。
例子:
void Puts(const char *pstr)
{
while(*pstr)
putchar(*pstr++);
}

// Printf函數支持可變類型爲
// %c->char
// %d->int
// %s->char*
// %S->std::string
// Bug:不支持'"'等轉義符
// 處理%%等出現問題
void Printf(const char *_Format, ……)
{
va_list arg_ptr;
va_start(arg_ptr,_Format);
const char *pWork = _Format;
while(*pWork != '"0')
{
if(pWork == _Format)
{
if(*pWork != '%')
putchar(*pWork);
}
else
{
if(*(pWork-1) == '%')
{
switch(*pWork)
{
case 'c':
{
char cvalue = va_arg(arg_ptr,char);
putchar(cvalue);
break;
}
case 'd':
{
int ivalue = va_arg(arg_ptr,int);
char buffer[32];
_itoa(ivalue, buffer, 10);
Puts(buffer);
break;
}
case 's':
{
char* psvalue = va_arg(arg_ptr,char*);
Puts(psvalue);
break;
}
case 'S':
{
std::string pstringvalue = va_arg(arg_ptr,std::string);
Puts(pstringvalue.c_str());
break;
}
default:
putchar('%');
putchar(*pWork);
}
}
else if(*pWork != '%')
{
putchar(*pWork);
}
}
pWork++;
}
va_end(arg_ptr);
}


調用代碼:
std::string s = "abc";
Printf("%cyPrint %cunction %s:%d, Support C++ std::string %S……version %d",'M','F',"Version",1, s, 2);
輸出結果爲:
MyPrint Function Version:1, Support C++ std::string abc...version 2

可變參數真是神奇的很啊。。。


2 原理:
我們來看看這幾個宏到底幹了什麼
typedef char * va_list; // 這個僅僅是個重定義而已。。。

// 獲取v的地址
#define _ADDRESSOF(v) ( &(v) ) 
// n的整數字節的大小,必須是sizeof(int)的整數倍。如sizeof(n)爲5的話,_INTSIZEOF(n)爲8(假設爲32位機器的話)
#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
// 給v的地址加上v的大小 
#define va_start(ap,v) ( ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v) ) 

// 給ap自增t的大小,並且獲取原有ap的地址的數據,強制轉型爲t類型
// 這個相當於 ( *(t *)ap )
// (ap += _INTSIZEOF(t))
// 這一個宏相當於完成兩件事情
#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )

// 給ap置0
#define va_end(ap) ( ap = (va_list)0 )

我們有必要了解一下C函數的調用規則了,在調用一個函數之前,調用方會將這個函數參數push(修改ESP指針),並且push規則是先push最後一個參數,最後push第一個參數,因此ESP指針最後應該是指向第一個參數。可變參數就是利用了這一點,一旦獲取到第一個參數的地址後,就能夠通過地址向前查找所有的參數。(注意:x86上的堆棧是反向的,push會使ESP的值減少,而不是增加)
上面的宏就是幫助用戶查找所有的可變參數。

問題:
printf以及Printf都不是類型安全的。調用方必須保證參數個數的正確,以及參數類型的正確,否則將會發生不可預期的錯誤。

3 探索
是否可以寫一個沒有固定參數的函數,比如
int f (...);
據ANSI C說不行,但是我的vc8可行。
問題是寫出這樣的函數,va_start就用不上了,因爲它需要可變參之前的那個固定參數。
其實我們可以自己從ESP中獲取相關參數。 
例子:
// 獲取第一個參數值的指針
// 函數在訪問的過程中最重要的事情就是要確保堆棧的平衡,而在win32(vc8)的環境下保持平衡的辦法是這樣的:
// 1.讓EBP保存ESP的值;
// push ebp
// mov ebp, esp
// 2.在結束的時候調用
// mov esp,ebp
// pop ebp
// retn
// 下面這個宏將ebp(原esp)中的指針+8放入ap中
// 注意:給ebp加8是因爲中間隔着一個ebp和一個函數返回地址
#define va_start_get_first_parameter(ap) \
__asm mov eax, ebp \
__asm add eax, 8 \
__asm mov ap,eax


void NoFirstParameterPrintf()
{
va_list arg_ptr;
va_start_get_first_parameter(arg_ptr);

int first = va_arg(arg_ptr,int);
int second = va_arg(arg_ptr,int);
int third = va_arg(arg_ptr,int);
printf("First=%d"nSecond=%d"nThird=%d"n",first,second,third);

va_end(arg_ptr);
}
調用代碼:
NoFirstParameterPrintf(3, 5, 7);

輸出結果:
First=3
Second=5
Third=7

這樣完全是可以獲取第一個參數的地址的。問題是,由於沒有類型信息和類型個數等信息(類似於printf中的%c等信息),所以這樣的例子貌似意義不是很大。

C函數要在程序中用到以下這些宏: 
void va_start( va_list arg_ptr, prev_param ); 
type va_arg( va_list arg_ptr, type ); 
void va_end( va_list arg_ptr );

va_list:用來保存宏va_start、va_arg和va_end所需信息的一種類型。爲了訪問變長參數列表中的參數,必須聲明
va_list類型的一個對象 定義: typedef char * va_list;
va_start:訪問變長參數列表中的參數之前使用的宏,它初始化用va_list聲明的對象,初始化結果供宏va_arg和
va_end使用;
va_arg: 展開成一個表達式的宏,該表達式具有變長參數列表中下一個參數的值和類型。每次調用va_arg都會修改
用va_list聲明的對象,從而使該對象指向參數列表中的下一個參數;
va_end:該宏使程序能夠從變長參數列表用宏va_start引用的函數中正常返回。
va在這裏是variable-argument(可變參數)的意思. 
這些宏定義在stdarg.h中,所以用到可變參數的程序應該包含這個頭文件.下面我們寫一個簡單的可變參數的函數,改函數至少有一個整數參數,第二個參數也是整數,是可選的.函數只是打印這兩個參數的值. 

#include <stdio.h>;  
#include <string.h>;  
#include <stdarg.h>;  

/* ANSI標準形式的聲明方式,括號內的省略號表示可選參數 */  

int demo(char *msg, ... )  
{  
va_list argp;     /* 定義保存函數參數的結構 */  
int argno = 0;      /* 紀錄參數個數 */  
char *para;     /* 存放取出的字符串參數 */  
                    /* argp指向傳入的第一個可選參數,   msg是最後一個確定的參數 */  
va_start( argp, msg );  
while (1) 
{  
para = va_arg( argp, char *);                 /*   取出當前的參數,類型爲char *. */  
if ( strcmp( para, "/0") == 0 )  
                                          /* 採用空串指示參數輸入結束 */  
break;  
printf("Parameter #%d is: %s/n", argno, para);  
argno++;  
}  
va_end( argp );                                   /* 將argp置爲NULL */  
return 0;  
}


void main( void )  
{  
demo("DEMO", "This", "is", "a", "demo!" ,"333333", "/0");  


}  


從這個函數的實現可以看到,我們使用可變參數應該有以下步驟: 
1)首先在函數裏定義一個va_list型的變量,這裏是arg_ptr,這個變 
量是指向參數的指針. 
2)然後用va_start宏初始化變量arg_ptr,這個宏的第二個參數是第 
一個可變參數的前一個參數,是一個固定的參數. 
3)然後用va_arg返回可變的參數,並賦值給整數j. va_arg的第二個 
參數是你要返回的參數的類型,這裏是int型. 
4)最後用va_end宏結束可變參數的獲取.然後你就可以在函數裏使 
用第二個參數了.如果函數有多個可變參數的,依次調用va_arg獲 
取各個參數.

二、可變參類型陷井

下面的代碼是錯誤的,運行時得不到預期的結果:

view plaincopy to clipboardprint?
va_start(pArg, plotNo); 
fValue = va_arg(pArg, float); // 類型應改爲double,不支持float 
va_end(pArg); 
va_start(pArg, plotNo);
fValue = va_arg(pArg, float); // 類型應改爲double,不支持float
va_end(pArg);

下面列出va_arg(argp, type)宏中不支持的type:

—— char、signed char、unsigned char
—— short、unsigned short
—— signed short、short int、signed short int、unsigned short int
—— float

在C語言中,調用一個不帶原型聲明的函數時,調用者會對每個參數執行“默認實際參數提升(default argument promotions)”。該規則同樣適用於可變參數函數——對可變長參數列表超出最後一個有類型聲明的形式參數之後的每一個實際參數,也將執行上述提升工作。

提升工作如下:
——float類型的實際參數將提升到double
——char、short和相應的signed、unsigned類型的實際參數提升到int
——如果int不能存儲原值,則提升到unsigned int

然後,調用者將提升後的參數傳遞給被調用者。

所以,可變參函數內是絕對無法接收到上述類型的實際參數的。


關於該陷井,C/C++著作中有以下描述:


在《C語言程序設計》對可變長參數列表的相關章節中,並沒有提到這個陷阱。但是有提到默認實際參數提升的規則:
在沒有函數原型的情況下,char與short類型都將被轉換爲int類型,float類型將被轉換爲double類型。
——《C語言程序設計》第2版 2.7 類型轉換 p36

在其他一些書籍中,也有提到這個規則:

事情很清楚,如果一個參數沒有聲明,編譯器就沒有信息去對它執行標準的類型檢查和轉換。
在這種情況下,一個char或short將作爲int傳遞,float將作爲double傳遞。
這些做未必是程序員所期望的。
腳註:這些都是由C語言繼承來的標準提升。
對於由省略號表示的參數,其實際參數在傳遞之前總執行這些提升(如果它們屬於需要提升的類型),將提升後的值傳遞給有關的函數。——譯者注
——《C++程序設計語言》第3版-特別版 7.6 p138

…… float類型的參數會自動轉換爲double類型,short或char類型的參數會自動轉換爲int類型 ……
——《C陷阱與缺陷》 4.4 形參、實參與返回值 p73

這裏有一個陷阱需要避免:
va_arg宏的第2個參數不能被指定爲char、short或者float類型。
因爲char和short類型的參數會被轉換爲int類型,而float類型的參數會被轉換爲double類型 ……
例如,這樣寫肯定是不對的:
c = va_arg(ap,char);
因爲我們無法傳遞一個char類型參數,如果傳遞了,它將會被自動轉化爲int類型。上面的式子應該寫成:
c = va_arg(ap,int);
——《C陷阱與缺陷》p164


























發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章