1.C实现变参函数
C语言中,有时需要变参函数来完成特殊的功能,比如C标准库函数printf()和scanf()。C中提供了省略符“…”能够帮主programmer完成变参函数的书写。变参函数原型申明如下:
type functionname(type param1,...);
- 1
变参函数至少要有一个固定参数,省略号“…”不可省略,比如printf()的原型如下:
int printf(const char *format,...);
- 1
在头文件stdarg.h中定义了三个宏函数用于获取指定类型的实参:
void va_start(va_list arg,prev_param);
type va_arg(va_list arg,type);
void va_end(va_list arg);
- 1
- 2
- 3
va在这里是variable argument(可变参数)的意思,那么变参函数的实现就变得相对简单很多。一般的变参函数处理过程:
①定义一个va_list变量设为va;
②调用va_start()使得va存放变参函数的变参前的一个固定参数的地址;
③不断调用va_arg()使得va指向下一个实参;
④最后调用va_end()表示变参处理完成,将va置空。
原理就是:函数的参数在内存中从低地址向高地址依次存放。
看一个例子:模仿pritnf()的实现[1][1]:
#include<iostream>
#include<stdarg.h>
#include<string.h>
using namespace std;
void func(char *c,...){
int i=0;
double result=0;
va_list arg; //va_list变量
va_start(arg,c); //arg指向固定参数c
while(c[i]!='\0'){
if(c[i]=='%'&&c[i+1]=='d'){
printf("%d",va_arg(arg,int));
i++;
}
else if(c[i]=='%'&&c[i+1]=='f'){
printf("%f",va_arg(arg,double));
i++;
}
else
putchar(c[i]);
i++;
}
va_end(arg);
}
int main(){
int i=100;
double j=100.0;
printf("%d be equal %f\n",i,j);
func("%d be equal %f\n",i,j);
system("pause");
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
程序输出:
100 be equal 100.000000
100 be equal 100.000000
请按任意键继续. . .
- 1
- 2
- 3
C变参函数缺点[2][2]:
(1)缺乏类型检查,容易出现不合理的强制类型转换。在获取实参时,是通过给定的类型进行获取,如果给定的类型与实际参数类型不符,则会出现类型安全性问题,容易导致获取实参失败。
(2)不支持自定义类型。自定义类型在程序中经常用到,比如我们要使用printf()来打印一个Student类型的对象的内容,该用什么格式字符串去指定实参类型,通过C提供的va_list,我们无法提取实参内容。
鉴于以上两点,李健老师在其著作《编写高质量代码改善C++程序的150个建议》建议尽量不要使用C风格的变参函数。
2.C++实现变参函数
为了编写能够处理不同数量实参的函数,C++11提供了两种主要方法:
(1)如果所有实参类型相同,可以传递initializer_list的标准库类型;
(2)如果实参类型不同,可以编写一种特殊的函数,也就是所谓的可变参数模板。
2.1initializer_list形参[3][3]
initializer_list是C++11引入的一种标准库类模板,用于表示某种特定类型的值的数组。initializer_list类型定义在同名的头文件中,它提供的操作有:
initializer_list<T> lst; //默认初始化T类型的空列表。
initializer_list<T> lst{a,b,c,...}; //lst的元素是对应初始值的副本,且列表中的元素是const。
lst2(lst); //拷贝构造一个initializer_list对象,不拷贝列表中的元素,与原始列表共享元素
lst2=lst; //赋值,与原始列表共享元素。
lst.size(); //列表中的元素数量。
lst.begin(); //返回指向lst中首元素的指针。
lst.end(); //返回lst中尾元素下一位置的指针。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
和vector与list一样,initializer_list也是一种模板类型,定义initializer_list对象时必须指明列表中所含元素的类型。与vector和list不同之处在于initializer_list中的元素不可修改,拷贝构造和赋值时元素不会并不会被拷贝。如此设计,让initializer_list更加符合参数通过指针传递的,而非值传递,提高性能。所以C++11采用了initializer_list作为变参函数的形参,下面给出一个打印错误的变参函数:
void error_msg(initializer\_list<string> il){
for(auto beg=il.begin();beg!=il.end())
cout<<*beg<<" ";
cout<<endl;
}
- 1
- 2
- 3
- 4
- 5
2.2可变参数模板
简介:
目前大部分主流编译器的最新版本均支持了C++11标准(官方名为ISO/IEC14882:2011)大部分的语法特性,其中比较难理解的新语法特性可能要属可变参数模板(variadic template)了,GCC 4.6和Visual studio 2013都已经支持变参模板。可变参数模板就是一个接受可变数目参数的函数模板或类模板。可变数目的参数被称为参数包(parameter packet),这个也是新引入 C++ 中的概念,可以细分为两种参数包:
(1)模板参数包(template parameter packet)
表示零个或多个模板参数。
(2)函数参数包(function parameter packet)
表示零个或多个函数参数。
可变参数模板示例:
使用省略号…来指明一个模板的参数包,在模板参数列表中,class...
或typename...
指出接下来的参数表示零个或多个类型参数;一个类型名后面跟一个省略号表示零个或多个给定类型的非类型参数。声明一个带有可变参数个数的模板的语法如下所示:
//1.申明可变参数的类模板
template<typename... Types> class tuple;
tuple<int, string> a; // use it like this
//2.申明可变参数的函数模板
template<typename T,typename... Types> void foo(const T& t,const Types&... rest);
foo<int,float,double,string>(1,2.0,3.0,"lvlv");//use like this
//3.申明可变非类型参数的函数模板(可变非类型参数也可用于类模板)
template<typename T,unsigned... args> void foo(const T& t);
foo<string,1,2>("lvlv");//use like this
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
其中第一条示例中Types就是模板参数包,第二条示例中rest就是函数参数包,第三条示例中args就是非类型模板参数包。
参数包扩展:
现在我们知道parameter packet了,怎么在程序中真正具体地去处理打包进来的“任意个数”的参数呢?也就是说可变参数模板,我们如何进行参数包的扩展,获取传入的参数包中的每一个实参呢?
对于一个参数包,除了可以通过运算符sizeof…来获取参数包中的参数个数,比如:
template<typename... Types> void g(Types... args){
cout<<sizeof...(Types)<<endl; //类型参数数目
cout<<sizeof...(args)<<endl; //函数参数
}
- 1
- 2
- 3
- 4
我们能够对参数包唯一能做的事情就是对其进行扩展,扩展一个包就是将它分解为构成的元素,通过在参数包的右边放置一个省略号…来触发扩展操作,例如:
template<typename T,typename... Types> ostream& print(ostream& os,const T& t,const Types&... rest){
os<<t<<",";
return print(os,rest...);
}
- 1
- 2
- 3
- 4
上面的示例代码中,存在两种包扩展操作:
(1)const Types&... rest
表示模板参数包的扩展,为print函数生成形参列表;
(2)对print的调用中rest...
表示函数参数包的扩展,为print调用生成实参列表。
可变参数函数实例:
可变参数函数通常以递归的方式来获取参数包的每一个参数。第一步调用处理包中的第一个实参,然后用剩余实参调用自身。最后,定义一个非可变参数的同名函数模板来终止递归。我们以自定义的print函数为例,实现如下:
#include <iostream>
using namespace std;
template<typename T> ostream& print(ostream& os,const T& t){
os<<t<<endl; //包中最后一个元素之后打印换行符
}
template<typename T,typename... Types> ostream& print(ostream& os,const T& t,const Types&... rest){
os<<t<<","; //打印第一个实参
print(os,rest...); //递归调用,打印其他实参
}
int main(){
print(cout,10,123.0,"lvlv",1); //例1
print(cout,1,"lvlv0","lvlv1");
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
程序输出:
10,123,lvlv,1
1,lvlv0,lvlv1
- 1
- 2
上面递归调用print,以例1为例,执行的过程如下:
调用 | t | rest… |
---|---|---|
print(cout,10,123.0,”lvlv”,1) | 10 | 123.0,”lvlv”,1 |
print(cout,123.0,”lvlv”,1) | 123.0 | “lvlv”,1 |
print(cout,”lvlv”,1) | “lvlv” | 1 |
print(cout,1),调用非变参版本的print | 1 | 无 |
前三个调用只能与可变参数版本的print匹配,非变参版本是不可行的,因为这三个调用要传递两个以上实参,非可变参数的print只接受两个实参。对于最后一次递归调用print(cout,1)
,两个版本的print都可以,因为这个调用传递两个实参,第一个实参的类型为ostream&,另一个是const T&参数。但是由于非可变参数模板比可变参数模板更加特例化,因此编译器选择非可变参数版本。
[1]编写高质量代码改善C++程序的150个建议.李健.2012:34-35
[2]c /c++变参函数
[3]Stanley B. Lippman著,王刚 杨巨峰译.C++ Primer中文版第五版.2013:197-199