可變參數函數

可變參數函數

c/c++支持可變參數的函數,即函數的參數是不確定的。

 

一、爲什麼要使用可變參數的函數?

 

一般我們編程的時候,函數中形式參數的數目通常是確定的,在調用時要依次給出與形式參數對應的所有實際參數。但在某些情況下希望函數的參數個數可以根據需要確定,因此c語言引入可變參數函數。這也是c功能強大的一個方面,其它某些語言,比如fortran就沒有這個功能。

 

典型的可變參數函數的例子有大家熟悉的printf()scanf()等。

 

二、c/c++如何實現可變參數的函數?

 

爲了支持可變參數函數,C語言引入新的調用協議, C語言調用約定 __cdecl  採用C/C++語言編程的時候,默認使用這個調用約定。如果要採用其它調用約定,必須添加其它關鍵字聲明,例如WIN32 API使用PASCAL調用約定,函數名字之前必須加__stdcall關鍵字。

 

採用C調用約定時,函數的參數是從右到左入棧,個數可變。由於函數體不能預先知道傳進來的參數個數,因此採用本約定時必須由函數調用者負責堆棧清理。舉個例子:

 

//C調用約定函數

int __cdecl Add(int a, int b)

{

return (a + b);

}

 

函數調用:

Add(1, 2);

//彙編代碼是:

push 2 ;參數b入棧

push 1 ;參數a入棧

call @Add ;調用函數。其實還有編譯器用於定位函數的表達式這裏把它省略了

add esp,8 ;調用者負責清棧

 

如果調用函數的時候使用的調用協議和函數原型中聲明的不一致,就會導致棧錯誤,這是另外一個話題,這裏不再細說。

 

另外c/c++編譯器採用宏的形式支持可變參數函數。這些宏包括va_startva_argva_end等。之所以這麼做,是爲了增加程序的可移植性。屏蔽不同的硬件平臺造成的差異。

 

支持可變參數函數的所有宏都定義在stdarg.h  varargs.h中。例如標準ANSI形式下,這些宏的定義是:

 

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 )

 

使用宏_INTSIZEOF是爲了按照整數字節對齊指針,因爲c調用協議下面,參數入棧都是整數字節(指針或者值)。

 

三、如何定義這類的函數。

 

可變參數函數在不同的系統下,採用不同的形式定義。

 

1、用ANSI標準形式時,參數個數可變的函數的原型聲明是:

 

type funcname(type para1, type para2, …);

 

關於這個定義,有三點需要說明:

 

一般來說,這種形式至少需要一個普通的形式參數,可變參數就是通過三個’.'來定義的。所以”…”不表示省略,而是函數原型的一部分。type是函數返回值和形式參數的類型。

例如:

 

int MyPrintf(char const* fmt, …);

 

但是,我們也可以這樣定義函數:

 

void MyFunc(…);

 

但是,這樣的話,我們就無法使用函數的參數了,因爲無法通過上面所講的宏來提取每個參數。所以除非你的函數代碼中的確沒有用到參數表中的任何參數,否則必須在參數表中使用至少一個普通參數。

 

注意,可變參數只能位於函數參數表的最後。不能這樣:

 

void MyFunc(…, int i);

 

2、採用與UNIX 兼容系統下的聲明方式時,參數個數可變的函數原型是:

 

type funcname(va_alist);

 

但是要求函數實現的時候,函數名字後面必須加上va_dcl。例如:

 

#include

int average( va_list );

 

void main( void )

{

。。。//代碼

}

 

/* UNIX兼容形式*/

int average( va_alist )

va_dcl

{

。。。//代碼

}

 

這種形式不需要提供任何普通的形式參數。type是函數返回值的類型。va_dcl是對函數原型聲明中參數va_alist的詳細聲明,實際是一個宏定義。根據平臺的不同,va_dcl的定義稍有不同。

 

varargs.h中,va_dcl的定義後面已經包括了一個分號。因此函數實現的時候,va_dcl後不再需要加上分號了。

 

3、採用頭文件stdarg.h編寫的程序是符合ANSI標準的,可以在各種操作系統和硬件上運行;而採用頭文件varargs.h的方式僅僅是爲了與以前的程序兼容,兩種方式的基本原理是一致的,只是在語法形式上有一些細微的區別。 所以一般編程的時候使用stdarg.h。下面的所有例子代碼都採用ANSI標準格式。

 

四、可變參數函數的基本使用方法

 

下面通過若干例子,說明如何實現可變參數函數的定義和調用。

 

//================================ 例子程序1 ===============

#include < stdio.h >

#include < string.h >

#include < stdarg.h >

 

/* 函數原型聲明,至少需要一個確定的參數,注意括號內的省略號 */

int demo( char *, … );

 

void main( void )

{

demo(DEMOThisisademo!/0);

}

 

int demo( char *msg, … )

{

va_list argp; /* 定義保存函數參數的結構 */

int argno = 0; /* 紀錄參數個數 */

char *para; /* 存放取出的字符串參數 */

 

// 使用宏va_start, 使argp指向傳入的第一個可選參數,

// 注意 msg是參數表中最後一個確定的參數,並非參數表中第一個參數

va_start( argp, msg );

 

while (1)

{

//取出當前的參數,類型爲char *

//如果不給出正確的類型,將得到錯誤的參數

para = va_arg( argp, char *);

 

if ( strcmp( para, /0) == 0 ) /* 採用空串指示參數輸入結束 */

break;

printf(”參數 #%d : %s/n, argno, para);

argno++;

}

va_end( argp ); /* argp置爲NULL */

return 0;

}

 

//輸出結果

參數 #0 : This

參數 #1 : is

參數 #2 : a

參數 #3 : demo!

 

注意到上面的例子沒有使用第一個參數,下面的例子將使用所有參數

 

//================================ 例子程序2 ===============

 

#include

#include

int average( int first,  ); //輸入若干整數,求它們的平均值

 

void main( void )

{

/* 調用3個整數(-1表示結尾) */

printf( “Average is: %d/n”, average(2,3,4, -1));

 

/*調用4個整數*/

printf( “Average is: %d/n”, average(5,7,9, 11,-1));

 

/*只有結束符的調用*/

printf( “Average is: %d/n”, average(-1) );

}

 

/* 返回若干整數平均值的函數 */

int average( int first, … )

{

int count = 0, sum = 0, i = first;

va_list marker;

 

va_start( marker, first ); //初始化

while( i != -1 )

{

sum += i; //先加第一個參數

count++;

i = va_arg( marker, int);//取下一個參數

}

va_end( marker );

return( sum ? (sum / count) : 0 );

}

 

//輸出結果

Average is: 3

Average is: 8

Average is: 0

 

五、關於可變參數的傳遞問題

 

有人問到這個問題,假如我定義了一個可變參數函數,在這個函數內部又要調用其它可變參數函數,那麼如何傳遞參數呢?上面的例子都是使用宏va_arg逐個把參數提取出來使用,能否不提取,直接把它們傳遞給另外的函數呢?

 

我們先看printf的實現:

 

int __cdecl printf (const char *format, …)

{

va_list arglist;

int buffing;

int retval;

 

va_start(arglist, format); //arglist指向format後面的第一個參數

 

。。。//不關心其它代碼

retval = _output(stdout,format,arglist); //format格式和參數傳遞給output函數

 

。。。//不關心其它代碼

return(retval);

}

 

我們先模仿這個函數寫一個:

 

#include

#include

 

int mywrite(char *fmt, …)

{

va_list arglist;

va_start(arglist, fmt);

return printf(fmt,arglist);

}

 

void main()

{

int i=10, j=20;

char buf[] = “This is a test”;

double f= 12.345;

mywrite(”String: %s/nInt: %d, %d/nFloat :%4.2f/n”, buf, i, j, f);

}

 

運行一下看看,哈,錯誤百出。仔細分析原因,根據宏的定義我們知道 arglist是一個指針,它指向第一個可變的參數,但是所有的參數都位於棧中,所以arglist指向棧中某個位置,通過arglist的值,我們可以直接查看棧裏面的內容:

 

arglist -> 指向棧裏面,內容包括

 

0067FD78 E0 FD 67 00 //指向字符串”This is a test

0067FD7C 0A 00 00 00 //整數 i 的值

0067FD80 14 00 00 00 //整數 j 的值

0067FD84 71 3D 0A D7 //double 變量 f, 佔用8個字節

0067FD88 A3 B0 28 40

0067FD8C 00 00 00 00

 

如果直接調用 printf(fmt, arglist); 僅僅是把arglist指針的值0067FD78入棧,然後把格式字符串入棧,相當於調用:

 

printf(fmt, 0067FD78);

 

自然這樣的調用肯定會出現錯誤。

 

我們能不能逐個把參數提取出來,再傳遞給其它函數呢?先考慮一次性把所有參數傳遞進去的問題。

 

如果調用的是系統庫函數,這種情況下是不可能的。因爲提取參數是在運行態,而參數入棧是在編譯的時候確定的。無法讓編譯器預知運行態的事情給出正確的參數入棧代碼。而我們在運行態雖然可以提取每個參數,但是無法將參數一次性全部壓棧,即使使用匯編代碼實現起來也是很困難的,因爲不單是一個簡單的push代碼就可以做到。

 

如果接受參數的函數也是我們自己寫的,自然我們可以把arglist指針入棧,然後在函數中自己解析arglist指針裏面的參數,逐個提取出來處理。但是這樣做似乎沒有什麼意義,一方面,這個函數沒有必要也做成可變參數函數,另一方面直接在第一個函數中解析參數,然後處理不是更簡單麼?

 

我們唯一可以做到的是,逐個解析參數,然後循環中調用其它可變參數函數,每次傳遞一個參數。這裏又有一個問題,就是參數表中的不可變參數的傳遞問題,有些情況下不能簡單的傳遞,以上面的例子爲例, 通常我們解析參數的同時,還需要解析格式字符串:

 

#include

#include

#include

 

//測試一下這個,開個玩笑

void t(…)

{

printf(”/n”);

}

 

int mywrite(char *fmt, …)

{

va_list arglist;

va_start(arglist, fmt);

 

char temp[255];

strcpy(temp, fmt); //Copy the Format string

char Format[255];

 

char *p = strchr(temp,’%');

int i=0;

int iParam;

double fParam;

while(p != NULL)

{

while((*p< 'a' || *p>‘z’) && (*p!=0) ) p++;

if(*p == 0)break;

p++;

 

//格式字符串

int nChar = p - temp;

strncpy(Format,temp, nChar);

Format[nChar] = 0;

//參數

if(Format[nChar-1] != ‘f’)

{

iParam = va_arg( arglist, int);

printf(Format, iParam);

}

else

{

fParam = va_arg( arglist, double);

printf(Format, fParam);

}

 

i++;

if(*p == 0) break;

strcpy(temp, p);

p = strchr(temp, ‘%’);

}

if(temp[0] != 0)

printf(temp);

 

return i;

 

}

 

void main()

{

int i=10, j=20;

char buf[] = “This is a test”;

double f= 123.456;

mywrite(”String: %s/nInt: %d, %d/nFloat :%4.2f/nEnd”, buf, i, j, f, 0);

t(”aaa”, i);

}

 

//輸出:

String: This is a test

Int: 10, 20

Float :123.46

End




在可變長參數中,應用的是"加寬"原則。也就是float類型被擴展成double;char、 short類型被擴展成int。因此,如果你要去可變長參數列表中原來爲float類型的參數,需要用va_arg(argp, double)。對char和short類型的則用va_arg(argp, int)。

發佈了12 篇原創文章 · 獲贊 1 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章