程序员应了解的那些事(6)C++类中的数据成员能否在定义类的时候就初始化?/类的数组不能使用成员初始化表进行初始化等
【问题1】C++类中的数据成员能否在定义类的时候就初始化?
(注:在最新的C++11标准中已经支持在定义类的时候进行成员初始化。但初始化不是在编译时发生,这只是语法糖,而成员变量是在默认构造函数中初始化的)
如前所述,它是语法糖。但由于规则可能太难记住,通过一个逻辑实验来验证:
struct foo { int size = 3; };
template<int N>
struct experiment { enum { val = N }; };
假设初始化发生在编译时,然后我们可以写
foo a;
experiment<a.size> b;
编译失败!有人可能认为失败是由于foo::size不稳定所以让我们试试
struct foo { const int size = 3; }; // constexpr instead of const would fail as well
再次,gcc通知我们'a'的值不能用于常量表达式或(更清楚地)visual studio 2013告诉我们
错误C2975:'N':'example'的模板参数无效,是预期的编译时常量表达式
因此,我们推断初始化不会在编译时发生。
继续解答问题1。一般而言,类中的数据成员在定义类的时候是不能初始化的。这是一个可以值得探讨的话题,每个人的解释都不一样,大部分人都认为这是编译语法的规定,那么有没有考虑为什么会有这个规定呢? 可能的原因有:
(1)类只是一个抽象类型,并不是实体的东西,利用这个抽象类型会实例化成不同的个体,每个个体的特征(数据成员)都不一样,如果在类定义中将特征初始化了,岂不是破坏了抽象性,做了一个面向都一样的模子?
(2)类的定义实际相当与类型声明,并没有分配存储空间,初始化的数据哪里放? 类实例化以后才会有实体存储地址及空间。
所以:
1、一般的数据成员可以在构造函数中初始化。(构造初始化列表初始化和构造函数体内赋值初始化)
2、const数据成员必须在构造函数的初始化列表中初始化
3、static要在类的定义外面初始化。
4、数组成员是不能在初始化列表里初始化的,不能给数组指定明显的初始化。
【问题2】类的数组成员是不能在初始化列表里初始化的。//数组作为成员变量时只有默认初始化!!! ,也就是无法传递参数。
在类的构造函数中可以使用成员初始化表初始化各个成员变量,这种方法是很受推荐和赞扬的,因为使用成员初始化表中对成员进行初始化和在构造函数体中对成员赋值是不同的,特别对于成员对象,在效率上是很有差别的。
那么,用惯了成员初始化列表的朋友们,因为尝到了其甜头,通常在第一次对待成员数组时,都会选择成员初始化列表,而这导致编译失败!
类的数组成员不能使用成员初始化列表进行初始化,而只能通过在构造函数体中对数组的各个成员进行赋值!
class bb
{
public:
bb() : a[0](0) {} //出现c2059错误
bb(int (&a)[3]) : a[0](a[0]),a[1](a[1]),a[2](a[2]) {} //出现c2059错误
bb(int (&a)[3])
{
this->a = {a[0],a[1],a[2]}; //错误,不能使用初始化表
}
//正确写法:
bb()
{
this->a[0] = 0;
this->a[1] = 0;
this->a[2] = 0;
}
bb(int (&a)[3])
{
this->a[0] = a[0];
this->a[1] = a[1];
this->a[2] = a[2];
}
int a[3];
};
//vector代替数组。下面的代码应该是可行的
class C {
std::vector<int> l;
public:
C():l{1,2,3}{}
int first() const ( return *l.begin(); }
}
【问题3】为什么不能在子类的初始化列表里初始化父类的成员?
class A {
public:
int n_;
};
class B : public A {
public:
B() : n_(0)
{}
};
intellisense会在高亮行提示:
“n_” is not a nonstatic data member or base class of class “B”
子类的初始化列表不能初始化父类或者祖先类的成员 。这是标准规定的,至于为什么会有这样一个规定,可以参考的解释如下:
(1)首先是初始化列表的作用
初始化列表其实是一种 后天强加 的初始化语义。
编译器处理后,会把初始化列表的内容先转化,然后插入到构造函数的开头,之后的内容才是你在构造函数里写的语句(如果你有写的话)。
但是,这两部分是截然不同的语义:前者是编译器插入的初始化语句,且开始执行用户自己的语句时,编译器要保证所有需要初始化的成员都已经初始化了,这也是各大书籍推荐使用初始化列表显式初始化成员的原因。
(2)继承情况下的初始化顺序
对应一个基类在上的继承树,一个子类对象的初始化顺序是自顶向下 。
子类对象的构造函数会首先利用父类的构造函数创建一个父类对象,然后再父类对象的基础之上再把自己创建出来。(想象一个递归调用栈)
所以,在子类利用构造函数初始化的时候,其父类对象已经是确定构造完毕的。
(3)标准要求,每个对象在其生命周期内只能被初始化一次。这是一个非常显然的要求。
所以,如果我们在子类的初始化列表中对父类成员进行初始化,那么在子类构造函数开始时,这个对象已经可能被父类构造函数初始化了(内建类型需要显式初始化,带有Non-trivial默认构造的函数就算不指定也会被初始化),此时如果子类再初始化,就违反了上述要求3)。
(4)总之,子类初始化父类成员这种越俎代庖的行为是不合理的。(替代方案:可以对父类的构造函数传参数对其进行初始化。或者在子类构造函数体里内直接赋值)
【编程题】当数组作为类的成员变量时,应该怎么对它(数组)进行初始化?(对应问题2)
<方法1-连续赋值>
Class A
{
public:
A();
~A(){};
private:
int abc[3];
}
A::A()
{
for( int nLoop=1;nLoop<=3;nLoop++)
abc[nLoop]=nLoop;
}
但假如需要初始化的是没有默认构造的对象数组又如何呢?列如:
class B
{
public:
B(int a);
~B(){};
private:
int m_nB;
}
B::B(int a): m_nB(a)
{
}
Class A
{
public:
A();
~A(){};
private:
B abc[3];
}
这时该如何初始化呢? 数组作为成员变量时只有默认初始化,也就是无法传递参数。有两种解决方案:你可以把对象数组改成指针数组,或者把ClassB类的构造和初始化分开。
<方法1:对象数组改成指针数组>
Class A{
public:
A();
~A(){};
private:
B *abc[3];
}
A::A()
{
abc[0] = new B(1);
abc[1] = new B(2);
abc[2] = new B(3);
}
<方法2:ClassB类的构造和初始化分开>
class ClassB{
private:
int data;
public:
ClassB(int d):data(d){ }
ClassB(){ }
void Init(int d){data=d;}
};
class ClassA{
private:
ClassB arrayOfObjectClassB[2];
public:
ClassA(int i){
arrayObjectOfClassB[0].Init(i);
arrayObjectOfClassB[1].Init(i);
}
};