3.5 列表初始化
类别:所有人
3.5.1 初始化列表
在C++98中,标准允许使用花括号"{}"对数组元素进行统一的集合(列表)初始值设定,比如:
int arr[5]={0};
int arr[]={1,2,3,4};
这些都是合法的表达式。不过一些自定义类型,却无法享受这样便利的初始化。通常,如标准程序库中的vector这样的容器,总是需要声明对象-循环初始化这样的重复动作,这对于使用模板的泛型编程无疑是非常不利的。
在2.7节中,我们看到了C++11对类成员的快速就地初始化。有一种初始化形式就是使用花括号的集合(列表)初始化。而事实上,在C++11中,集合(列表)的初始化已经成为C++语言的一个基本功能,在C++11中,这种初始化的方法被称为“初始化列表”(initializer list)。让我们来看看代码清单3-29所示的这个例子。
代码清单3-29
include <vector>
include <map>
using namespace std;
int a[]={1,3,5};//C++98通过,C++11通过
int b[]{2,4,6};//C++98失败,C++11通过
vector<int> c{1,3,5};//C++98失败,C++11通过
map<int,float>d=
{{1,1.0f},{2,2.0f},{5,3.2f}};//C++98失败,C++11通过
//编译选项:g++ -c-std=c++11 3-5-1.cpp
在代码清单3-29中,我们看到了变量b、c、d,在C++98的情况下均无法通过编译,在C++11中,却由于列表初始化的存在而可以通过编译。这里,列表初始化可以在“{}”花括号之前使用等号,其效果与不带使用等号的初始化相同。
这样一来,自动变量和全局变量的初始化在C++11中被丰富了。程序员可以使用以下几种形式完成初始化的工作:
❑等号“=”加上赋值表达式(assignment-expression),比如int a=3+4。
❑等号“=”加上花括号式的初始化列表,比如int a={3+4}。
❑圆括号式的表达式列表(expression-list),比如int a(3+4)。
❑花括号式的初始化列表,比如int a{3+4}。
而后两种形式也可以用于获取堆内存new操作符中,比如:
int*i=new int(1);
double*d=new double{1.2f};
这在C++11中也是合法的表达式。
代码清单3-29中可能令读者比较惊讶的是,使用初始化列表对vector、map等非内置的复杂的数据类型进行初始化竟然也是可以的。进一步地,读者可能会猜测是否初始化列表是专属于内置类型、数组,以及标准模板库中容器的功能呢?
事实并非如此,如同我们所提到的,在C++11中,标准总是倾向于使用更为通用的方式来支持新的特性。标准模板库中容器对初始化列表的支持源自<initializer_list>这个头文件中initialize_list类模板的支持。程序员只要#include了<initializer_list>头文件,并且声明一个以initialize_list<T>模板类为参数的构造函数,同样可以使得自定义的类使用列表初始化。让我们来看一看代码清单3-30的例子。
代码清单3-30
include <vector>
include <string>
using namespace std;
enum Gender{boy,girl};
class People{
public:
People(initializer_list<pair<string,Gender>>l){//initializer_list的构造函数
auto i=l.begin();
for(;i!=l.end();++i)
data.push_back(*i);
}
private:
vector<pair<string,Gender>>data;
};
People ship2012={{"Garfield",boy},{"HelloKitty",girl}};
//编译选项:g++ -c-std=c++11 3-5-2.cpp
在代码清单3-30中,我们为类People定义了一个使用initializer_list<pair<string,Gender>>模板类作为参数的构造函数。这里我们使用了C++11的auto关键字来自动类型推导以简化代码的编写(其意义比较明显,这里就不展开解释了,详情请查看4.2节)。由于该构造函数的存在,ship2012声明就可以使用列表初始化了。事实上,编写一个列表初始化的构造函数并不困难。对于旧有的代码,列表初始化构造函数还常常可以调用已有的代码来实现。
同样的,函数的参数列表也可以使用初始化列表,如代码清单3-31所示。
代码清单3-31
include <initializer_list>
using namespace std;
void Fun(initializer_list<int> iv){}
int main(){
Fun({1,2});
Fun({});//空列表
}
//编译选项:g++ -std=c++11 3-5-3.cpp
在代码清单3-31中,定义了一个可以接受初始化列表的函数Fun。同理,类和结构体的成员函数也可以使用初始化列表,包括一些操作符的重载函数。而在代码清单3-32所示的这个例子中,我们利用了初始化列表重载了operator[],并且重载了operator=以及使用辅助的数组。虽然这个例子比较复杂,但重载的效果还是能够让人感觉眼前一亮的。
代码清单3-32
include <iostream>
include <vector>
using namespace std;
class Mydata{
public:
{
for(auto i=l.begin();i!=l.end();++i)
idx.push_back(*i);
return*this;
}
Mydata&operator=(int v)
{
if(idx.empty()!=true){
for(auto i=idx.begin();i!=idx.end();++i){
d.resize((i>d.size())?i:d.size());
d[*i-1]=v;
}
idx.clear();
}
return*this;
}
void Print(){
for(auto i=d.begin();i!=d.end();++i)
cout<<*i<<"";
cout<<endl;
}
private:
vector<int> idx;//辅助数组,用于记录index
vector<int> d;
};
int main(){
Mydata d;
d[{2,3,5}]=7;
d[{1,4,5,8}]=4;
d.Print();//4 7 7 4 4 0 0 4
}
//编译选项:g++ -std=c++11 3-5-4.cpp
在代码清单3-32中,我们看到自定义类型Mydata拥有一个以前所有C++代码都不具备的功能,即可以在[]符号中使用列表,将设置数组中的部分为一个指定的值。在这里我们先把数组的第2、3、5位设为数值7,而后又将其1、4、5、8位设为数值4,最终我们得到数组的内容为“4 7 7 4 4 0 0 4”。读者可以自行分析一下代码的实现方式(这段代码比较粗糙,读者应该重点体会初始化列表带来的编程上的灵活性)。当然,由于内置的数组不能重载operator[],我们也就无法为其实现相应的功能。
此外,初始化列表还可以用于函数返回的情况。返回一个初始化列表,通常会导致构造一个临时变量,比如:
vector<int> Func(){return{1,3};}
当然,跟声明时采用列表初始化一样,列表初始化构造成什么类型是依据返回类型的,比如:
deque<int> Func2(){return{3,5};}
上面的返回值就是以deque<int>列表初始化构造函数而构造的。而跟普通的字面量相同,如果返回值是一个引用类型的话,则会返回一个临时变量的引用。比如:
const vector<int> &Func1(){return{3,5};}
这里注意,必须要加const限制符。该规则与返回一个字面常量是一样的。