如果派生类没有分配原始动态内存,为什么基类在这里需要有一个虚拟析构函数?


12

以下代码导致内存泄漏:

#include <iostream>
#include <memory>
#include <vector>

using namespace std;

class base
{
    void virtual initialize_vector() = 0;
};

class derived : public base
{
private:
    vector<int> vec;

public:
    derived()
    {
        initialize_vector();
    }

    void initialize_vector()
    {
        for (int i = 0; i < 1000000; i++)
        {
            vec.push_back(i);
        }
    }
};

int main()
{
    for (int i = 0; i < 100000; i++)
    {
        unique_ptr<base> pt = make_unique<derived>();
    }
}

这对我来说没有多大意义,因为派生的类不会分配任何原始动态内存,而unique_ptr会自行分配。我得到的是,该类的隐式析构函数被调用,而不是派生的,但我不明白为什么这是一个问题。如果要为派生编写显式析构函数,则不会为vec编写任何内容。


4
您假设析构函数仅在手动编写时存在;这个假设是错误的:语言提供了一个~derived()委派给vec的析构函数的方法。另外,假设您unique_ptr<base> pt知道派生的析构函数。没有虚拟方法,情况就不会如此。尽管可以给unique_ptr一个删除函数,它是一个模板参数,没有任何运行时表示,并且该功能对该代码无用。
阿蒙(Amon)

我们可以将花括号放在同一行上以使代码更短吗?现在我要滚动。
laike9m

Answers:


14

当编译器执行的析构函数的隐式delete _ptr;内部unique_ptr_ptr指针存储在中unique_ptr)时,它确切地知道两件事:

  1. 要删除的对象的地址。
  2. 指针的类型_ptr是。由于指针在其中unique_ptr<base>,因此意味着_ptr该类型base*

这是编译器所知道的。因此,假设它正在删除类型的对象base,它将调用~base()

那么...破坏dervied实际指向的对象的那一部分呢?因为如果编译器不知道它正在销毁derived,那么它根本不知道derived::vec 存在,更不用说应该销毁它了。因此,您已通过破坏对象的一半破坏了对象。

编译器不能假定任何base*被破坏的对象实际上是derived*;。毕竟,可以有来自的任何数量的类base。它怎么知道该特定对象base*实际指向哪种类型?

编译器要做的是找出要调用的正确析构函数(是的,derived有一个析构函数。除非您= delete有析构函数,否则每个类都有一个析构函数,无论您是否编写一个析构函数)。为此,它必须使用存储在其中的某些信息base来获取要调用的析构函数代码的正确地址,该信息由实际类的构造函数设置。然后,它必须使用此信息将转换base*为指向相应derived类的地址的指针(该地址可以或可以不位于其他地址。是的,确实如此)。然后它可以调用该析构函数。

我刚刚描述的机制?它通常被称为“虚拟调度”:又名,virtual当您拥有对基类的指针/引用时,调用标记的函数会发生这种情况。

如果要在仅拥有基类指针/引用的情况下调用派生类函数,则必须声明该函数virtual。在这方面,析构函数从根本上没有什么不同。


0

遗产

继承的全部要点是在许多不同的实现中共享一个公共接口和协议,以便可以将派生类的实例与来自任何其他派生类型的任何其他实例相同地对待。

在C ++中,继承也带来了实现细节,将析构函数标记(或不标记)为虚拟就是这样一种实现细节。

功能绑定

现在,当调用函数或其特殊情况(如构造函数或析构函数)时,编译器必须选择要实现的函数。然后,它必须生成遵循此意图的机器代码。

解决此问题的最简单方法是在编译时选择函数,并发出足够的机器代码,以便无论执行任何值,当该段代码执行时,它始终为该函数运行代码。除继承外,这非常有用。

如果我们有一个带有函数的基类(可以是任何函数,包括构造函数或析构函数),而您的代码在其上调用了一个函数,这是什么意思?

以您的示例为例,如果您调用initialize_vector()了编译器,则必须确定您是否真的要调用中找到Base的实现或中找到的实现Derived。有两种方法可以决定这一点:

  1. 首先是要确定,因为您是从Base类型调用的,所以您要实现中的实现Base
  2. 第二个决定是因为存储在Base类型化的值中的值的运行时类型可以是Base,还是Derived必须在运行时(每次被调用)在运行时做出关于进行哪个调用的决定。

此时的编译器很混乱,两个选项都同样有效。这是当virtual混合。使用此关键字时,编译器会选择选项2,从而在所有可能的实现之间延迟决策,直到代码以实际值运行为止。缺少此关键字时,编译器会选择选项1,因为这是正常情况下的行为。

在调用虚拟函数的情况下,编译器可能仍会选择选项1。但只有能够证明情况始终如此。

构造函数和析构函数

那么为什么不指定虚拟构造函数呢?

更直观地将如何编译器的构造相同实现之间挑DerivedDerived2?这很简单,不能。没有预先存在的值,编译器就可以从中了解真正的意图。没有预先存在的值,因为那是构造函数的工作。

那么为什么我们需要指定一个虚拟析构函数呢?

更直观地说,编译器将如何在Base和的实现之间进行选择Derived?它们只是函数调用,因此会发生函数调用行为。没有声明的虚拟析构函数,编译器将决定直接绑定到Base析构函数,而不管运行时类型的值如何。

在许多编译器中,如果派生未声明任何数据成员,也不从其他类型继承,则的行为~Base()将是适当的,但不能保证。它纯属偶然,就像站在尚未点燃的喷火器前一样。你好一阵子。

声明C ++中任何基本或接口类型的唯一正确方法是声明一个虚拟析构函数,以便为该类型的类型层次结构的任何给定实例调用正确的析构函数。这允许最了解实例的函数正确清理该实例。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.