【问题标题】:Base class destructor is not virtual and child class destructor is virtual, program crash基类析构函数不是虚拟的,子类析构函数是虚拟的,程序崩溃
【发布时间】:2012-06-26 09:12:31
【问题描述】:

为什么下面的程序会崩溃?我有一个基类,它的析构函数不是虚拟的,但子类的析构函数是虚拟的:

#include <iostream>

class Base {
public:
  Base() {
    std::cout << "Base::Base CTOR " << std::endl;
  }
  ~Base() {
    std::cout << "Base::Base DTOR " << std::endl;
  }
};

class Child : public Base {
public:
  Child(){
    std::cout << "Child::Child CTOR " << std::endl;
  }
  virtual ~Child() {
    std::cout << "Child::Child DTOR " << std::endl;
  }

};

int main (int argc, char **argv) {
  Base *ptr = new Child;
  delete ptr;
}

【问题讨论】:

标签: c++ virtual-destructor


【解决方案1】:

您所观察到的称为“未定义行为”。如果您想通过Base 指针对Child 实例调用delete,则将Base 的dtor 设为虚拟。

从 2003 年标准,5.3.5/3:

在第一种选择(删除对象)中,如果是静态类型的 操作数与其动态类型不同,静态类型应为 操作数的动态类型和静态类型的基类应 具有虚拟析构函数或行为未定义。

【讨论】:

  • @unkulunkulu:因为他在向编译器撒谎他想要释放的内存。
  • 释放的内存不依赖于直接调用的析构函数(好吧,如果Child 持有一些内存资源,它可以)。实际上,我正在寻找确切的陈述,这是 UB 的构成,理想情况下它是对标准的参考。
【解决方案2】:

您有未定义的行为,因为要删除的指针操作数的静态类型与它指向的对象的动态类型不匹配,并且您不满足允许将指针传递给该规则的例外的要求被删除对象的基类,因为此异常要求 base 类具有虚拟析构函数。

任何行为都可能发生,包括代码“按预期”工作或崩溃。

【讨论】:

  • 你可以考虑改写一下,因为“例外”这个词让我很困惑(我想到了C++ 例外)。实际上,我只有在阅读了@wilx 对标准的引用后才理解您的答案。
  • @unkulunkulu:我考虑过改写,但现在我想不出比“例外”更好的词来描述规则的例外。
  • 但是您不仅可以通过替换这项工作来改写,还可以添加另一个句子,例如“您不能这样做,但是有一个例外,但是在您的情况下,您不符合要求'什么的:D
  • @CharlesBailey:没有这样的例外:如果该方法不是虚拟的,则从基指针调用方法将始终调用基方法。并且 delete 调用本身就是方法的析构函数。我在这里没有看到任何异常行为:调用了错误的函数,产生了意外的结果。
  • @EmilioGaravaglia:我说的是传递给delete 的指针的静态类型必须与被删除对象的动态类型匹配的规则的例外。如果不清楚,我很抱歉。
【解决方案3】:

希望这个例子能帮助你理解重点:

#include <iostream>
class Base {
public:
 Base() {
    std::cout << "Base::Base CTOR " << std::endl;
 }
 ~Base() {
   std::cout << "Base::Base DTOR " << std::endl;
 }
private:
protected:
};

class Child : public Base {
 public:
 Child(){
std::cout << "Child::Child CTOR " << std::endl;
  }
  ~Child(){
std::cout << "Child::Child DTOR " << std::endl;
 }
  private:
 protected:
 };
  class gChild : public Child {
   public:
   gChild(){
    std::cout << "Child::Child gCTOR " << std::endl;
   }
  ~gChild(){
    std::cout << "Child::Child gDTOR " << std::endl;
  }
private:
protected:
};
int main ( int argc, char **argv) {
    Base *ptr = new gChild;
 delete ptr;
}

如果是虚拟的 ~Base() ,那么所有析构函数的打印都会被打印出来。

如果是 virtual ~child() 或 virtual ~gChild(),则只打印基本析构函数。

这是因为析构函数的执行方向相反。这里的行为是未定义的。您必须定义基本析构函数虚拟才能获得预期的结果。

谢谢。

【讨论】:

    【解决方案4】:

    看看这个:

    #include <iostream>
    
    class Base
    {
    public:
        void nonvirtualmethod()
        { std::cout << "Base nonvirtualmethod" << std::endl; }
        virtual void virtualmethod()
        { std::cout << "Base virtualmethod" << std::endl; }
    };
    
    class Derived: public Base
    {
    public:
        void nonvirtualmethod()
        { std::cout << "Derived nonvirtualmethod" << std::endl; }
        virtual void virtualmethod()
        { std::cout << "Derived virtualmethod" << std::endl; }
    };
    
    int main()
    {
        Derived d;
        Derived* pd = &d;
        Base* pb = &d;    //< NOTE: both pd and pb point to the same object
    
        pd->nonvirtualmethod();
        pb->nonvirtualmethod();
        pd->virtualmethod();
        pb->virtualmethod();
    }
    

    我给你以下输出:

    Derived nonvirtualmethod
    Base nonvirtualmethod
    Derived virtualmethod
    Derived virtualmethod  //< invoked by a Base*
    

    这是因为pb指针(Base*)的静态类型有区别 以及它指向的动态类型 (Derived)。 虚拟方法和普通方法的区别在于非虚拟方法遵循静态类型映射(因此Base 指针调用Base::methods),而虚拟方法遵循运行时类型链,因此如果Base*指向Derived,将调用Derived方法。

    从这个意义上讲,析构函数没什么特别的:如果它不是虚拟的,Base 指针将不会调用 Derived 指针,因此你会得到一个半毁坏的对象,它会返回给内存存储。

    这是 UB 的原因(而不是简单地拒绝),是因为“内存存储”不是由语言本身管理,而是由程序所在的平台管理:崩溃很可能取决于以下事实:缺少Derived 部分(仍然存在)将导致操作系统尝试释放具有错误起始地址的内存块。

    【讨论】:

      猜你喜欢
      • 2021-08-22
      • 2018-06-29
      • 2014-10-02
      • 2011-09-03
      • 2019-04-20
      • 1970-01-01
      • 2013-01-26
      • 2011-03-21
      • 2013-11-01
      相关资源
      最近更新 更多