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:

Mydata&operator

{

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限制符。该规则与返回一个字面常量是一样的。