派生类对象是否包含基类对象

Does a derived class object contain a base class object?

本文关键字:对象 基类 包含 是否 派生      更新时间:2023-10-16

考虑以下示例代码:

#include <iostream>
using namespace std;
class base
{
   public:
      base()
      {
         cout << "ctor in base classn";
      }
};
class derived1 : public base
{
   public:
      derived1()
      {
         cout <<"ctor in derived classn";
      }
};
int main()
{
   derived1 d1obj;
   return 0;
}

  1. 创建d1obj时,按派生顺序调用构造函数:先调用基类构造函数,然后调用派生类构造函数。这是因为以下原因:In-order to construct the derived class object the base class object needs to be constructed first ?

  2. d1obj是否包含基类对象

我再加一个问题

3)创建d1obj时,控件首先到达基类构造函数,然后转到派生类构造函数?或者是另一种方式:它首先到达派生类构造函数,发现它具有基类,因此控件转到基类中的构造函数?

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

2)是的。您可以完全从字面上理解:在分配给派生类对象的内存中,有一个区域称为"基类子对象"。派生类对象"包含"基类子对象的方式与它包含任何非静态数据成员的成员子对象的方式完全相同。实际上,问题中给出的例子恰好是一个特殊情况:"空基类优化"。这个基类的子对象允许大小为0,即使base类型的完整对象的大小永远不会为0。

不过,这种包容是一种低级别的东西。正如其他人所说,在概念上基与成员不同,并且即使子对象本身都只是类布局的一部分,语言的语法和语义也会以不同的方式对待它们。

3)这是一个实现细节。基类构造函数体中的代码在派生类构造函数体中的代码之前执行,实际上派生类构造函数随后在编译器生成的不可见的try/catch块中执行,以确保如果抛出,基类将被销毁。但这取决于编译器如何实现这一点,就所发出代码中的函数入口点实际做什么而言。

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

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

  1. 嗯,从概念上讲,不是真的。d1obj包含base的实例所包含的所有数据成员,并响应该实例所包含的所有成员函数,但不"包含"base的实例:您不能说d1obj.base.func()为instance。例如,如果您重载了父类声明的方法,那么您可以调用d1obj.base::func()来获得它的实现,而不仅仅是调用d1obj->func()

虽然说包含父类的所有数据和方法而在概念上不包含父类的实例似乎有点鸡肋,但这是一个重要的区别,因为通过创建包含"父"类作为成员数据的类,通常可以获得直接继承的许多好处,如下所示:

class derived2 /*no parent listed */ {
public:
   derived2() :_b() {}
private:
    base _b;
}

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

  1. 是的。假设在派生类的构造函数中,希望使用基类的一些成员。因此,它们需要初始化。因此,首先调用基类构造函数是有意义的。

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

是,是。


3)派生类构造函数调用基类构造函数作为它的第一个任务;然后所有成员对象构造函数,最后执行构造函数体。

析构函数以相反的方式工作:首先执行析构函数体,然后销毁成员对象(与销毁顺序相反),最后调用基类子对象析构函数(这就是为什么总是需要在基类中使用可访问的析构函数,即使它是纯虚函数)。

1——是的。这是合乎逻辑的。

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

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