va_start(),va_arg(),va_end()
下面我們來探討如何寫一個簡單的可變引數的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在這裡是variable-argument(可變引數)的意思.
這些巨集定義在stdarg.h中,所以用到可變引數的程式應該包含這個
標頭檔案.下面我們寫一個簡單的可變引數的函式,改函式至少有一個整數
引數,第二個引數也是整數,是可選的.函式只是列印這兩個引數的值.
void simple_va_fun(int i, ...)
{
va_list arg_ptr;
int j=0;
va_start(arg_ptr, i);
j=va_arg(arg_ptr, int);
va_end(arg_ptr);
printf("%d %d\n", i, j);
return;
}
我們可以在我們的標頭檔案中這樣宣告我們的函式:
extern void simple_va_fun(int i, ...);
我們在程式中可以這樣調用:
simple_va_fun(100);
simple_va_fun(100,200);
從這個函式的實現可以看到,我們使用可變引數應該有以下步驟:
1)首先在函式裡定義一個va_list型的
量是指向引數的指標.
2)然後用va_start巨集初始化變數arg_ptr,這個巨集的第二個引數是第
一個可變引數的前一個引數,是一個固定的引數.
3)然後用va_arg返回可變的引數,並賦值給整數j. va_arg的第二個
引數是你要返回的引數的型別,這裡是int型.
4)最後用va_end巨集結束可變引數的獲取.然後你就可以在函式裡使
用第二個引數了.如果函式有多個可變引數的,依次呼叫va_arg獲
取各個引數.
如果我們用下面三種方法呼叫的話,都是合法的,但結果卻不一樣:
1)simple_va_fun(100);
結果是:100 -123456789(會變的值)
2)simple_va_fun(100,200);
結果是:100 200
3)simple_va_fun(100,200,300);
結果是:100 200
我們看到第一種呼叫有錯誤,第二種呼叫正確,第三種調用盡管結果
正確,但和我們函式最初的設計有衝突.下面一節我們探討出現這些結果
的原因和可變引數在編譯器中是如何處理的.
(二)可變引數在編譯器中的處理
我們知道va_start,va_arg,va_end是在stdarg.h中被定義成巨集的,
由於1)硬體平臺的不同 2)編譯器的不同,所以定義的巨集也有所不同,下
面以VC++中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 )
定義_INTSIZEOF(n)主要是為了某些需要記憶體的對齊的系統.C語言的函
數是從右向左壓入堆疊的,圖(1)是函式的引數在堆疊中的分佈位置.我
們看到va_list被定義成char*,有一些平臺或作業系統定義為void*.再
看va_start的定義,定義為&v+_INTSIZEOF(v),而&v是固定引數在堆疊的
地址,所以我們執行va_start(ap, v)以後,ap指向第一個可變引數在堆
棧的地址,如圖:
高地址|-----------------------------|
|函式返回地址 |
|-----------------------------|
|....... |
|-----------------------------|
|第n個引數(第一個可變引數) |
|-----------------------------|<--va_start後ap指向
|第n-1個引數(最後一個固定引數)|
低地址|-----------------------------|<-- &v
圖( 1 )
然後,我們用va_arg()取得型別t的可變引數值,以上例為int型為例,我
們看一下va_arg取int型的返回值:
j= ( *(int*)((ap += _INTSIZEOF(int))-_INTSIZEOF(int)) );
首先ap+=sizeof(int),已經指向下一個引數的地址了.然後返回
ap-sizeof(int)的int*指標,這正是第一個可變引數在堆疊裡的地址
(圖2).然後用*取得這個地址的內容(引數值)賦給j.
高地址|-----------------------------|
|函式返回地址 |
|-----------------------------|
|....... |
|-----------------------------|<--va_arg後ap指向
|第n個引數(第一個可變引數) |
|-----------------------------|<--va_start後ap指向
|第n-1個引數(最後一個固定引數)|
低地址|-----------------------------|<-- &v
圖( 2 )
最後要說的是va_end巨集的意思,x86平臺定義為ap=(char*)0;使ap不再
指向堆疊,而是跟NULL一樣.有些直接定義為((void*)0),這樣編譯器不
會為va_end產生程式碼,例如gcc在linux的x86平臺就是這樣定義的.
在這裡大家要注意一個問題:由於引數的地址用於va_start巨集,所
以引數不能宣告為暫存器變數或作為函式或陣列型別.
關於va_start, va_arg, va_end的描述就是這些了,我們要注意的
是不同的作業系統和硬體平臺的定義有些不同,但原理卻是相似的.
(三)可變引數在程式設計中要注意的問題
因為va_start, va_arg, va_end等定義成巨集,所以它顯得很愚蠢,
可變引數的型別和個數完全在該函式中由程式程式碼控制,它並不能智慧
地識別不同引數的個數和型別.
有人會問:那麼printf中不是實現了智慧識別引數嗎?那是因為函式
printf是從固定引數format字串來分析出引數的型別,再呼叫va_arg
的來獲取可變引數的.也就是說,你想實現智慧識別可變引數的話是要通
過在自己的程式裡作判斷來實現的.
另外有一個問題,因為編譯器對可變引數的函式的原型檢查不夠嚴
格,對程式設計查錯不利.如果simple_va_fun()改為:
void simple_va_fun(int i, ...)
{
va_list arg_ptr;
char *s=NULL;
va_start(arg_ptr, i);
s=va_arg(arg_ptr, char*);
va_end(arg_ptr);
printf("%d %s\n", i, s);
return;
}
可變引數為char*型,當我們忘記用兩個引數來呼叫該函式時,就會出現
core dump(Unix) 或者頁面非法的錯誤(window平臺).但也有可能不出
錯,但錯誤卻是難以發現,不利於我們寫出高質量的程式.
以下提一下va系列巨集的相容性.
System V Unix把va_start定義為只有一個引數的巨集:
va_start(va_list arg_ptr);
而ANSI C則定義為:
va_start(va_list arg_ptr, prev_param);
如果我們要用system V的定義,應該用vararg.h標頭檔案中所定義的
巨集,ANSI C的巨集跟system V的巨集是不相容的,我們一般都用ANSI C,所以
用ANSI C的定義就夠了,也便於程式的移植.
1:當無法列出傳遞函式的所有實參的型別和數目時,可用省略號指定引數表
void foo(...);
void foo(parm_list,...);
2:函式引數的傳遞原理
函式引數是以資料結構:棧的形式存取,從右至左入棧.eg:
先介紹一下可變引數表的呼叫形式以及原理:
首先是引數的記憶體存放格式:引數存放在記憶體的堆疊段中,在執行函式的時候,從最後一個開始入棧。因此棧底高地址,棧頂低地址,舉個例子如下:
void func(int x, float y, char z);
那麼,呼叫函式的時候,實參 char z 先進棧,然後是 float y,最後是 int x,因此在記憶體中變數的存放次序是 x->y->z,因此,從理論上說,我們只要探測到任意一個變數的地址,並且知道其他變數的型別,通過指標移位運算,則總可以順藤摸瓜找到其他的輸入變數。
下面是 <stdarg.h> 裡面重要的幾個巨集定義如下:
typedef char* va_list;
void va_start ( va_list ap, prev_param ); /* ANSI version */
type va_arg ( va_list ap, type );
void va_end ( va_list ap );
va_list 是一個字元指標,可以理解為指向當前引數的一個指標,取參必須通過這個指標進行。
<Step 1> 在呼叫引數表之前,定義一個 va_list 型別的變數,(假設va_list 型別變數被定義為ap);
<Step 2> 然後應該對ap 進行初始化,讓它指向可變引數表裡面的第一個引數,這是通過 va_start 來實現的,第一個引數是 ap 本身,第二個引數是在變參表前面緊挨著的一個變數,即“...”之前的那個引數;
<Step 3> 然後是獲取引數,呼叫va_arg,它的第一個引數是ap,第二個引數是要獲取的引數的指定型別,然後返回這個指定型別的值,並且把 ap 的位置指向變參表的下一個變數位置;
<Step 4> 獲取所有的引數之後,我們有必要將這個 ap 指標關掉,以免發生危險,方法是呼叫 va_end,他是輸入的引數 ap 置為 NULL,應該養成獲取完引數表之後關閉指標的習慣。
例如 int max(int n, ...); 其函式內部應該如此實現:
int max(int n, ...) { // 定參 n 表示後面變引數量,定界用,輸入時切勿搞錯
va_list ap; // 定義一個 va_list 指標來訪問引數表
va_start(ap, n); // 初始化 ap,讓它指向第一個變參,n之後的引數
int maximum = -0x7FFFFFFF; // 這是一個最小的整數
int temp;
for(int i = 0; i < n; i++) {
temp = va_arg(ap, int); // 獲取一個 int 型引數,並且 ap 指向下一個引數
if(maximum < temp) maximum = temp;
}
va_end(ap); // 善後工作,關閉 ap
return max;
}
// 在主函式中測試 max 函式的行為(C++ 格式)
int main() {
cout << max(3, 10, 20, 30) << endl;
cout << max(6, 20, 40, 10, 50, 30, 40) << endl;
}
基本用法闡述至此,可以看到,這個方法存在兩處極嚴重的漏洞:其一,輸入引數的型別隨意性,使得引數很容易以一個不正確的型別獲取一個值(譬如輸入一個float,卻以int型去獲取他),這樣做會出現莫名其妙的執行結果;其二,變參表的大小並不能在執行時獲取,這樣就存在一個訪問越界的可能性,導致後果嚴重的 RUNTIME
ERROR。
#include <iostream>
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
3:獲取省略號指定的引數
在函式體中宣告一個va_list,然後用va_start函式來獲取引數列表中的引數,使用完畢後呼叫va_end()結束。像這段程式碼:
void TestFun(char* pszDest, int DestLen, const char* pszFormat, ...)
{
va_list args;
va_start(args, pszFormat); //一定要“...”之前的那個引數
_vsnprintf(pszDest, DestLen, pszFormat, args);
va_end(args);
}
4.va_start使argp指向第一個可選引數。va_arg返回引數列表中的當前引數並使argp指向引數列表中的下一個引數。va_end把argp指標清為NULL。函式體內可以多次遍歷這些引數,但是都必須以va_start開始,並以va_end結尾。
1).演示如何使用引數個數可變的函式,採用ANSI標準形式
#include 〈stdio.h〉
#include 〈string.h〉
#include 〈stdarg.h〉
/*函式原型宣告,至少需要一個確定的引數,注意括號內的省略號*/
int demo( char, ... );
void main( void )
{
demo("DEMO", "This", "is", "a", "demo!", "");
}
/*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);
if ( strcmp( para, "") == 0 )
break;
printf("Parameter #%d is: %s/n", argno, para);
argno++;
}
va_end( argp );
/*將argp置為NULL*/
return 0;
}
2)//示例程式碼1:可變引數函式的使用
#include "stdio.h"
#include "stdarg.h"
void simple_va_fun(int start, ...)
{
va_list arg_ptr;
int nArgValue =start;
int nArgCout=0; //可變引數的數目
va_start(arg_ptr,start); //以固定引數的地址為起點確定變參的記憶體起始地址。
do
{
++nArgCout;
printf("the %d th arg: %d/n",nArgCout,nArgValue); //輸出各引數的值
nArgValue = va_arg(arg_ptr,int); //得到下一個可變引數的值
} while(nArgValue != -1);
return;
}
int main(int argc, char* argv[])
{
simple_va_fun(100,-1);
simple_va_fun(100,200,-1);
return 0;
}
3)//示例程式碼2:擴充套件——自己實現簡單的可變引數的函式。
下面是一個簡單的printf函式的實現,參考了<The C Programming Language>中的例子
#include "stdio.h"
#include "stdlib.h"
void myprintf(char* fmt, ...) //一個簡單的類似於printf的實現,//引數必須都是int 型別
{
char* pArg=NULL; //等價於原來的va_list
char c;
pArg = (char*) &fmt; //注意不要寫成p = fmt !!因為這裡要對//引數取址,而不是取值
pArg += sizeof(fmt); //等價於原來的va_start
do
{
c =*fmt;
if (c != '%')
{
putchar(c); //照原樣輸出字元
}
else
{
//按格式字元輸出資料
switch(*++fmt)
{
case'd':
printf("%d",*((int*)pArg));
break;
case'x':
printf("%#x",*((int*)pArg));
break;
default:
break;
}
pArg += sizeof(int); //等價於原來的va_arg
}
++fmt;
}while (*fmt != '/0');
pArg = NULL; //等價於va_end
return;
}
int main(int argc, char* argv[])
{
int i = 1234;
int j = 5678;
myprintf("the first test:i=%d/n",i,j);
myprintf("the secend test:i=%d; %x;j=%d;/n",i,0xabcd,j);
system("pause");
return 0;
}