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語言可變引數以及printf()、sprintf()、vsprintf() 的區別與聯絡C語言
- c++可變模板引數C++
- java 之泛型與可變引數詳解Java泛型
- C語言函式傳遞指標引數的問題詳解C語言函式指標
- C++逆向 可變引數HookC++Hook
- C++11 可變引數模板C++
- C語言 printf詳解C語言
- C語言#define詳解C語言
- Go語言Slice作為函式引數詳解Go函式
- 淺談C#可變引數paramsC#
- c語言運算子詳解C語言
- C語言sizeof()變數、字元、字串C語言變數字元字串
- C/C++語言精髓 *和&詳解C++
- GO語言————6.3 傳遞變長引數Go
- C語言指標詳解(一)C語言指標
- C語言指標詳解(二)C語言指標
- C語言-srand種子詳解C語言
- 對 “C語言指標變數作為函式引數” 的個人理解C語言指標變數函式
- c語言模擬Python的命名引數C語言Python
- 【Java】可變引數Java
- C++ 可變引數模板遞迴展開C++遞迴
- C 語言回撥函式詳解函式
- C語言-變數常量資料型別C語言變數資料型別
- C語言--靜態區域性變數C語言變數
- C語言學習筆記之變數C語言筆記變數
- go-可變引數Go
- 可變引數例項
- Swift 呼叫 Objective-C 的可變引數函式SwiftObject函式
- C語言指標(二) 指標變數 ----by xhxhC語言指標變數
- 關於C99可變引數巨集的例項程式碼講解
- go語言變數Go變數
- C++反射機制:可變引數模板實現C++反射C++反射
- 可變引數va_list
- Java方法05:可變引數Java
- Java - 可變引數的使用Java
- 【重學Java】可變引數Java