C語言:va_start、va_end、va_arg 實現可變長參數
1、可變長參數
即參數的個數不確定,個數可變。例如printf函數的定義:
int printf( const char* format, ...);
2、C語言實現
C語言可變參數通過三個宏(va_start、va_end、va_arg)和一個類型(va_list)實現:
void va_start ( va_list ap, paramN );
參數:
ap: 可變參數列表地址
paramN: 確定的參數
功能:初始化可變參數列表(把函數在 paramN 之後的參數地址放到 ap 中)。
void va_end ( va_list ap );
功能:關閉初始化列表(將 ap 置空)。
type va_arg ( va_list ap, type );
功能:返回下一個參數的值。
va_list :存儲參數的類型信息。
3、用法
(1)首先在函數裡定義一具va_list型的變量,這個變量是指向參數的指針;
(2)然後用va_start宏初始化變量剛定義的va_list變量;
(3)然後用va_arg返回可變的參數,va_arg的第二個參數是你要返回的參數的類型(如果函數有多個可變參數的,依次調用va_arg獲取各個參數);
(4)最後用va_end宏結束可變參數的獲取。
4、注意問題
(1)宏定義在 stdarg.h 中,所以使用時,不要忘了添加頭文件。
(2)設定一個參數結束標志(cplusplus 上說,va_arg 並不能確定哪個參數是最後一個參數)。
(3)類型的匹配
(4)可變參數的類型和個數完全由程序代碼控制,它並不能智能地識別不同參數的個數和類型;
(5)如果我們不需要一一詳解每個參數,只需要將可變列表拷貝至某個緩沖,可用vsprintf函數;
(6)因為編譯器對可變參數的函數的原型檢查不夠嚴格,對編程查錯不利.不利於我們寫出高質量的代碼;
5、實例
#include <stdio.h> #include <stdarg.h> #define END -1 int va_sum (int first_num, ...) { // (1) 定義參數列表 va_list ap; // (2) 初始化參數列表 va_start(ap, first_num); int result = first_num; int temp = 0; // 獲取參數值 while ((temp = va_arg(ap, int)) != END) { result += temp; } // 關閉參數列表 va_end(ap); return result; } int main () { int sum_val = va_sum(1, 2, 3, 4, 5, END); printf ("%d", sum_val); return 0; }
6、源碼分析
typedef char * va_list;
#define _crt_va_start(ap,v) ( ap = (va_list)&(v) + _INTSIZEOF(v) )//獲取可變參數列表的第一個參數的地址(ap是類型為va_list的指針,v是可變參數最左邊的參數)
#define _crt_va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )//獲取可變參數的當前參數,返回指定類型並將指針指向下一參數(t參數描述了當前參數的類型)
#define _crt_va_end(ap) ( ap = (va_list)0 ) //清空va_list可變參數列表
#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )//獲取類型占用的空間長度,最小占用長度為int的整數倍
(1)定義_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宏,所以參數不能聲明為寄存器變量或作為函數或數組類型.