什麼是可變參數函數
在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 類型
最後來一張圖,幫助大家理解前文講的宏。
【完】
參考資料
https://www.cnblogs.com/clover-toeic/p/3736748.html
————————————————
版權聲明:本文爲CSDN博主「ARM的程序員敲着詩歌的夢」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/longintchar/article/details/85490103