C語言可變引數詳解
C語言可變引數詳解
什麼是可變引數函式
在C語言程式設計中有時會遇到一些引數可變的函式,例如printf()、scanf()
,其函式原型為:
int printf(const char* format,…)
int scanf(const char *format,…)
就拿 printf 來說吧,它除了有一個引數 format 固定以外,後面的引數其個數和型別都是可變的,用三個點“…”作為引數佔位符。
引數列表的構成
任何一個可變引數的函式都可以分為兩部分:固定引數和可選引數。至少要有一個固定引數,其宣告與普通函式引數宣告相同;可選引數由於數目不定(0個或以上),宣告時用"…"表示。固定引數和可選引數共同構成可變引數函式的引數列表。
實現原理
C語言中使用 va_list
系列變參巨集實現變參函式,此處va意為variable-argument(可變引數)。
x86平臺VC6.0編譯器中,stdarg.h標頭檔案內變參巨集定義如下:
typedef char * va_list;
// 把 n 圓整到 sizeof(int) 的倍數
#define _INTSIZEOF(n) ( (sizeof(n)+sizeof(int)-1) & ~(sizeof(int)-1) )
// 初始化 ap 指標,使其指向第一個可變引數。v 是變參列表的前一個引數
#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) )
// 該巨集返回當前變參值,並使 ap 指向列表中的下個變參
#define va_arg(ap, type) ( *(type *)((ap += _INTSIZEOF(type)) - _INTSIZEOF(type)) )
// /將指標 ap 置為無效,結束變參的獲取
#define va_end(ap) ( ap = (va_list)0 )
_INTSIZEOF(n)
_INTSIZEOF
巨集考慮到某些系統需要記憶體地址對齊。從巨集名看應按照sizeof(int)
即棧粒度對齊,引數在記憶體中的地址均為sizeof(int)=4
的倍數。
例如,若1≤sizeof(n)≤4
,則_INTSIZEOF(n)=4
;若5≤sizeof(n)≤8
,則_INTSIZEOF(n)=8
。
va_start(ap,v)
va_start
巨集首先根據(va_list)&v
得到引數 v 在棧中的記憶體地址,加上_INTSIZEOF(v)
即v所佔記憶體大小後,使 ap 指向 v 的下一個引數。在使用的時候,一般用這個巨集初始化 ap 指標,v 是變參列表的前一個引數,即最後一個固定引數,初始化的結果是 ap 指向第一個變參。
va_arg(ap, type)
這個巨集取得 type 型別的可變引數值。首先ap += _INTSIZEOF(type)
,即 ap 跳過當前可變引數而指向下個變參的地址;然後ap-_INTSIZEOF(type)
得到當前變參的記憶體地址,型別轉換後解引用,最後返回當前變參值。
va_end(ap)
va_end 巨集使 ap 不再指向有效的記憶體地址。該巨集的某些實現定義為((void*)0),編譯時不會為其產生程式碼,呼叫與否並無區別。但某些實現中 va_end 巨集用於在函式返回前完成一些必要的清理工作:如 va_start 巨集可能以某種方式修改棧,導致返回操作無法完成,va_end 巨集可將有關修改復原;又如 va_start 巨集可能為引數列表動態分配記憶體以便於遍歷,va_end 巨集可釋放此記憶體。因此,從使用 va_start 巨集的函式中退出之前,必須呼叫一次 va_end 巨集。
程式碼示例
變參巨集無法智慧識別可變引數的數目和型別,因此實現變參函式時需自行判斷可變引數的數目和型別。所以我們就要想一些辦法,比如
- 顯式提供變引數目或設定遍歷結束條件
- 顯式提供變參型別列舉值,或在固定引數中包含足夠的型別資訊(如
printf
函式通過分析format
字串即可確定各變參型別) - 主調函式和被調函式可約定變參的數目和型別
- …
例1:函式通過固定引數指定可變引數個數,列印所有變參值。
#include <stdarg.h>
#include <stdio.h>
void parse_valist_by_num(int arg_cnt, ...);
int main(void)
{
parse_valist_by_num(4,1,2,3,4);
parse_valist_by_num(4,1,2,3);
parse_valist_by_num(4,1,2,3,4,5); //多餘的變參被忽略
}
//第一個引數定義可變引數的個數
void parse_valist_by_num(int arg_cnt, ...)
{
va_list p_args;
va_start(p_args, arg_cnt);
int idx;
int val;
for(idx = 1; idx <= arg_cnt; ++idx){
val = va_arg(p_args, int);
printf("第 %d 個引數: %d\n", idx, val);
}
printf("---------------\n");
va_end(p_args);
}
執行結果如下:
注意第2個結果,第4個引數是一個魔數,這是因為列印出了棧中引數3上方的引數值。
例2:函式定義一個結束標記(-1),呼叫時通過最後一個引數傳遞該標記,列印標記前所有變參值。
#include <stdarg.h>
#include <stdio.h>
void parse_valist_by_flag(int num_1, ...);
int main(void)
{
parse_valist_by_flag(1,-1);
parse_valist_by_flag(1,2,3,5,-1);
parse_valist_by_flag(-1);
}
//函式定義一個結束標記(-1),呼叫時通過最後一個引數傳遞該標記,以結束變參的遍歷列印。
//最後一個引數作為變參結束符(-1),用於迴圈獲取變參內容
void parse_valist_by_flag(int num_1, ...)
{
va_list p_args;
va_start(p_args, num_1);
int idx = 0;
int val = num_1;
while(val != -1){
++idx;
printf("第 %d 個引數: %d\n", idx, val);
val = va_arg(p_args, int); //得到下個變參值
}
va_end(p_args);
printf("---------------\n");
}
執行結果是:
需要注意
va_arg(ap, type)
巨集中的 type 不可指定為以下型別:
- char
- short
- float
在C語言中,呼叫不帶原型宣告或宣告為變參的函式時,主調函式會在傳遞未顯式宣告的引數前對其執行預設引數提升(default argument promotions),將提升後的引數值傳遞給被調函式。
提升操作如下:
- float 型別的引數提升為 double 型別
- char、short 和相應的 signed、unsigned 型別引數提升為 int 型別
- 若 int 型別不能容納原值,則提升為 unsigned int 型別
最後來一張圖,幫助大家理解前文講的巨集。
【完】
參考資料
相關文章
- C語言怎麼實現可變引數C語言
- C語言實現可變引數列表的system介面:巨集__VA_ARGS__C語言
- c++可變模板引數C++
- java 之泛型與可變引數詳解Java泛型
- 關鍵字引數與非關鍵字引數(可變引數)詳解
- C語言可變引數以及printf()、sprintf()、vsprintf() 的區別與聯絡C語言
- C 可變長引數 VS C++11 可變長模板C++
- C語言函式傳遞指標引數的問題詳解C語言函式指標
- C++逆向 可變引數HookC++Hook
- C++11 可變引數模板C++
- C可變引數的實現
- [ASM C/C++] C語言函式的可選性自變數ASMC++C語言函式變數
- C語言 printf詳解C語言
- C語言#define詳解C語言
- Go語言Slice作為函式引數詳解Go函式
- C語言斷言assert詳解C語言
- 淺談C#可變引數paramsC#
- C++ 函式的可變引數C++函式
- C可變引數函式 實現函式
- C語言sizeof()變數、字元、字串C語言變數字元字串
- c語言指標詳解C語言指標
- 詳解C語言函式C語言函式
- c語言列舉詳解C語言
- c語言運算子詳解C語言
- 對 “C語言指標變數作為函式引數” 的個人理解C語言指標變數函式
- GO語言————6.3 傳遞變長引數Go
- C/C++語言精髓 *和&詳解C++
- 【Java】可變引數Java
- Swift: 可變引數Swift
- Java可變引數Java
- Java 可變引數Java
- Swift語言中為外部引數設定預設值可變引數常量引數變數引數輸入輸出引數Swift變數
- C語言裡全域性變數管理C語言變數
- C語言_結構體變數指標做函式引數的使用案例C語言結構體變數指標函式
- C語言-srand種子詳解C語言
- C語言指標詳解(一)C語言指標
- C語言指標詳解(二)C語言指標
- C語言 sizeof函式詳解C語言函式