C++可變參數(shù)的實現(xiàn)方法
可變參數(shù)的實現(xiàn)要解決三個問題:
1.如何調(diào)用帶有可變參數(shù)的函數(shù)
2.如何編譯有可變參數(shù)的程序
3.在帶有可變參數(shù)的函數(shù)體中如何持有可變參數(shù)
第一個問題, 調(diào)用時在可以傳入可變參數(shù)的地方傳入可變參數(shù)即可,當然,還有一些需要注意的地方,后面會提到。
第二個問題,編譯器需要在編譯時采用一種寬松的檢查方案,,這會帶來一些問題, 比如對編程查錯不利。
第三個是我在這里要關心的問題,先以C語言為例分析其實現(xiàn)原理。
printf和scanf是C語言標準庫中最常見的可變參數(shù)函數(shù), printf的簽名是
int printf(const char* format, ...);
其中,... 表示可變參數(shù),現(xiàn)在模仿printf寫一個簡單的例子。
一、一個簡單了例子:
#include <windows.h>
#include <stdio.h>
void VariableArgumentMethod(int argc, ...);
int main(){
VariableArgumentMethod(6, 4, 7, 3, 0, 7, 9);
return 0;
}
void VariableArgumentMethod(int argc, ...){
// 聲明一個指針, 用于持有可變參數(shù)
va_list pArg;
// 將 pArg 初始化為指向第一個參數(shù)
va_start(pArg, argc);
// 輸出參數(shù)
for(int i = 0; i != argc; ++i){
// 獲取 pArg 所指向的參數(shù)并輸出
printf("%d, ", va_arg(pArg, int) );
}
va_end(pArg);
}
void VariableArgumentMethod(int argc, ...)是一個可變參數(shù)函數(shù),這個函數(shù)用于將 argc 指定個數(shù)的可變參數(shù)輸出。
VariableArgumentMethod(6, 4, 7, 3, 0, 7, 9); 是對這個函數(shù)的調(diào)用,第一個實參 6 表示后面跟了 6 個參數(shù)。
在 VariableArgumentMethod 的函數(shù)體中:
1. va_list pArg;
定義了一個用于持有可變參數(shù)的指針,通過將這個指針在傳入的可變參數(shù)表中移動,可以持有第一個可變參數(shù)。
2. va_start(pArg, argc);
讓 pArg 指向可變參數(shù)列表中的第一個參數(shù)。argc 是一個用來定位的參數(shù),因為可變參數(shù)是從 argc 后開始的,后面會說明為什么要這樣定位。
3. va_arg(pArg, int);
這句話放在循環(huán)體中,用于取出可變參數(shù)表中的參數(shù)。并且,它會讓 pArg 移向下個可變參數(shù)(如果已經(jīng)到達末尾,則它將指向一個沒有意義的地址)。
4. va_end(pArg);
給 pArg 清零,個人認為在這里可有可無,因為 pArg 已經(jīng)不需要了。
就這樣,VariableArgumentMethod 函數(shù)體遍歷了可變參數(shù)表中傳入的參數(shù),并用printf("%d, ", va_arg(pArg, int) ) 進行了輸出。
二、實現(xiàn)細節(jié)
1. 先了解一下編譯器如何處理傳遞參數(shù)這個問題的。
編譯器是將參數(shù)壓入棧中進行傳遞的。傳遞實參的時候,編譯器會從實參列表中,按從右到左的順序?qū)?shù)入棧,對于 VariableArgumentMethod(6, 4, 7, 3, 0, 7, 9)調(diào)用,則入棧的順序是 9, 7, 0, 3, 7, 4, 6 (注意沒有可變參數(shù)與不可變參數(shù)之分)。由于棧的地址是從高到低的,所以實參入棧后,實參在棧中的分布如下圖??梢钥闯?,實參在棧中,還是保持了左邊參數(shù)處于低地址,右邊參數(shù)處于高地址的狀態(tài)。OK,知道這些就夠了。
低地址 高地址
... |
6 |
4 |
7 |
3 |
0 |
7 |
9 |
... |
棧
2. va_list, va_start, va_arg 和 va_end
va_list 是一個定義的指針類型,va_start, va_arg 和 va_end 都是C語言用于處理可變參數(shù)而定義的宏,在stdarg.h文件中。由于硬件平臺的不同,編譯器的不同,導致它們的定義也有所不同,但基本思路相同。以下是相關宏的定義。
typedef char * va_list;
#define _ADDRESSOF(v) ( &(v) )
#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
#define va_start(ap,v) ( ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v) )
#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
#define va_end(ap) ( ap = (va_list)0 )
可以看出,此處引入了另外兩個宏 _ADDRESSOF 和 _INTSIZEOF。
_ADDRESSOF(v) 是用于獲取變量地址的,這一眼就能看出來;
_INTSIZEOF(n) 是用于對齊的。(什么是對齊呢?這是因為棧的結構導致的,在 32 位機中,棧中每個單元都是占 4 個字節(jié)的,這往往是一個 int 型的長度,但實際傳過來的參數(shù)可能并不正好是 4 個字節(jié),或者正好是 4 的倍數(shù)個字節(jié),就好像坐車時不會賣半個座位給乘客一樣,如果傳入的數(shù)據(jù)沒有正好占 4 個或 4 的倍數(shù)個字節(jié),則需要對齊(補齊)。至于為什么這個表達式能夠?qū)R,需要分析一下);
va_start(ap,v) 中,ap 是用于持有可變參數(shù)的指針, v 是最后一個非可變參數(shù)的參數(shù),(va_list)_ADDRESSOF(v) 獲取 v 的地址,并轉(zhuǎn)為 va_list 類型的,v 是最后一個非可變參數(shù)的參數(shù),在本例中應為 6, 在上圖中處理棧的低地址端,_INTSIZEOF(v) 獲取了一個對齊地址,這里應為 4, 兩個相加后,即指向了第一個可變參數(shù),即上圖中的 4, 將這個值賦給 ap 后,就讓 ap 指向了第一個可變參數(shù)。(從這里可以看出,將va_list 定義為 char* 是很有用的,因為 char 長度為一個字節(jié),便于指針運算);
va_arg(ap,t) 中,ap 是用于持有可變參數(shù)的指針,t 是要獲取參數(shù)的類型,ap += _INTSIZEOF(t) 讓 ap 指向下一個參數(shù),但是,此處還需要獲取當前參數(shù)的值,所以又將表達式減回來,返回的應是一個 va_list(char*) 型的指針,因此要轉(zhuǎn)型為 t* 后再進行解引用運算,得到當前參數(shù)的值。(注意這里有個將 ap 移向下一個參數(shù)又減回來的操作,本人感覺不太好,一方面這里有個浪費的操作,對性能會有一些影響,另一方面,我更希望將取當前值的操作和移向下一個的操作分離,這樣可以讓程序員有更多的控制,并且容易理解。)
va_end(ap) 則是讓 ap 指向一個空地址。
通過以上分析,可以發(fā)現(xiàn),C 語言中可變參數(shù)是從棧中按順序訪問的,過程中所使用的三個宏,也只是對操作的簡單包裝,完全可以自己編程實現(xiàn)。而且,參數(shù)的類型和個數(shù)是不能直接確定的,在本例中,VariableArgumentMethod 的第一個參數(shù)用于指定參數(shù)的個數(shù),而參數(shù)的類型約定為整形,這樣程序才能正常運行,再說到 printf,它之所以能識別參數(shù)的個數(shù),是因為它的第一個參數(shù)中必須要描述后面參數(shù)的格式字符串,這正是一開始所提到的第一個問題中說到的要注意的問題。這也是它被很多人所詬病的原因,但是,本人認為這種方式是很好的,后面會與 java 和 .net 的實現(xiàn)方式進行比較。
三、java 和 .net 實現(xiàn)可變參數(shù)的方式。
java 從1.5以后,開始支持可變參數(shù),其定義語法為:
void testMethod(String ... args)
對于這個方法,可以這樣調(diào)用:testMethod("gly", "zxy", "ChenFei");
.net 也支持可變參數(shù),其定義語法為:
void TestMethod(params string[] args)
對于這個方法,可以這樣調(diào)用:TestMethod("gly", "zxy", "ChenFei");
在 java 和 .net 中,對于可變參數(shù)的實現(xiàn)基本是一樣的:編譯器在編譯時,將方法簽名中的可變參數(shù)視為相應類型的數(shù)組,編譯相應的調(diào)用時,根據(jù)實參生成一個數(shù)組,將參數(shù)裝入到數(shù)組中進行傳遞,而在可變參數(shù)方法的方法體中,按使用數(shù)組的方式使用可變參數(shù)。
四、兩種實現(xiàn)方式的比較
C 語言的實現(xiàn)方式與 java .net 的實現(xiàn)方式相比,C 語言需要程序員做更多的工作,而且,確實增加了出錯的機會,java .net 的實現(xiàn)方式可以很容易的確定參數(shù)的類型和個數(shù),這些 C 的實現(xiàn)中是沒有的,但是 java .net 的實現(xiàn)方式會生成臨時數(shù)組,當然 java .net 有垃圾回收機制,但是,垃圾什么時候被回收是不確定的,而且是代價很大的,垃圾回收是個好東西,但我不喜歡,我認為不需要的東西應該立即釋放,這是完美的一個方面的體現(xiàn)。C 中沒有這個問題,參數(shù)的個數(shù)和類型問題可以靠約定或指定來解決,而這兩個問題在 java 和 .net 中,參數(shù)個數(shù)其實是間接傳遞過去了(數(shù)組的長度),參數(shù)類型則是在方法簽名中約定了。當然,java .net 的設計目標和 C 語言不同,這里說多了。
相關文章
Qt串口通信開發(fā)之QSerialPort模塊Qt串口通信接收數(shù)據(jù)不完整的解決方法
這篇文章主要介紹了Qt串口通信開發(fā)之QSerialPort模塊Qt串口通信接收數(shù)據(jù)不完整的解決方法,需要的朋友可以參考下2020-03-03Qt5+QMediaPlayer實現(xiàn)音樂播放器的示例代碼
這篇文章主要為大家詳細介紹了如何利用Qt5和QMediaPlayer實現(xiàn)簡易的音樂播放器,文中的示例代碼講解詳細,具有一定的借鑒價值,需要的可以參考一下2022-12-12string,CString,char*之間的轉(zhuǎn)化
下面是MFC/C++/C中字符類型CString, int, string, char*之間的轉(zhuǎn)換的說明與舉例,經(jīng)常用的東西,相信對于用C/C++的朋友,還是比較有用的2013-03-03