一个派生类对象是否包含基类对象?

4

考虑以下示例代码:

#include <iostream>

using namespace std;

class base
{
   public:
      base()
      {
         cout << "ctor in base class\n";
      }
};

class derived1 : public base
{
   public:
      derived1()
      {
         cout <<"ctor in derived class\n";
      }
};

int main()
{
   derived1 d1obj;
   return 0;
}

问题

  1. 当创建时,构造函数的调用顺序是:首先调用基类构造函数,然后调用派生类构造函数。这样做的原因是:为了构建派生类对象,需要先构造基类对象

  2. 包含一个基类对象吗?

我再加一个问题

3) 当创建时,控制首先到达基类构造函数,然后再进入派生类构造函数吗?还是反过来:首先到达派生类构造函数,发现它有基类,然后控制流转到基类构造函数中?


1
询问内容问题可能比询问决策问题更有用。 - Kerrek SB
你应该养成在派生类中显式调用父类构造函数的习惯。假设基类没有提供默认构造函数,那么这段代码将无法编译。但是如果你在派生类的初始化列表中调用非默认构造函数,则可以编译通过。 - danca
3
我试图根据来自SO成员的输入拓展问题,同时确认我对C++初学者概念的信念。一个初学者来到SO会发现这个问题很有用。此外,在SO中没有任何地方说“如果一个人在C++ faq中找到了问题,他就不应该在SO中提问”。 - nitin_cherian
5个回答

7

1) 是的,先构造基类,然后是非静态数据成员,最后调用派生类的构造函数。这样做的原因是为了让该类构造函数中的代码可以看到和使用一个完全构造好的基类。

2) 是的。你可以完全按照字面意思理解:在分配给派生类对象的内存中,有一个名为“基类子对象”的区域。派生类对象“包含”一个基类子对象,就像对于任何非静态数据成员的成员子对象一样。不过,问题中给出的例子恰好是一种特殊情况:空基类优化。即使类型为base的完整对象从未为零,但允许此基类子对象的大小为零。

虽然这是一种低级别的事情。尽管子对象本身只是类布局的一部分,但语言的语法和语义还是将基类和成员视为不同的东西,并以不同的方式处理它们。

3) 这是一个实现细节。在基类构造函数的主体代码执行之前,派生类构造函数的主体代码会被编译器生成的一个看不见的try / catch块执行,以确保如果它抛出异常,则销毁基类。但是,编译器如何在发出的代码中实现这一点取决于它。

当一个类有虚拟基类时,构造函数通常会导致两个不同的函数体被生成——一个用于当该类是最派生类型时使用,另一个用于当该类本身是一个基类时使用。原因是虚基类由最派生类构造,以确保当它们共享时只构造一次。因此,第一个版本的构造函数将调用所有基类构造函数,而第二个版本将仅调用非虚基类的构造函数。

编译器总是“知道”该类具有哪些基类,因为你只能构造完整类型的对象,这意味着编译器可以看到类定义,并指定了基类。因此,在进入构造函数时才“发现它有一个基类”的问题并不存在——编译器“知道”它有一个基类,如果在派生类构造函数代码内部调用基类构造函数,那只是为了编译器的方便。它可以在每个构造对象的地方和对于可以并已内联的派生类构造函数的情况下发出对基类构造函数的调用,那就是最终的效果。


+1,在3)上,尽管发出的确切符号是实现细节,但编译器必须从最派生类型开始构造,因为那是生成的代码片段,具有传递给基础构造函数的正确参数,因此执行必须从那里开始,跳过基类,然后继续向下层次结构。 - David Rodríguez - dribeas
@David:是的,这种表述方式很好,派生类构造函数主体之前的初始化列表中可能有大量代码。这就是为什么构造函数没有名称,也不能获取它们的地址--为了让实现自由地将任何代码推到函数开头执行,然后再执行构造函数主体,从而确保不能(合法地)直接调用它。 - Steve Jessop
@SteveJessop 如何从派生对象中提取嵌入的基类对象?或者它的地址? - ziemowit141
@ziemowit141:对于虚基类,使用dynamic_cast转换派生类指针,对于非虚基类,使用static_cast即可。 - Steve Jessop

2
  1. 是的

  2. 概念上来说并不完全正确。d1obj包含了所有一个base实例的数据成员,并且响应了所有该实例的成员函数,但是它并不“包含”一个base实例:你不能像这样调用 d1obj.base.func()。如果你重载了父类声明的方法,你可以通过调用 d1obj.base::func() 来获取其实现,而不仅仅是调用 d1obj->func()。

虽然从某种程度上来说,你包含了父类的所有数据和方法,但是没有概念上包含父类实例这一点非常重要,因为你可以通过创建一个包含“父类”作为成员数据的类来获得直接继承的许多好处,就像这样:

class derived2 /*no parent listed */ {
public:

   derived2() :_b() {}

private:
    base _b;
}

这样的结构允许您利用已由base实现的方法作为自己方法的实现细节,而无需公开任何base声明为公共但您不希望授予访问权限的方法。一个重要的例子是stack,它可以包含另一个STL容器的实例(如vectordequelist),然后使用它们的back()来实现top()push_back()来实现push()pop_back()来实现pop(),所有这些都不需要向用户公开原始方法。

1
如果您重载了父类声明的方法,那么您不能使用d1obj.base.func()这样的语法。但是,您可以使用略有不同的语法:d1obj.base::func(),即使在派生类型中隐藏覆盖base成员函数func,也会调用它。(overloaded意味着在派生类型中定义了一个不同的func签名,这将隐藏父版本) - David Rodríguez - dribeas
我的观点在于语法。在我给出的例子中,您正在尝试访问某种包含对象。在您的示例中,类似于我给出的第二个示例,您要求 d1obj 在那个调用中像其父类一样行事。这似乎是一个小差异,但是 derived1::func() 可能会以不同的方式修改与 base::func() 相同的成员,因此它确实是一种行为转变,而不仅仅是修改包含的基类。您从 base 单独继承了数据成员,但它们在内部并没有被组合在一个 base 结构中。 - matthias
1
我不太清楚您获取的基本原理,但是"您会从基础中单独继承数据成员,但它们在基础内部没有捆绑在一起"这句话并不准确,派生类型确实包含作为base组合在一起的基类型子对象。考虑上溯转换或执行未被覆盖的成员函数时发生的情况,必须能够定位具有相同内存分布的基成员,无论最终对象是否为类型basederived。我同意概念上派生类型是一个而不是包含 - David Rodríguez - dribeas
它没有被包含在内存中,数据成员只是按照它们被声明的顺序排列,所有base的成员都排在任何derived1的成员之前。当您进行向上转换时,您获取从this开始的sizeof(base)大小的内存块。那里没有保护/智能。当derived1想要访问从base继承的成员时,它只是this->m。它不关心它是否是继承的成员,它只知道需要从this偏移多少来读取m类型的对象。 - matthias
1
从标准来看,1.8p2 对象可以包含其他对象,称为子对象。子对象可以是成员子对象(9.2),基类子对象(第10条款)或数组元素。不是任何其他对象的子对象的对象称为完整对象。 引用将基类称为完整对象的子对象 - David Rodríguez - dribeas
显示剩余4条评论

1
  1. 是的。想象一下,在派生类的构造函数中,您想要使用基类的某些成员。因此,它们需要被初始化。因此,调用基类构造函数是有意义的。

  2. d1obj 是一个基类对象。这就是继承的含义。在某种程度上,您可以说它包含了一个基类对象。在内存中,对象的第一部分将对应于基本对象(在您的示例中没有虚函数,如果有,您将首先拥有指向 derived1vftable 指针,然后是属于基类的成员),之后是属于 derived1 的成员。


3
不,确实按字面意思理解。派生对象中确实包含一个基类子对象。你甚至可以通过将指向派生对象的指针进行静态转换来获得指向该基类子对象的指针。 - Kerrek SB
要小心的是,_包含_很容易被误解为_组合_,这是一种类似但不同的范例。 - Chad
@KerrekSB 你是对的,但这取决于你对“字面上”的理解。 我会找到像class B; class A { B _b }; 这样的东西,其中“包含”字面上适用。 我可能是错的。 - Luchian Grigore
@LuchianGrigore:也许你对语法,即如何命名子对象及其成员感到困惑。确实,该语法与为命名成员对象的语法不同。尽管如此,子对象确实存在。(请记住,C++中并非所有东西都有名称,例如构造函数) - Kerrek SB

1

是的,没错。


自从您的编辑,第3点派生类构造函数调用基类构造函数作为其首要职责; 然后所有成员对象构造函数,最后执行构造函数体。

销毁的工作方式相反:首先执行析构函数体,然后销毁成员对象(按照它们销毁的相反顺序),最后调用基础子对象析构函数(这就是为什么您始终需要在基类中具有可访问的析构函数,即使它是纯虚拟的原因)。


0

1-- 是的。这样是合乎逻辑的。

类型为derived1的对象是特殊的base类型对象,这意味着它们首先是base类型的对象。这是首先构造的,然后“derived1”将其“特殊性”添加到对象中。

2-- 这不是包含的问题,而是继承。请参考我上面的段落以更好地理解这个答案。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接