【问题标题】:Understanding virtual destructors了解虚拟析构函数
【发布时间】:2014-11-04 23:20:54
【问题描述】:

我试图让自己熟悉 OOP 概念,但不能完全理解 virtual 的概念。

  1. 可以创建virtual destructor,但不能创建virtual constructor。为什么?
  2. 内部如何处理virtual destructors?我的意思是链接Virtual Destructors 说明了这个概念,但我的问题是vtables(派生和基础)的vptr 是如何被调用的? (虚拟成员函数出现这种情况时一般只调用Derived类的vptr指向的函数)
  3. 是否还有其他可能需要使用virtual destructor 的场景?

谁能通过链接/示例帮助我理解上述概念?

【问题讨论】:

标签: c++ gcc


【解决方案1】:

我假设我们有一个基类 A,它是派生的 B。

1.:你可以通过A指针删除B,然后正确的方法是调用B的析构函数。 但是,您不能说应该在实际调用 A 构造函数时创建 B 对象。只是没有这种情况。 你可以说:

A* a = new B ();

B b;

但都是直接调用B的构造函数。

2.: 嗯,我不完全确定,但我猜它会遍历类层次结构的相关部分,并搜索最接近的函数调用。如果一个函数不是虚函数,它会停止迭代并调用它。

3.: 如果你想从那个类继承一些东西,你应该总是使用虚拟析构函数。如果这是最后一堂课,你不应该这样做。

【讨论】:

    【解决方案2】:

    可以创建虚拟析构函数,但不能创建虚拟构造函数。为什么?

    虚拟函数根据调用它们的对象的类型进行调度。当构造函数被调用时,没有对象——构造函数的工作是创建一个对象。没有对象就不可能进行虚拟调度,所以构造函数不能是虚拟的。

    内部如何处理虚拟析构函数?

    虚拟调度的内部细节是实现定义的;语言没有指定实现,只有行为。通常,析构函数是通过 vtable 调用的,就像任何虚函数一样。

    如何调用两个 vtable(Derived 和 Base)的 vptr?

    只有最衍生的析构函数会被虚拟调用。所有析构函数,无论是否虚拟,都将隐式调用所有成员和直接基类子对象的析构函数。 (在存在虚拟继承的情况下情况会稍微复杂一些;但这超出了这个问题的范围)。

    是否还有其他可能需要使用虚拟析构函数的场景?

    你需要一个来支持多态删除;也就是说,能够通过指向基类型的指针删除派生类型的对象。如果没有基本类型的虚拟析构函数,这是不允许的,并且会给出未定义的行为。

    【讨论】:

    • “通常,析构函数是通过 vtable 调用的,就像任何虚函数一样。” - 这似乎暗示了虚函数调用通常不是实现定义的,这他们是IIRC。我确定这就是您的意思,但是这句话有点模棱两可。
    • @Cwan:这可能意味着,如果我不只是说“语言没有指定实现,只有行为”,并且用“通常”这个词来限定的话。但我添加了更多限定条件,以防其他人觉得它模棱两可。
    • 是的,好吧,因为英语不是我的母语,我很可能弄错了。恕我直言,现在更清楚了。
    【解决方案3】:
    1. 因为在运行阶段调用了一个虚函数,但是在初始化阶段调用了构造函数,所以没有构造对象。所以有一个虚构造函数是没有意义的。

    2. 一个。在你的链接中只调用基类析构函数的原因,析构函数没有标记为虚拟,所以析构函数地址在编译/链接时链接到基类析构函数,显然指针的类型是 Base 而不是 Derived在编译时。

      b.为什么在向 Base 析构函数添加 virtual 之后调用 Base 和 Derived 构造函数。这是相同的行为,如下所示: 导出d; // 当 d 退出生命周期时,Derived 和 Base 的析构函数都会被调用。

    3. 假设当你至少有一个虚函数时,你应该有一个虚析构函数。

    【讨论】:

      【解决方案4】:

      先说一点虚函数和非虚函数的区别:

      代码中的每个非虚拟函数调用都可以在编译或链接期间解析。

      resolved是指函数的地址可以由编译器或链接器计算出来。

      所以在创建的目标代码中,函数调用可以替换为操作码,用于跳转到该函数在内存中的地址。

      使用虚函数,您可以调用只能在运行时解析的函数。

      我们不解释它,让我们通过一个简单的场景:

      class Animal
      {
          virtual void Eat(int amount) = 0;
      };
      
      class Lion : public Animal
      {
          virtual void Eat(int amount) { ... }
      };
      
      class Tiger : public Animal
      {
          virtual void Eat(int amount) { ... }
      };
      
      class Tigon : public Animal
      {
          virtual void Eat(int amount) { ... }
      };
      
      class Liger : public Animal
      {
          virtual void Eat(int amount) { ... }
      };
      
      void Safari(Animal* animals[], int numOfAnimals, int amount)
      {
          for (int i=0; i<numOfAnimals; i++)
              animals[i]->Eat(amount);
          // A different function may execute at each iteration
      }
      

      您可能会理解,Safari 函数可以让您灵活地喂养不同的动物。

      但由于每个动物的确切类型直到运行时才知道,所以要调用的确切 Eat 函数也是如此。


      类的构造函数不能是虚的,因为:

      调用对象的虚函数是通过对象类的V-Table来实现的。

      每个对象都有一个指向其类的 V-Table 的指针,但这个指针只在运行时初始化,即对象创建时。

      也就是说,这个指针只有在构造函数被调用时才被初始化,因此构造函数本身不能是虚的。

      除此之外,构造函数一开始就没有任何意义。

      虚函数背后的想法是,您可以在不知道调用它们的对象的确切类型的情况下调用它们。

      当你创建一个对象时(即当你隐式调用一个构造函数时),你确切地知道你正在创建什么类型的对象,所以你不需要这个机制。


      基类的析构函数必须是虚拟的,因为:

      当你静态分配一个类继承自基类的对象时,那么在函数(如果该对象是本地的)或程序(如果该对象是全局的)结束时,该类的析构函数会自动调用,然后调用基类的析构函数。

      在这种情况下,析构函数是虚拟的这一事实没有任何意义。

      另一方面,当您动态分配 (new) 其类继承自基类的对象时,您需要在稍后执行程序的某个时间点动态释放 (delete) 它.

      delete 运算符接受一个指向对象的指针,其中指针的类型可能是基类本身。

      在这种情况下,如果析构函数是虚拟的,那么delete 运算符会调用类的析构函数,而后者又会调用基类的析构函数。

      但是如果析构函数不是虚拟的,那么delete 操作符会调用基类的析构函数,而实际类的析构函数永远不会被调用。

      考虑以下示例:

      class A
      {
          A() {...}
          ~A() {...}
      };
      
      class B: public A
      {
          B() {...}
          ~B() {...}
      };
      
      void func()
      {
          A* b = new B(); // must invoke the destructor of class 'B' at some later point
          ...
          delete b; // the destructor of class 'B' is never invoked
      }
      

      【讨论】:

        【解决方案5】:

        可以创建虚拟析构函数,但不能创建虚拟构造函数。 为什么?

        我将尝试用外行的方式解释这一点。 c++ 中的类仅在其构造函数完成后才存在。每个基类在派生类及其成员(包括 vtable 链接)初始化之前就存在。因此,拥有一个虚拟构造函数没有意义(因为要构造,你需要知道类型)。此外(在 c++ 中),从构造函数调用虚函数不起作用(因为尚未设置派生类的 vtable 部分)。如果仔细考虑一下,允许从构造函数调用虚函数会带来很多麻烦(例如,如果在成员初始化之前调用派生类的虚函数会怎样)。

        就析构函数而言,在销毁点,vtable 是“完整的”,我们(c++ 运行时)完全了解类型(可以这么说)。找到了类型的最派生部分的析构函数(如果是虚拟的,则通过 vtable),因此可以调用该析构函数,当然也可以调用所有基类的析构函数。

        内部如何处理虚拟析构函数?我是说链接 Virtual Destructors 说明了这个概念,但我的问题是如何 两个 vtable(Derived 和 Base)的 vptr 都被调用了吗?

        析构函数的处理方式与普通虚函数相同(也就是说,如果地址是虚函数,则在 vtable 中查找地址,代价是增加一个(可能是 2 个?)额外的间接级别)。此外,c++ 保证在派生的析构函数完成后,所有基析构函数都应执行(以相反的构造顺序,依赖于声明的顺序)。

        可以通过使用原型模式(或克隆)等模式或使用工厂来模仿/模拟虚拟构造。在这种情况下,要么存在一个真实类型的实例(以多态方式使用),要么存在一个工厂(从抽象工厂派生),它根据所提供的一些知识(通过虚函数)创建一个类型。

        希望这会有所帮助。

        【讨论】:

          【解决方案6】:

          我浪费了几天时间试图找出为什么我的派生虚拟析构函数在发现答案之前没有被调用,所以希望我可以通过这个回复来避免其他人的痛苦。

          我开始在我的项目中使用三层和四层的派生类。虚拟函数似乎工作正常,但后来我发现我有大量内存泄漏,因为我的析构函数没有被调用。没有编译器或运行时错误 - 只是没有调用析构函数。

          网络上有大量关于此的文档和示例,但没有一个有用,因为我的语法是正确的。

          我决定如果编译器不打算调用我的析构函数,我需要创建自己的虚拟析构方法来调用。然后我得到了解决问题的编译器错误 - “如果前向引用类”。在基类中为派生类头文件添加包含解决了这个问题。编译器需要类定义才能调用析构函数!

          我建议在创建新的派生类时,将头文件包含在基类和中间类中。将条件调试代码添加到析构函数以检查它们是否被 bing 调用可能也是一个好主意。

          鲍勃·赖斯

          【讨论】:

            猜你喜欢
            • 2013-07-06
            • 2011-08-12
            • 2012-04-13
            • 2012-04-18
            • 2019-06-07
            • 2017-03-16
            • 2012-12-20
            • 1970-01-01
            • 2011-09-21
            相关资源
            最近更新 更多