我有以下计划:
#include<iostream>
using namespace std;
struct Base01{
int m;
Base01():m(2){}
void p(){cout<<m<<endl;}
};
struct Derived01:public Base01{
Derived01():m(3){}
};
struct Derived02:virtual public Base01{
Derived01():m(4){}
};
struct my: Derived01,Derived02{
my():m(5){}
};
int main(){
return 0;
}
gcc / clang都报告编译错误。
我只想知道这里的语言设计考虑是什么,为什么派生类只能在初始化列表中调用基类ctor,但不能直接使用基类成员?
答案 0 :(得分:8)
您在构造函数初始化程序列表中执行的操作是初始化。在对象的生命周期中,只需要一次。一般情况下,这就是启动对象生命周期的原因。
基类的构造函数(在派生类的构造函数正确变为活动状态之前完成工作)已经初始化了基类的所有直接子对象。它已经开始了他们的一生。如果您尝试从派生类的构造函数中伸入并初始化基类的直接子对象,那么这显然是同一对象的第二次初始化。这在C ++中是完全不可接受的。语言设计通常不允许您第二次初始化。
在您的情况下,相关子对象具有基本类型int
,因此很难看到这种“重新初始化”的危害。但是考虑一些不那么微不足道的东西,比如std::string
对象。你如何建议派生类应该“撤消并重做”已经由基类执行的初始化?虽然形式上可以正确地执行它,但构造函数初始化程序列表并不是为此目的。
在一般情况下,做类似的事情需要一个语言功能,允许用户告诉基类的构造函数的某些内容“请将你的这个子对象保留为未初始化,我将进入并在以后从派生类“。但是,C ++不为用户提供此类功能。虚拟基类初始化中存在一个模糊的类似特征,但它有一个非常特定(和不同)的目的。
答案 1 :(得分:3)
在C ++中执行此操作的正确方法是将该值传递给基类构造函数。您的Base01
类需要一个额外的构造函数,它可以获取m的所需值。像这样:
struct Base01{
int m;
Base01():m(2){}
// Added this:
Base01(int mVal) : m(mVal) {}
void p(){cout<<m<<endl;}
};
struct Derived01:public Base01{
Derived01() : Base01(3) {} // Calling base constructor rather than
// initializing base member
};
struct Derived02:virtual public Base01{
Derived01() : Base01(4){} // Same here
};
struct my: Derived01,Derived02{
my(): Base01(5){} // And here.
};
正如AnT所说,你不能初始化两次 - 但是你可以设置它,这样就可以通过上面的方式按照你想要的方式初始化事物。
答案 2 :(得分:2)
您当然可以在ctor-initializer列表中使用基类成员:
struct Base
{
int x;
Base(int x) : x(x) {}
};
struct Derived
{
int y;
Derived() : Base(7), y(x) {}
}
此处,基础成员x
出现在派生成员y
的初始化程序中;它的价值将被使用。
AnT做了一个非常好的工作,解释了为什么ctor-initializer列表不能用于(重新)初始化基础子对象的成员。
答案 3 :(得分:0)
基本语言设计考虑因素是关注点的分离(避免使基类依赖于其派生类),并且基类负责初始化自己的成员(以及它拥有的任何基础)。
一个相关的考虑因素是,在基类构造函数完成之前,基类的成员不存在 - 就派生类构造函数而言。如果派生类的初始化列表能够进入并初始化基类成员,则会产生两种可能的后果
这些可能性在实践中确实没有意义,除非基类设计不当(例如,它的构造函数没有正确初始化其成员)。所需的机器所以它们可能有意义(例如,改变层次结构中基类构造的顺序,取决于派生类试图初始化的成员)将使编译器机制更复杂(例如,能够控制和跟踪基类成员的构造顺序,如果派生类应该选择到达,并且还意味着类的构造顺序将取决于派生类)。这将在派生类上引入基类行为(初始化的方式)的依赖性。
更简单的方法是让基类提供一个构造函数,该构造函数正确初始化有问题的成员,并使派生类构造函数在其初始化列表中调用该基类构造函数。所有上述(假设的)考虑都会消失。