想要函數使用可變參數,那就必須要包含stdarg.h
這個頭文件,簡單就不說了,我們重新來看看可變參數的定義和使用吧。
1.聲明可變參數
可變參數的聲明有兩點
使用‘…’來代表可變參數可變參數之前必須有一個命名的參數簡單說就是如果你想聲明一個可變參數的函數,那么有兩種形式
func(...) //錯誤,前面必須有一個命名的參數func(xxx,...) //正確,xxx可以用任意的參數代替,比如char *name,int i都可以2.定義可變參數
可變參數的定義和聲明相同,兩者保持一致即可
3.可變參數的使用
要使用可變參數,主要會用到下列幾個函數
#include <stdarg.h>void va_start(va_list ap, last);type va_arg(va_list ap, type);void va_end(va_list ap);void va_copy(va_list dest, va_list src);這里四個函數是參考man手冊上的,最后的va_copy
沒有用過,不太清楚什么情況下需要使用,麻煩各位在留言賜教 剩下的三個我們來一個一個看,這里舉個簡單的例子方便講解
va_list
類型的變量,這個變量就相當于是指向可變參數列表的指針,通過va_start
函數將這個指針賦值,后面就可以通過va_arg
來獲取每一個參數。va_arg
的函數原型里面有個type
,這個type
類型怎么理解呢? 大家可以這樣理解,因為可變參數是沒有聲明參數類型的,那么編譯器怎么去檢測到底類型是否匹配呢?最簡單的辦法就是向上提升,比如
由于默認肯定會向上提升,所以一定要盡量避免以下類型的參數 type絕對不能為以下類型:
char、signed char、unsigned charshort、unsigned shortsigned short、short int、signed short int、unsigned short intfloat因此在示例中,第一個int
類型的數字我用int
類型接收,第二個字符我依然使用int
類型來接收,第三個字符串就必須使用char *
類型的接收,第四個浮點型使用double
類型來接收,如果你不小心寫錯了類型,系統的提示如下(這里我把示例中的double改成了float)
最后的va_end
就相當于是結束標記,一個va_start
必須和一個va_end
對應起來使用才可以。
使用的話我想大家應該都會,那具體原理是什么樣的呢? 要搞清楚原理,首先需要知道參數到底是怎么傳遞進來的,事實上,在進程中,堆棧地址時由高向低分配的,在調用參數的時候,首先入棧的函數參數,接下來是函數的返回地址,再下來是函數的執行代碼,而參數的入棧順序是先入最后一個參數,最后入第一個參數
如上圖所示,參數在堆棧的排列是從高地址向低地址的,實際上具體是宏定義如下
typedef char * va_list; // x86平臺下va_list的定義#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
需要特別介紹下,這個宏是為了求出變量所占內存空間的大小,具體實現如下
網上有位大神對這個函數又深刻的理解,大家可以去看看他的文章_INTSIZEOF(n)這個函數簡單說就是把n
轉化成int
的整數倍,來實現格式對齊,明白了這里,我們接著往下看
va_start
這個應該很好理解,就是說固定參數的地址加上他本身的內存大小,結合上面的圖也就是第一個可變參數的地址,這樣ap
就指向了第一個可變參數,后面我們通過ap
就可以得到其他的參數
va_arg
這個宏寫的有些復雜,我們需要把它拆成兩部分看 1. ap += _INTSIZEOF(t); // 此時指針ap已經指向下一個參數了 /* ap減去當前參數的大小得到當前參數的地址,再把地址強制類型轉換后返回它的值 */ 2. return (t )( ap - _INTSIZEOF(t)) 通過第一步我們讓指針ap
指向了后一個參數,通過第二步返回了當前的參數
va_end
這個宏很簡單,就是清空了指針,記著需要和va_start
配套使用
看了C語言的具體實現,不得不感嘆,大神們真的是把指針使用的淋漓盡致,不過任何事情都有兩方面,這樣做雖然高效快捷,但同時也留下了不小的安全隱患
參考文檔 深入淺出va函數 關于va_arg中的type
新聞熱點
疑難解答