【问题标题】:C++ Non-Abstract Destructor InheritanceC++ 非抽象析构函数继承
【发布时间】:2023-03-30 00:35:02
【问题描述】:

我以前见过这个问题,但不清楚或与我遇到的情况相同。

我有一个抽象基类。它有一个受保护的构造函数和一个析构函数。它被几个完整的类型继承,这些类型也有公共的构造函数和析构函数。如果对象被基类型引用,我遇到的问题是删除对象不会调用子析构函数。

class Tree
{
protected:
        Tree(){ }
public:
        ~Tree(){ }
};

class OakTree : public Tree
{
public:
        OakTree(){ }
        ~OakTree(){ }
};

vector<Tree*> Trees; // Store objects using the base type
Trees.push_back(new OakTree()); // Create derived object
delete Trees[0]; // OakTree desctructor does not get called

如何调用 OakTree 析构函数?我尝试将所有析构函数标记为虚拟,但没有奏效。基类析构函数不能是抽象的(这样可以解决调用问题,但不能解决删除问题)。

【问题讨论】:

  • 抱歉,我在其中一个析构函数中出现错误,导致它在我的内存转储中显示内存泄漏,这让我认为没有调用析构函数。在这种情况下,我 99% 确定析构函数应该是虚拟的,但我将它们设置为虚拟的,但这并没有解决我的内存泄漏问题。我粗心大意,但为您的回答 +1。

标签: c++ inheritance c++11


【解决方案1】:

制作你的基类析构函数virtual

class Tree
{
protected:
        Tree(){ }
public:
        virtual ~Tree(){ }
}

否则,如果您尝试通过基类指针进行删除,则会导致未定义的行为。这有点过时了,但 Scott Meyers 在第二版 Effective C++ 中生动地表达了这一点:

C++ 语言标准在这个主题上异常清晰:当您尝试通过基类指针删除派生类对象并且基类具有 非虚拟 析构函数时(就像 EnemyTarget 所做的那样) ,结果未定义。这意味着编译器可以生成代码来做任何他们喜欢的事情:重新格式化你的磁盘,向你的老板发送暗示性邮件,将源代码传真给你的竞争对手,等等。 (在运行时经常发生的是派生类的 destrutcor 永远不会被调用。...)

【讨论】:

  • 这实际上是特定于实现的,我相当肯定现有的实现不会随机调用函数,尤其是格式化磁盘驱动器。我测试过的编译器称为“最接近的东西”,它是指针 T 的析构函数。
  • 这是夸张。但是,未定义的行为可能是一种安全风险,在某些情况下可以被利用来实现各种攻击者的目标。
  • 只是不要认为夸张在技术文献中占有一席之地,我认为 Mayers 先生在他的许多陈述中都试图变得“古怪”/有趣和不必要的丰富多彩。我越来越讨厌这种行为,因为在讲座中渴望得到关注的教授往往会花更多的时间在“令人印象深刻的年轻人”面前培养花哨的形象,而不是实际教授手头的主题。
  • 迈耶斯并不孤单。在 C/C++ 讨论中,您还会遇到恶魔从您的鼻子中飞出,或者猴子从不太令人愉快的位置飞出的参考。这些也是夸张的。它旨在创造一个持久的印象,即未定义的行为是多么不受欢迎。
  • 对我来说并没有以这种方式工作。更像是为了展示蹩脚的幽默感。也许我只是更务实一点,但通过开玩笑来强调某事的严重性和重要性似乎有点自相矛盾。如果你确实是对的,而且他们这样做是为了教育,为什么他们在发表这样的言论后,总是有一种自满的感觉,当他们这样做的时候,他们的脸上会出现掌声、笑声和钦佩之情?
【解决方案2】:

这是因为你的基类析构函数没有声明为虚拟的。

class Tree
{
protected:
        Tree(){ }
public:
        virtual ~Tree(){ }
}

【讨论】:

    【解决方案3】:

    如果你要使用多态性,你的基类中必须有一个虚拟析构函数。

    问题在于,当您在 Tree * 上调用 delete 时,没有人真正知道它是哪种树,并且通过使用虚拟析构函数,编译器会通过指向该特定函数表的指针生成调用对象实例,然后您将获得该类型的析构函数。

    否则标准会说它是未定义的行为,但大多数情况下似乎发生的是编译器生成对析构函数的调用,无论你的多态指针或引用是什么类型,在你的情况下,它就是基类。

    【讨论】:

      【解决方案4】:

      我想知道为什么没有人真正问作者想用代码实现什么?

      虽然,是的,在基类中拥有一个虚拟析构函数将帮助您调用析构函数,但您仍然留下未定义的行为

      delete Trees[0]; 之后你有什么期待?

      或者,更确切地说,你想用那条线实现什么?

      如果您尝试从矢量中删除项目,您可能需要:

      Trees.erase(Trees.begin());
      

      但是,如果您忘记删除行,您将面临内存泄漏。

      因此,不要在向量中使用纯指针。考虑:

      std::vector< std::shared_ptr<Tree> > Trees;
      Trees.push_back( std::make_shared<OakTree>() );
      Trees.erase(Trees.begin());
      

      可运行@Ideone

      【讨论】:

      • 这是一个大型项目的简化代码。基类存储一些数据,派生类以各种方式操作这些数据。派生类还具有需要在对象生命周期结束时删除的本地资源。我知道向量是如何工作的,这不是问题的一部分。
      • 内存管理呢?如果仅用于析构函数,则您的问题中不需要任何矢量代码
      • 只是提醒大家注意我将其存储为Tree* 而不是OakTree*Tree。我们不要太挑剔。问题得到解答。
      猜你喜欢
      • 2020-06-19
      • 2012-04-28
      • 2023-03-14
      • 2016-03-01
      • 2015-02-23
      • 2014-07-10
      • 2012-11-14
      • 2018-11-29
      • 2021-04-13
      相关资源
      最近更新 更多