可变参数入门

2019-09-05 21:36:57


由于本人是个蒟蒻,并且语文不好,所以如果本文有任何错误,请指出,我将尽快修正。

1.可能需要的知识

  • 子函数的编写(必需)

  • C++的类型

  • 强制类型转换

  • 指针/迭代器

  • auto

  • 模板

2.引言

假如wenge问你:

如何不用for,求出2个数的最大值?

你说,这还不简单,

ans=max(a,b);

wenge又问你:

如何不用for,求出3个数的最大值?

你说,这还不简单,

ans=max(a,max(b,c));

wenge又问你:

如何不用for,求出10个数的最大值?

你说,这还不简单,

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说这太乱了。

你说,这个总行了吧,

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]);

能不能一行写完呢?

能。

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的函数模板声明。

注意到用红色框起来的函数模板声明。

这个函数以initializer_list为参数。initializer_list是啥?

就是一个初始化表。初始化表是啥?让我们看一个例子。

{1,2,3,4,5}

什么?你告诉我这个就是initializer_list?让我们再看一个例子。

vector<int> a={1,2,3,4,5};

如果你还不知道initializer_list是啥,那就再看一个例子。

int a[5]={1,2,3,4,5};

你可能会问了,这不是数组赋值吗?

但是,这个数组赋值用了初始化表,所以{1,2,3,4,5}就是一个initializer_list

initializer_list是C++11的一个新特性。这玩意除了能给数组赋值以外,还可以给自己定义的结构体赋值。比如

#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函数:

#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)),另外一个是使用迭代器。使用迭代器的方法是这样的:

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()函数与stringvectorsize()的作用完全一致,即返回initializer_list中的元素个数。看看下面的例子:

#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作为参数。比如

void print(initializer_list<int> a,initializer_list<char> b);//print()的具体定义略
int main(){
    print({1,2,3},{'a','b','c'});
    return 0;
}

这样使用是合法的。

并且你可以增加一个模板,使一个initializer_list可以支持同种不同类型的数据:

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)。比如下面的代码:

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:

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说起

另外,说起可变参数,我们可能想到最多的例子就是scanfprintf。实际上,它们的确是真真正正的可变参数函数。而scanfprintf是C的原生函数,其诞生早在initializer_list之前。那么scanfprintf是如何实现的呢?

还是看一下函数声明。

那个...是什么?

没错,那就是可变参数的标志。

但是那些参数叫什么呢?

没有名字。

那怎么读取它们呢?

va_list

2)va_list实现可变参数

C/C++函数可以通过在其普通参数后添加逗号和三个点(,...)的方式来接受数量不定的附加参数,而无需相应的参数声明。

特别的,虽然直接使用三个点作为函数的参数是合法的,但是这些参数并不能被读取(后面会说明原因)。不推荐使用这样的函数。

注意三个点必须加在参数列表的最后。

例如:

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_listva_startva_argva_end。在C++中,C++11标准又新增了宏va_copy。所以,va_listva_startva_argva_end是C与C++通用的。

让我们看一个使用va_list实现可变参数的例子:

#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类型的aa即是printint()的参数表,即其包含了包括count在内的所有参数。你可以把va_list看做一个栈(实际上也是这么存储的),参数从右至左入栈。

  2. 然后,我们调用了va_start宏。va_start宏有两个参数,第一个需要写我们之前创建的va_list的名字(在这个例子里是a),第二个参数则写...之前的上一个参数(在这个例子里是count)。你可以想象有一个指针,一开始什么都不指向(即指向NULL),我们调用va_start宏,则这个指针就指向了栈顶,并且一直弹栈,直到指针指向了...之前的上一个参数(在这个例子里是count)的位置。

  3. 然后,我们使用va_arg宏读取函数的参数。va_arg宏有两个参数,第一个需要写我们之前创建的va_list的名字,第二个填写当前所要读取的参数的类型(这也是scanf为什么有那么多占位符的原因,因为它必须获取参数的类型)。你可以认为这个宏先进行弹栈,然后读取栈顶内容并返回。注意,va_arg的第二个参数中,char,char_16t,wchar_t,short以及其signed,unsigned版本要写成intfloat要写成doublechar_32t要写成unsigned long。原因是C/C++的默认类型转换。否则必定RE。这个东西gcc会给出warning。除此之外,如果第二个参数的实际类型不同于va_arg的第二个参数,这个参数会被吃掉。

  4. 最后,使用va_end宏结束函数参数的读取。你可以认为这个宏使指针重新指向NULL,并且删除赋予va_list的内存,使可变参数函数能正确返回。有助于代码的健壮。

同时这个函数也可以使用while完成:

#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

#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

#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版本要写成intfloat要写成doublechar_32t要写成unsigned long。原因是C/C++的默认类型转换。否则必定RE。这个东西gcc会给出warning。除此之外,如果第二个参数的实际类型不同于va_arg的第二个参数,这个参数会被吃掉。然而,写成intva_arg读取的也是int。比如下面的手写printf的错误例子,不会输出char类型的A,而会输出int类型的65A的ASCII码)。 所以想出默认类型转换这个馊主意的人真是个大锑。

#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使用详解

C可变参数的实现