本文實例展示了C++可變參數的函數與模板的實現方法,有助於大家更好的理解可變參數的函數與模板的應用,具體內容如下:
首先,所謂可變參數指的是函數的參數個數可變,參數類型不定的函數。爲了編寫能處理不同數量實參的函數,C++提供了兩種主要的方法:如果所有的實參類型相同,可以傳遞一個名爲initializer_list的標準庫類型;如果實參的類型不同,我們可以編寫可變參數模板。另外,C++還有一種特殊的省略符形參,可以用它傳遞可變數量的實參,不過這種一般只用於與C函數交互的接口程序。
一、可變參數函數
1、initializer_list形參
如果函數的實參數量未知但是全部實參的類型都相同,我們可以使用initializer_list類型的形參(C++11新標準)。和vector一樣,initializer_list也是一種模板類型。下面看看initializer_list提供的一些操作:
#include<initializer_list> // 頭文件
initializer_list<T> lst; // 默認初始化,T類型元素的空列表
initializer_list<T> lst{a,b,c...}; // 初始化爲初始值列表的副本
lst2(lst) // 拷貝或賦值不會拷貝列表中的元素;拷貝後,
lst2 = lst // 原始列表和副本共享元素
lst.size() // 列表中的元素數量
lst.begin() // 返回指向lst中首元素的指針
lst.end() // 返回指向lst中尾元素下一位置的指針
#include<initializer_list> // 頭文件
initializer_list lst; // 默認初始化,T類型元素的空列表
initializer_list lst{a,b,c…}; // 初始化爲初始值列表的副本
lst2(lst) // 拷貝或賦值不會拷貝列表中的元素;拷貝後,
lst2 = lst // 原始列表和副本共享元素
lst.size() // 列表中的元素數量
lst.begin() // 返回指向lst中首元素的指針
lst.end() // 返回指向lst中尾元素下一位置的指針
下面給出一個例子,需要注意的是,含有initializer_list形參的函數也可以同時擁有其他形參。另外,如果想給initializer_list形參傳遞一個實參的序列,必須把序列放在一對花括號內:
string func(initializer_list<string> li)
{
string str("");
for(auto beg=li.begin(); beg!=li.end(); ++beg)
str += *beg;
return str;
}
int main()
{
cout << func({"This"," ","is"," ","C++"}) << endl;
return 0;
}
2、省略符形參
函數可以用省略符形參”…“表示不定參數部分,省略符形參只能出現在形參列表的最後一個位置,它的形式如下:
void foo(parm_list, ...);
// 典型例子
int printf(const char* format, ...)
省略符形參應該僅僅用於C和C++通用的類型,因爲大多數類類型的對象在傳遞給省略符形參時都無法正確拷貝。下面是頭文件中的幾個宏定義:
#include<cstdarg> // C中是<stdarg.h>
// va_list是一種數據類型,args用於持有可變參數。
// 定義typedef char* va_list;
va_list args;
// 調用va_start並傳入兩個參數:第一個參數爲va_list類型的變量
// 第二個參數爲"..."前最後一個參數名
// 將args初始化爲指向第一個參數(可變參數列表)
va_start(args, paramN);
// 檢索參數,va_arg的第一個參數是va_list變量,第二個參數指定返回值的類型
// 每一次調用va_arg會獲取當前的參數,並自動更新指向下一個可變參數。
va_arg(args,type);
// 釋放va_list變量
va_end(args);
下面給出一個例子:
int add_nums(int count,...)
{
int result = 0;
va_list args;
va_start(args, count);
for(int i=0; i<count; ++i)
result += va_arg(args, int);
va_end(args);
return result;
}
int main()
{
cout << add_nums(4, 25, 25, 50, 50) << endl;
return 0;
}
編譯器是將參數壓入棧中進行傳遞的。傳遞實參的時候,編譯器會從實參列表中,按從右到左的順序將參數入棧,對於add_nums(4, 25, 25, 50, 50)的調用,則入棧的順序是 50, 50, 25, 25, 4 (注意沒有可變參數與不可變參數之分)。由於棧的地址是從高到低的,所以在知道了第一個參數地址和參數的類型之後,就可以獲取各個參數的地址。
二、可變參數模板
一個可變參數模板(variadic template)就是一個接受可變數目參數的模板函數或模板類。可變數目的參數被稱爲參數包(parameter packet)。存在兩種參數包:模板參數包(表示零個或多個模板參數)和函數參數包(表示零個或多個函數參數)。
上述說到我們可以使用一個initializer_list來定義一個可接受可變數目實參的函數,但是所有實參必須具有相同的類型。當我們既不知道要處理的實參數目也不知道它們的類型時,我們就需要使用可變參數的函數模板了。我們用一個省略號來指出一個模板參數或函數參數表示一個包:在一個模板參數列表中,class…或typename…指出接下來的參數表示零個或多個類型的列表;一個類型名後面跟一個省略號表示零個或多個給定類型的非類型參數的列表。在函數參數列表中,如果一個參數的類型是一個模板參數包,則此參數也是一個函數參數包。
// Args是一個模板參數包;rest是一個函數參數包
template <typename T, typename...Args>
void foo(const T &t, const Args&...rest);
// Args是一個模板參數包;rest是一個函數參數包
template <typename T, typename…Args>
void foo(const T &t, const Args&…rest);
可變參數函數模板通常是遞歸的。第一步調用處理包中的第一個實參,然後用剩餘的實參調用自身。爲了終止遞歸,我們還需要定義一個非可變參數的函數模板:
// 用來終止遞歸併處理包中最後一個元素
template <typename T>
void print(const T &t)
{
cout << t;
}
// 包中除了最後一個元素之外的其他元素都會調用這個版本的print
template <typename T, typename...Args>
void print(const T &t, const Args&...rest)
{
cout << t << " "; // 打印第一個實參
print(rest...); // 遞歸調用,打印其他實參
}
// 測試
int main()
{
print("string1", 2, 3.14f, "string2", 42);
cout << endl;
return 0;
}
非可變參數版本的print負責終止遞歸併打印初始調用中的最後一個實參。對於最後一次遞歸調用print(42),兩個print版本都是可行的。但是,非可變參數模板比可變參數模板更特例化,因此編譯器選擇非可變參數版本。