2.7 快速初始化成员变量

类别:部分人

在C++98中,支持了在类声明中使用等号“=”加初始值的方式,来初始化类中静态成员常量。这种声明方式我们也称之为“就地”声明。就地声明在代码编写时非常便利,不过C++98对类中就地声明的要求却非常高。如果静态成员不满足常量性,则不可以就地声明,而且即使常量的静态成员也只能是整型或者枚举型才能就地初始化。而非静态成员变量的初始化则必须在构造函数中进行。我们来看看下面的例子,如代码清单2-14所示。

代码清单2-14


class Init{

public:

Init():a(0){}

Init(int d):a(d){}

private:

int a;

const static int b=0;

int c=1;//成员,无法通过编译

static int d=0;//成员,无法通过编译

static const double e=1.3;//非整型或者枚举,无法通过编译

static const char*const f="e";//非整型或者枚举,无法通过编译

};

//编译选项:g++ -c 2-7-1.cpp


在代码清单2-14中,成员c、静态成员d、静态常量成员e以及静态常量指针f的就地初始化都无法通过编译(这里,使用g++的读者可能发现就地初始化double类型静态常量e是可以通过编译的,不过这实际是GNU对C++的一个扩展,并不遵从C++标准)。在C++11中,标准允许非静态成员变量的初始化有多种形式。具体而言,除了初始化列表外,在C++11中,标准还允许使用等号=或者花括号{}进行就地的非静态成员变量初始化。比如:


struct init{int a=1;double b{1.2};};


在这个名叫init的结构体中,我们给了非静态成员a和b分别赋予初值1和1.2。这在C++11中是一个合法的结构体声明。虽然这里采用的一对花括号{}的初始化方法读者第一次见到,不过在第3章中,读者会在C++对于初始化表达式的改动发现,花括号式的集合(列表)初始化已经成为C++11中初始化声明的一种通用形式,而其效果类似于C++98中使用圆括号()对自定义变量的表达式列表初始化。不过在C++11中,对于非静态成员进行就地初始化,两者却并非等价的,如代码清单2-15所示。

代码清单2-15


include <string>

using namespace std;

struct C{

C(int i):c(i){};

int c;

};

struct init{

int a=1;

string b("hello");//无法通过编译

C c(1);//无法通过编译

};

//编译选项:g++ -std=c++11-c 2-7-2.cpp


从代码清单2-15中可以看到,就地圆括号式的表达式列表初始化非静态成员b和c都会导致编译出错。

在C++11标准支持了就地初始化非静态成员的同时,初始化列表这个手段也被保留下来了。如果两者都使用,是否会发生冲突呢?我们来看下面这个例子,如代码清单2-16所示。

代码清单2-16


include <iostream>

using namespace std;

struct Mem{

Mem(){cout<<"Mem default,num:"<<num<<endl;}

Mem(int i):num(i){cout<<"Mem,num:"<<num<<endl;}

int num=2;//使用=初始化非静态成员

};

class Group{

public:

Group(){cout<<"Group default.val:"<<val<<endl;}

Group(int i):val('G'),a(i){cout<<"Group.val:"<<val<<endl;}

void NumOfA(){cout<<"number of A:"<<a.num<<endl;}

void NumOfB(){cout<<"number of B:"<<b.num<<endl;}

private:

char val{'g'};//使用{}初始化非静态成员

Mem a;

Mem b{19};//使用{}初始化非静态成员

};

int main(){

Mem member;//Mem default,num:2

Group group;//Mem default,num:2

//Mem,num:19

//Group default.val:g

group.NumOfA();//number of A:2

group.NumOfB();//number of B:19

Group group2(7);//Mem,num:7

//Mem,num:19

//Group.val:G

group2.NumOfA();//number of A:7

group2.NumOfB();//number of B:19

}

//编译选项:g++2-7-3.cpp-std=c++11


在代码清单2-16中,我们定义了有两个初始化函数的类Mem,此外还定义了包含两个Mem对象的Group类。类Mem中的成员变量num,以及class Group中的成员变量a、b、val,采用了与C++98完全不同的初始化方式。读者可以从main函数的打印输出中看到,就地初始化和初始化列表并不冲突。程序员可以为同一成员变量既声明就地的列表初始化,又在初始化列表中进行初始化,只不过初始化列表总是看起来“后作用于”非静态成员。也就是说,初始化列表的效果总是优先于就地初始化的。

相对于传统的初始化列表,在类声明中对非静态成员变量进行就地列表初始化可以降低程序员的工作量。当然,我们只在有多个构造函数,且有多个成员变量的时候可以看到新方式带来的便利。我们来看看下面的例子,如代码清单2-17所示。

代码清单2-17


include <string>

using namespace std;

class Mem{public:

Mem(int i):m(i){}

private:

int m;};

class Group{

public:

Group(){}//这里就不需要初始化data、mem、name成员了

Group(int a):data(a){}//这里就不需要初始化mem、name成员了

Group(Mem m):mem(m){}//这里就不需要初始化data、name成员了

Group(int a,Mem m,string n):data(a),mem(m),name(n){}

private:

int data=1;

Mem mem{0};

string name{"Group"};

};

//编译选项:g++2-7-4.cpp-std=c++11-c


在代码清单2-17中,Group有4个构造函数。可以想象,如果我们使用的是C++98的编译器,我们不得不在Group()、Group(int a),以及Group(Mem m)这3个构造函数中将data、mem、name这3个成员都写进初始化列表。但如果使用的是C++11的编译器,那么通过对非静态成员变量的就地初始化,我们就可以避免重复地在初始化列表中写上每个非静态成员了(在C++98中,我们还可以通过调用公共的初始化函数来达到类似的目的,不过目前在书写的复杂性及效率性上远低于C++11改进后的做法)。

此外,值得注意的是,对于非常量的静态成员变量,C++11则与C++98保持了一致。程序员还是需要到头文件以外去定义它,这会保证编译时,类静态成员的定义最后只存在于一个目标文件中。不过对于静态常量成员,除了const关键字外,在本书第6章中我们会看到还可以使用constexpr来对静态常量成员进行声明。