可变参数入门
wenge
2019-09-05 21:36:57
由于本人是个蒟蒻,并且语文不好,所以如果本文有任何错误,请指出,我将尽快修正。
## 1.可能需要的知识
- 子函数的编写(必需)
- C++的类型
- 强制类型转换
- 指针/迭代器
- auto
- 模板
## 2.引言
假如wenge问你:
如何不用for,求出2个数的最大值?
你说,这还不简单,
```cpp
ans=max(a,b);
```
wenge又问你:
如何不用for,求出3个数的最大值?
你说,这还不简单,
```cpp
ans=max(a,max(b,c));
```
wenge又问你:
如何不用for,求出10个数的最大值?
你说,这还不简单,
```cpp
ans=max(a[1],max(a[2],max(a[3],max(a[4],max(a[5],max(a[6],max(a[7],max(a[8],max(a[9],a[10])))))))));
```
wenge说这太乱了。
你说,这个总行了吧,
```cpp
ans=max(ans,a[1]);
ans=max(ans,a[2]);
ans=max(ans,a[3]);
ans=max(ans,a[4]);
ans=max(ans,a[5]);
ans=max(ans,a[6]);
ans=max(ans,a[7]);
ans=max(ans,a[8]);
ans=max(ans,a[9]);
ans=max(ans,a[10]);
```
能不能一行写完呢?
能。
```cpp
ans=max({a[1],a[2],a[3],a[4],a[5],a[6],a[7],a[8],a[9],a[10]});//C++11
```
但是库函数`max`为什么可以为什么可以这样使用呢?
好了,现在让我们进入正题——C++的两种可变参数。
## 3.C++ STL的可变参数(initializer_list)
**注意:此节为C++11的新特性,编译时请增加命令行选项-std=c++11(这个貌似大家都懂)**
### 1)什么是initializer_list
首先让我们看看max的函数模板声明。
![](https://cdn.luogu.com.cn/upload/image_hosting/uzwgztlb.png)
注意到用红色框起来的函数模板声明。
这个函数以`initializer_list`为参数。`initializer_list`是啥?
就是一个初始化表。初始化表是啥?让我们看一个例子。
```cpp
{1,2,3,4,5}
```
什么?你告诉我这个就是`initializer_list`?让我们再看一个例子。
```cpp
vector<int> a={1,2,3,4,5};
```
如果你还不知道`initializer_list`是啥,那就再看一个例子。
```cpp
int a[5]={1,2,3,4,5};
```
你可能会问了,这不是数组赋值吗?
但是,这个数组赋值用了初始化表,所以`{1,2,3,4,5}`就是一个`initializer_list`。
`initializer_list`是C++11的一个新特性。这玩意除了能给数组赋值以外,还可以给自己定义的结构体赋值。比如
```cpp
#include <iostream>
#include <initializer_list>
struct point{
int x,y;
};
int main(){
point a={1,2};
cout<<a.x<<" "<<a.y;
return 0;
}
```
程序会输出`1 2`。然而我们要讲的是`initializer_list`实现可变参数。
### 2)initializer_list实现可变参数
用`initializer_list`实现可变参数的方式,就是把`initializer_list`作为函数的参数。
现在首先看一看本人实现的`max`函数:
```cpp
#include <iostream>
#include <algorithm>
#include <initializer_list>
using namespace std;
//C++ STL的可变参数
int mymax(initializer_list<int> a){
int ans=-2147483648;//int的最小值
for(auto i:a){
ans=max(i,ans);
}
return ans;
}
int main(){
int a=1,b=2,c=3,d=4,e=5;
cout<<mymax({a,b,c,d,e});
return 0;
}
```
程序输出了5,因为`a,b,c,d,e`的最大值就是5。
现在让我们看看`mymax`这个函数是如何实现的。
首先定义了一个临时变量`ans`存储答案。
现在来看`for`。也许可能有人不明白这个`for`是什么意思。实际上这个`for`的用途是遍历整个`initializer_list`。
比较麻烦的一点是,简单的遍历一个`initializer_list`只有两种方式,一种是使用C++11的`auto`新特性(也就是代码里的`for(auto i:a)`),另外一个是使用迭代器。使用迭代器的方法是这样的:
```cpp
int mymax(initializer_list<int> a){
int ans=-2147483648;//int的最小值
for(initializer_list<int>::iterator i=a.begin();i!=a.end();i++){
ans=max(*i,ans);
}
return ans;
}
```
看上去很麻烦。但是`initializer_list<int>::iterator`这东西可以换成`auto`。但是即使`initializer_list<int>::iterator`可以换成`auto`,个人还是推荐使用`for(auto i:a)`。
但是上述方法只能遍历整个`initializer_list`容器。如何遍历一个`initializer_list`容器的一部分?一个简单的方法是建立一个数组,把`initializer_list`中所有的数据拷贝进这个数组里。
一个`initializer_list`的大小可以用`initializer_list.size()`函数来获取。`initializer_list`类型的`size()`函数与`string`和`vector`的`size()`的作用完全一致,即返回`initializer_list`中的元素个数。看看下面的例子:
```cpp
#include <iostream>
#include <algorithm>
#include <initializer_list>
using namespace std;
int b[10];
int average(initializer_list<int> a){
int ans=0;
int j=1;
for(auto i:a){
b[j]=i;
j++;
}
sort(b+1,b+a.size()+1);
for(int i=2;i<a.size();i++){
ans+=b[i];
}
ans/=(a.size()-2);
return ans;
}
struct point{
int x,y;
};
int main(){
int a=80,b=10,c=40,d=40,e=70;
cout<<average({a,b,c,d,e});
return 0;
}
```
这个例子是求多个数的去除一个最大值和一个最小值的平均值。可以看到,我们将`initializer_list`类型的a拷贝进了预先定义的数组b,再在数组b上执行`sort()`。
你可以在单个函数使用多个`initializer_list`作为参数。比如
```cpp
void print(initializer_list<int> a,initializer_list<char> b);//print()的具体定义略
int main(){
print({1,2,3},{'a','b','c'});
return 0;
}
```
这样使用是合法的。
并且你可以增加一个模板,使一个`initializer_list`可以支持同种不同类型的数据:
```cpp
template<typename T>
void print(initializer_list<T> a);//print()的具体定义略
int main(){
print({114514,1919,810});
print({'c','h','a','r'});
print({"xyzzy","plugh","abracadabra"});
return 0;
}
```
### 3)类型转换问题以及其他需要注意的地方
**首先,因为`initializer_list`是拿大括号括起来的,所以传入一个`initializer_list`参数的时候要拿大括号括起来。**
C++标准要求“`initializer_list`的元素类型都必须相同,但编译器将进行必要的转换”、“但不能进行隐式的窄化转换”(C++ Primer)。比如下面的代码:
```cpp
double a[5]={1.14514,2.33,-3.456789,4.0,5};
//int类型的5被编译器隐式转换成double类型的5.0
long long b[5]={114514ll,233ll,-3456789ll,4ll,5};
//int类型的5被编译器隐式转换成long long类型的5
int c[5]={114514,233,-3456789,4,5.0};
//double类型的5.0被编译器转换成int,属于隐式窄化转换,错误
```
但是在C++ Primer里所说的这个错误并不一定导致CE,比如本人在gcc4.9.2编译,只出现了一个warning。但是**为了避免潜在的CE,应该尽量避免类型转换问题。**
除了类型转换问题,`initializer_list`的元素类型都必须相同,所以**不能将不同类型的元素装进`initializer_list`里。**例如下列代码将会导致CE:
```cpp
void print(initializer_list<int> a);//print()的具体定义略
int main(){
print("string");
return 0;
}
```
另外,即使你使用了多个`initializer_list`,由于每个`initializer_list`的元素类型都必须相同,每个`initializer_list`只能处理一种类型的数据。这导致了一般的`initializer_list`实现的可变参数只能支持单种类型的数据。
## 4.C的可变参数(va_list)
### 1)从scanf和printf说起
另外,说起可变参数,我们可能想到最多的例子就是`scanf`和`printf`。实际上,它们的确是真真正正的可变参数函数。而`scanf`和`printf`是C的原生函数,其诞生早在`initializer_list`之前。那么`scanf`和`printf`是如何实现的呢?
还是看一下函数声明。
![](https://cdn.luogu.com.cn/upload/image_hosting/lb86odpq.png)
那个`...`是什么?
没错,那就是可变参数的标志。
但是那些参数叫什么呢?
没有名字。
那怎么读取它们呢?
用`va_list`。
### 2)va_list实现可变参数
C/C++函数可以通过在其普通参数后添加逗号和三个点(`,...`)的方式来接受数量不定的附加参数,而无需相应的参数声明。
特别的,虽然直接使用三个点作为函数的参数是合法的,但是这些参数并不能被读取(后面会说明原因)。**不推荐使用这样的函数。**
**注意三个点必须加在参数列表的最后。**
例如:
```cpp
void print(int count,...);//合法
void print(...);//合法
void print(int count,...,int count2);//非法,CE
```
C/C++头文件`<stdarg.h>`或`<cstdarg>`提供了对可变参数的支持。`va_list`是一个类型,定义在头文件`<stdarg.h>`或`<cstdarg>`中。`<stdarg.h>`中定义了3个宏,与`va_list`配套使用,分别是`va_list`,`va_start`,`va_arg`和`va_end`。在C++中,C++11标准又新增了宏`va_copy`。所以,`va_list`,`va_start`,`va_arg`和`va_end`是C与C++通用的。
让我们看一个使用`va_list`实现可变参数的例子:
```cpp
#include <iostream>
#include <cstdarg>
using namespace std;
void printint(int count,...){
va_list a;
va_start(a,count);
for(int i=1;i<=count;i++){
int b=va_arg(a,int);
cout<<b<<" ";
}
va_end(a);
}
int main(){
printint(5,114514,233,-3456789,4,5);
return 0;
}
```
这个函数从参数读取数量等同于参数`count`的整数并输出这个程序将会输出`114514 233 -3456789 4 5`。
1. 在函数体内,我们首先定义了一个`va_list`类型的`a`。`a`即是`printint()`的参数表,即其包含了包括`count`在内的所有参数。你可以把`va_list`看做一个栈(实际上也是这么存储的),参数从右至左入栈。
1. 然后,我们调用了`va_start`宏。`va_start`宏有两个参数,第一个需要写我们之前创建的`va_list`的名字(在这个例子里是`a`),第二个参数则写`...`之前的上一个参数(在这个例子里是`count`)。你可以想象有一个指针,一开始什么都不指向(即指向NULL),我们调用`va_start`宏,则这个指针就指向了栈顶,并且一直弹栈,直到指针指向了`...`之前的上一个参数(在这个例子里是`count`)的位置。
1. 然后,我们使用`va_arg`宏读取函数的参数。`va_arg`宏有两个参数,第一个需要写我们之前创建的`va_list`的名字,第二个填写当前所要读取的参数的类型(这也是`scanf`为什么有那么多占位符的原因,因为它必须获取参数的类型)。你可以认为这个宏先进行弹栈,然后读取栈顶内容并返回。**注意,`va_arg`的第二个参数中,`char,char_16t,wchar_t,short`以及其`signed,unsigned`版本要写成`int`,`float`要写成`double`,`char_32t`要写成`unsigned long`。原因是C/C++的默认类型转换。否则必定RE。这个东西gcc会给出warning。除此之外,如果第二个参数的实际类型不同于`va_arg`的第二个参数,这个参数会被吃掉。**
1. 最后,使用`va_end`宏结束函数参数的读取。你可以认为这个宏使指针重新指向NULL,并且删除赋予va_list的内存,使可变参数函数能正确返回。有助于代码的健壮。
同时这个函数也可以使用`while`完成:
```cpp
#include <iostream>
#include <cstdarg>
using namespace std;
void printint(int first,...){
va_list a;
int b=first;
va_start(a,first);
while(b!=-1){
cout<<b<<" ";
b=va_arg(a,int);
}
va_end(a);
}
int main(){
printint(114514,233,-3456789,4,5,-1);
return 0;
}
```
实际上,`va_list`的优点在于其可以接受不同类型的参数,前提是你知道这些参数的类型。下面是一个例子,是本人实现的`printf`,只实现了`%d`和`%c`:
```cpp
#include <iostream>
#include <cstdarg>
using namespace std;
void myprintf(string f,...){
va_list a;
va_start(a,f);
for(int i=0;i<f.size();i++){
if(f[i]=='%'){
i++;
if(f[i]=='d'){;
cout<<va_arg(a,int);
}
if(f[i]=='c'){
cout<<char(va_arg(a,int));
}
}
else cout<<f[i];
}
va_end(a);
}
int main(){
myprintf("test\n%d%c",114514,'A');
return 0;
}
```
这东西也可以有模板。但是由于类型转换方面的问题,要想支持int以下的类型,恐怕是得写一大堆的特化了。(想一想,怎么写)~~由于作者太懒,就不写了~~
那么`va_copy`呢?
实际上,如果你定义多个`va_list`,只有你定义的第一个`va_list`里面有那些参数的数据。`va_copy`有两个参数,类型都是`va_list`,用途是把第二个`va_list`的数据拷贝进第一个`va_list`里。
~~如果参数类型都相同的话谁会用这东西呢?还不如把这些参数拷贝进数组里(滑稽)~~ 所以这个东西用来重复处理具有多个数据类型的可变参数时会很方便。比如下面这个输出两遍的`printf`:
```cpp
#include <iostream>
#include <cstdarg>
using namespace std;
void myprintf(string f,...){
va_list a,b;
va_copy(b,a);
va_start(a,f);
for(int i=0;i<f.size();i++){
if(f[i]=='%'){
i++;
if(f[i]=='d'){;
cout<<va_arg(a,int);
}
if(f[i]=='c'){
cout<<char(va_arg(a,int));
}
}
else cout<<f[i];
}
va_end(a);
va_start(b,f);
for(int i=0;i<f.size();i++){
if(f[i]=='%'){
i++;
if(f[i]=='d'){;
cout<<va_arg(b,int);
}
if(f[i]=='c'){
cout<<char(va_arg(b,int));
}
}
else cout<<f[i];
}
va_end(b);
}
int main(){
myprintf("test\n%d%c",114514,'A');
return 0;
}
```
### 3)其他
之前我们已经知道了,直接使用三个点作为函数的参数是合法的,但是这些参数并不能被读取。为什么呢?因为如果有一个直接使用三个点作为参数的函数,则没有`...`之前的上一个参数,`va_start`将无法使用。然而亲测不使用`va_start`会RE,所以这种函数的参数并不能被读取。
之前我们已经知道了,`va_arg`的第二个参数中,`char,char_16t,wchar_t,short`以及其`signed,unsigned`版本要写成`int`,`float`要写成`double`,`char_32t`要写成`unsigned long`。原因是C/C++的默认类型转换。否则必定RE。这个东西gcc会给出warning。除此之外,如果第二个参数的实际类型不同于`va_arg`的第二个参数,这个参数会被吃掉。**然而,写成`int`,`va_arg`读取的也是`int`。比如下面的手写`printf`的错误例子,不会输出`char`类型的`A`,而会输出`int`类型的`65`(`A`的ASCII码)。** ~~所以想出默认类型转换这个馊主意的人真是个大锑。~~
```cpp
#include <iostream>
#include <cstdarg>
using namespace std;
void myprintf(string f,...){
va_list a;
va_start(a,f);
for(int i=0;i<f.size();i++){
if(f[i]=='%'){
i++;
if(f[i]=='d'){;
cout<<va_arg(a,int);
}
if(f[i]=='c'){
cout<<va_arg(a,int);
//没有转换,正确写法实际上是用强制类型转换
//cout<<char(va_arg(a,int));
}
}
else cout<<f[i];
}
va_end(a);
}
int main(){
myprintf("%c",'A');
return 0;
}
```
**所以,如果有使用更低等的类型(比如`char`)的必要,使用强制类型转换。**
在函数最后,一定要调用`va_end`。
~~不要越界!不要越界!!不要越界!!!~~
~~你们的程序里有千万块内存。只要不越界,这个系统就无法检测到非法读写。~~
~~如果越界,非法读写将被检测到,系统的保护模块将会触发,你们的程序将会RE!~~
~~不要越界!不要越界!!不要越界!!!~~
然后就没了。
## 5.参考资料
http://www.cplusplus.com/
C++ Primer
[va_start和va_end使用详解](https://www.cnblogs.com/hanyonglu/archive/2011/05/07/2039916.html)
[C可变参数的实现](https://blog.csdn.net/jasonyuchen/article/details/77145957)