C語(yǔ)言可變參數(shù)(從stdarg.h到應(yīng)用)
1. 什么是可變參數(shù)函數(shù)
在C語(yǔ)言編程中有時(shí)會(huì)遇到一些參數(shù)可變的函數(shù),例如printf()、scanf()
,其函數(shù)原型為:
就拿 printf 來(lái)說(shuō)吧,它除了有一個(gè)參數(shù) format 固定以外,后面的參數(shù)其個(gè)數(shù)和類(lèi)型都是可變的,用三個(gè)點(diǎn)“…”作為參數(shù)占位符。
2. 參數(shù)列表的構(gòu)成
任何一個(gè)可變參數(shù)的函數(shù)都可以分為兩部分:固定參數(shù)和可選參數(shù)。至少要有一個(gè)固定參數(shù),其聲明與普通函數(shù)參數(shù)聲明相同;可選參數(shù)由于數(shù)目不定(0個(gè)或以上),聲明時(shí)用"…"表示。固定參數(shù)和可選參數(shù)共同構(gòu)成可變參數(shù)函數(shù)的參數(shù)列表。
3. 實(shí)現(xiàn)原理
C語(yǔ)言中使用?va_list
?系列變參宏實(shí)現(xiàn)變參函數(shù),此處va意為variable-argument(可變參數(shù))。
x86平臺(tái)VC6.0編譯器中,stdarg.h頭文件內(nèi)變參宏定義如下:
_INTSIZEOF(n)
_INTSIZEOF
宏考慮到某些系統(tǒng)需要內(nèi)存地址對(duì)齊。從宏名看應(yīng)按照sizeof(int)
即棧粒度對(duì)齊,參數(shù)在內(nèi)存中的地址均為sizeof(int)=4
的倍數(shù)。
例如,若1≤sizeof(n)≤4
,則_INTSIZEOF(n)=4
;若5≤sizeof(n)≤8
,則_INTSIZEOF(n)=8
。
在不考慮內(nèi)存的情況下,庫(kù)函數(shù)中的這條語(yǔ)句?(sizeof(n)+ sizeof(int) - 1) & ~(sizeof(int)-1)背后的意義也可以當(dāng)作一種映射算法。具體上代碼:
相信大家都看出規(guī)律了,這里就不總結(jié)了。另外int num,int mask是有符號(hào)的,當(dāng)形參num為負(fù)數(shù)的時(shí)候,或者形參mask為負(fù)數(shù)的時(shí)候,這樣就還有三種情況,有的也是有規(guī)律的,這里就不贅述了。
va_start(ap,v)
va_start
宏首先根據(jù)(va_list)&v
得到參數(shù) v 在棧中的內(nèi)存地址,加上_INTSIZEOF(v)
即v所占內(nèi)存大小后,使 ap 指向 v 的下一個(gè)參數(shù)。在使用的時(shí)候,一般用這個(gè)宏初始化 ap 指針,v 是變參列表的前一個(gè)參數(shù),即最后一個(gè)固定參數(shù),初始化的結(jié)果是 ap 指向第一個(gè)變參。
va_arg(ap, type)
這個(gè)宏取得 type 類(lèi)型的可變參數(shù)值。首先ap += _INTSIZEOF(type)
,即 ap 跳過(guò)當(dāng)前可變參數(shù)而指向下個(gè)變參的地址;然后ap-_INTSIZEOF(type)
得到當(dāng)前變參的內(nèi)存地址,類(lèi)型轉(zhuǎn)換后解引用,最后返回當(dāng)前變參值。
va_end(ap)
va_end 宏使 ap 不再指向有效的內(nèi)存地址。該宏的某些實(shí)現(xiàn)定義為((void*)0),編譯時(shí)不會(huì)為其產(chǎn)生代碼,調(diào)用與否并無(wú)區(qū)別。但某些實(shí)現(xiàn)中 va_end 宏用于在函數(shù)返回前完成一些必要的清理工作:如 va_start 宏可能以某種方式修改棧,導(dǎo)致返回操作無(wú)法完成,va_end 宏可將有關(guān)修改復(fù)原;又如 va_start 宏可能為參數(shù)列表動(dòng)態(tài)分配內(nèi)存以便于遍歷,va_end 宏可釋放此內(nèi)存。因此,從使用 va_start 宏的函數(shù)中退出之前,必須調(diào)用一次 va_end 宏。
4. 代碼示例
變參宏無(wú)法智能識(shí)別可變參數(shù)的數(shù)目和類(lèi)型,因此實(shí)現(xiàn)變參函數(shù)時(shí)需自行判斷可變參數(shù)的數(shù)目和類(lèi)型。所以我們就要想一些辦法,比如
顯式提供變參數(shù)目或設(shè)定遍歷結(jié)束條件
顯式提供變參類(lèi)型枚舉值,或在固定參數(shù)中包含足夠的類(lèi)型信息(如
printf
函數(shù)通過(guò)分析format
字符串即可確定各變參類(lèi)型)主調(diào)函數(shù)和被調(diào)函數(shù)可約定變參的數(shù)目和類(lèi)型
…
例1:函數(shù)通過(guò)固定參數(shù)指定可變參數(shù)個(gè)數(shù),打印所有變參值
例2:函數(shù)定義一個(gè)結(jié)束標(biāo)記(-1),調(diào)用時(shí)通過(guò)最后一個(gè)參數(shù)傳遞該標(biāo)記,打印標(biāo)記前所有變參值。
需要注意
va_arg(ap, type)
宏中的 type 不可指定為以下類(lèi)型:
char
short
float
在C語(yǔ)言中,調(diào)用不帶原型聲明或聲明為變參的函數(shù)時(shí),主調(diào)函數(shù)會(huì)在傳遞未顯式聲明的參數(shù)前對(duì)其執(zhí)行缺省參數(shù)提升(default argument promotions),將提升后的參數(shù)值傳遞給被調(diào)函數(shù)。
提升操作如下:
float 類(lèi)型的參數(shù)提升為 double 類(lèi)型
char、short 和相應(yīng)的 signed、unsigned 類(lèi)型參數(shù)提升為 int 類(lèi)型
若 int 類(lèi)型不能容納原值,則提升為 unsigned int 類(lèi)型
最后來(lái)一張圖,幫助大家理解前文講的宏。

5. 應(yīng)用
在嵌入式開(kāi)發(fā)中,經(jīng)常使用int fputc(int ch, FILE *f)重映射printf()函數(shù),例如重映射到ITM、串口、oled屏幕等,這種方式只能映射到一個(gè)地方,不是很方便。下面介紹一個(gè)終極方法,實(shí)現(xiàn)可變參數(shù)任意端口打印。這里以oled為例,代碼如下:
6. 參考資料
https://blog.csdn.net/longintchar/article/details/85490103
https://www.cnblogs.com/clover-toeic/p/3736748.html