雖然學習C語言很久了,但實際上一直沒有去仔細看C語言中中的可變參函數是如何實現的,最常見的一個就是printf函數,最近閒暇時間看了下,對其有了初步的瞭解。
首先需要了解下C語言的傳遞參數機制,C語言調用函數時,參數保存在棧裏的,並且棧的增長方向是從低地址到高地址的,(棧是一種數據結構,其最顯著的特點就是“先進後出”)並且是從後往前掃描的。具體來說就是最後面參數最先入棧,最前面的參數最後入棧,同時會第一個出棧。
棧的地址,如圖:
高地址
|-----------------------------|
|函數返回地址 |
|-----------------------------|
|....... |
|-----------------------------|
|第n個參數(第一個可變參數) |
|-----------------------------|
|第n-1個參數(最後一個固定參數) |
|-----------------------------|
|...... 第一個參數 ..........|
低地址
C語言的可變參主要通過三個定義的macro來實現,在stdarg.h文件裏,分別如下:
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是自定義的數據類型,一般爲char *, 也有的系統定義爲void *,要實現可變參數,就必須按照要求使用這3個macro。
va_start定義如下:
- #define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
- #define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) ) //得到可變參數中第一個參數的首地址
void va-start(va-list ap,lastfix)是一個宏,它使va-list類型變量ap指向被傳遞給函數的可變參數表中的第一個參數,在第一次調用va-arg和va-end之前,必須首先調用該宏。va-start的第二個參數lastfix是傳遞給被調用函數的最後一個固定參數的標識符。va-start使ap只指向lastfix之外的可變參數表中的第一個參數,很明顯它先得到第一個參數內存地址,然後又加上這個參數的內存大小,就是下個參數的內存地址了。
#define va_arg(ap,type) ( *(type *)((ap += _INTSIZEOF(type)) - _INTSIZEOF(type)) ) //將參數轉換成需要的類型,並使ap指向下一個參數
type va-arg(va-list ap,type)也是一個宏,其使用有雙重目的,第一個是返回ap所指對象的值,第二個是修改參數指針ap使其增加以指向表中下一個參數。va-arg的第二個參數提供了修改參數指針所必需的信息。在第一次使用va-arg時,它返回可變參數表中的第一個參數,後續的調用都返回表中的下一個參數。
#define va_end(ap) ( ap = (va_list)0 )
va-end必須在va-arg讀完所有參數後再調用,否則會產生意想不到的後果。特別地,當可變參數表函數在程序執行過程中不止一次被調用時,在函數體每次處理完可變參數表之後必須調用一次va-end,以保證正確地恢復棧。
一個變參數函數至少需要有一個普通參數,其普通參數可以具有任何類型。
例如我們要實現一個簡單的sum函數,可接受可變數目參數,第一個參數爲後面可變參數的數目,假定後面的可變參數均爲int型,就需要按下面的步驟:
1.函數原型 int sum(int n, ...)
2.聲明一個va_list類型的變量ap並用va_start初始化
va_list ap;
va_start(ap, n);
3.利用va_arg(ap,int)循環獲取下一個參數的值
4.調用va_end(ap)進行終止。
代碼如下:
- #include<iostream>
- using namespace std;
- #include<stdarg.h>
- int sum(int n,...)
- {
- int i , sum = 0;
- va_list vap;
- va_start(vap , n); //指向可變參數表中的第一個參數
- for(i = 0 ; i < n ; ++i)
- sum += va_arg(vap , int); //取出可變參數表中的參數,並修改參數指針vap使其增加以指向表中下一個參數
- va_end(vap); //把指針vap賦值爲0
- return sum;
- }
- int main(void)
- {
- int m = sum(3 , 45 , 89 , 72);
- cout<<m<<endl;
- return 0;
- }
當然與printf相比,這個過於簡單,主要是參數的數目和類型都固定了,printf主要是掃描通過前面的格式化字符串從而確定可變參的數目和類型的,有時間了可以自己嘗試寫下printf。