使析构函数不是虚拟的,并在特殊情况下删除基指针是否安全?

Is it safe to make destructor not virtual, and delete base pointer in special circumstances?

假设我们有一个 class BST_Node :

struct BST_Node {
  BST_Node* left;
  BST_Node* right;
}

还有一个 class AVL_Node :

struct AVL_Node : BST_Node {
  int height;
}

在某些函数中

void destroyTree() {
  BST_Node *mynode = new AVL_Node;
  delete mynode; //  Is it ok ?
}

问题 #1

当析构函数是非虚拟的,但在派生中只有基元类型时,在基 class 上调用 delete 是否安全? (不会有内存泄漏吗?)

问题 #2

在 derived class only 中声明析构函数为 virtual 的规则是什么?据我所知,所有的析构函数都是同一个函数,我们可以将其称为 destructor() 然后当我们删除一个基指针时,析构函数只为基 class 调用,但是当删除派生 class, 析构函数也将被分派到子派生的 classes.

When the destructor is non virtual but there are only primitives types in derived, is it safe to call delete on base class ? (will there be no memory leaks ?)

您可能没有意识到,但这是两个不同的问题。

后一个答案是:不,不会有任何内存泄漏对于这个特定的例子,但可能会有其他示例。

原因是前一个问题的答案:不,这样做不安全。这构成了未定义的行为,即使几乎所有编译器都能很好地理解该行为——而且 'understood' 不是“可以安全地做”的同步性,只是要清楚。

当您编写像 delete mynode; 这样的代码时,编译器必须确定调用哪个析构函数。如果 mynode 的析构函数不是虚拟的,那么它将始终使用基析构函数,做基析构函数需要做的任何事情,而不是派生析构函数需要做的任何事情。

在这种情况下,这没什么大不了的:AVL_Node 唯一添加的是一个本地分配的 int 变量,它将作为清理过程的一部分进行清理整个指针。

但是如果您的代码是这样的:

struct AVL_Node : public BST_Node {
    std::unique_ptr<int> height = std::make_unique<int>();
};

那么这段代码肯定会导致内存泄漏,即使我们在派生对象的构造中明确使用了智能指针!智能指针并不能使我们免于 delete 使用非 virtual 析构函数构建基指针的苦难。

一般来说,如果 AVL_Node 对其他对象负责,您的代码可能会导致任何类型的泄漏,包括但不限于资源泄漏、文件句柄泄漏等。考虑一下,例如,如果 AVL_Node 有这样的东西,这在某些类型的图形代码中非常常见:

struct AVL_Node : public BST_Node {
    int handle;
    AVL_Node() {
        glGenArrays(1, &handle);
    }
    /*
     * Pretend we implemented the copy/move constructors/assignment operators as needed
     */
    ~AVLNode() {
        glDeleteArrays(1, &handle);
    }
};

您的代码不会泄漏 内存(在您自己的代码中),但它会泄漏 OpenGL 对象(以及该对象分配的任何内存)。

What is the rule when declaring a destructor virtual in derived class only ?

如果您从不打算存储指向基址的指针 class,那么这很好。

这也是不必要的,除非您还计划创建派生 class 的进一步派生实例。

为了清楚起见,下面是我们将使用的示例:

struct A {
    std::unique_ptr<int> int_ptr = std::make_unique<int>();
};

struct B : A {
    std::unique_ptr<int> int_ptr_2 = std::make_unique<int>();
    virtual ~B() = default;
};

struct C : B {
    std::unique_ptr<int> int_ptr_3 = std::make_unique<int>();
    //virtual ~C() = default; // Unnecessary; implied by B having a virtual destructor
};

下面是与这三个 classes 一起使用时安全和不安全的所有代码:

auto a1 = std::make_unique<A>(); //Safe; a1 knows its own type
std::unique_ptr<A> a2 = std::make_unique<A>(); //Safe; exactly the same as a1
auto b1 = std::make_unique<B>(); //Safe; b1 knows its own type
std::unique_ptr<B> b2 = std::make_unique<B>(); //Safe; exactly the same as b1
std::unique_ptr<A> b3 = std::make_unique<B>(); //UNSAFE: A does not have a virtual destructor!
auto c1 = std::make_unique<C>(); //Safe; c1 knows its own type
std::unique_ptr<C> c2 = std::make_unique<C>(); //Safe; exactly the same as c1
std::unique_ptr<B> c3 = std::make_unique<C>(); //Safe; B has a virtual destructor
std::unique_ptr<A> c4 = std::make_unique<C>(); //UNSAFE: A does not have a virtual destructor!

因此,如果 B(具有 virtual 析构函数的 class)继承自 A(不具有 virtual 析构函数的 class ),但作为一名程序员,您保证永远不会使用 A 指针引用 B 的实例,那么您就没有什么可担心的。因此,在那种情况下,就像我的示例试图展示的那样,可能有正当理由声明派生 class virtual 的析构函数,同时保留超级 class' 析构函数非 virtual.

运行离开

在没有虚析构函数的情况下通过指向基的指针删除派生对象是未定义的行为。无论派生类型多么简单,都是如此。

现在,在 运行 时间,每个编译器都将 delete foo 变成“找到析构函数代码,运行 它,然后清理内存”。但是您不能根据编译器发出的运行时间代码来理解 C++ 代码的含义。

所以你天真地认为“我不在乎我们 运行 是否有错误的销毁代码;我唯一添加的是一个 int。内存清理代码处理过度分配. 所以我们很好!"

你还去测试一下,你看看组装出来的,一切正常!然后你断定这里没有问题。

你错了。

编译器做两件事。首先,发射运行时间码。其次,他们使用你程序的结构来推理它。

第二部分是一个强大的功能,但它也使未定义的行为变得极其危险。

您的 C++ 程序在“抽象机器”中的含义,C++ 标准规定了事项。正是在那个抽象机器中发生了优化和代码转换。知道一段孤立的代码片段是如何在您的物理机器上发出的,并不能告诉您该代码片段的作用。

这是一个具体的例子:

struct Foo {};
struct Bar:Foo{};

Foo* do_something( bool cond1, bool cond2 ) {
  Foo* foo = nullptr;
  if (cond1)
    foo = new Bar;
  else
    foo = new Foo;

  if (cond2 && !cond1)
    inline_code_to_delete_user_folder();

  if (cond2) {
    delete foo;
    foo = nullptr;
  }
  return foo;
}

这里有一些玩具类型的玩具。

在其中,我们根据 cond1.

创建一个指向 BarFoo 的指针

那我们可能会做一些危险的事情。

最后,如果 cond2 为真,我们清理 Foo* foo

问题是,如果我们调用 delete foofoo 不是 Foo,它是未定义的行为。编译器可以合理地推理“好的,所以我们正在调用 delete foo,因此 *foo 是一个 Foo 类型的对象”。

但是如果foo是一个指向实际Foo的指针,那么显然cond1一定是false,因为只有当它是false 是 foo 指向实际的 Foo.

因此,从逻辑上讲,cond2 为真意味着 cond1 为真。总是。到处。追溯。

所以编译器实际上知道这是对程序的合法转换:

Foo* do_something( bool cond1, bool cond2 ) {
  if (cond2) {
    Foo* foo = new Foo;
    inline_code_to_delete_user_folder();
    delete foo;
    return nullptr;
  }       
  Foo* foo = nullptr;
  if (cond1)
    foo = new Bar;
  else
    foo = new Foo;

  return foo;
}

这很危险,不是吗?我们只是省略了检查 cond1 并在您将 true 传递给 cond2 时删除了用户文件夹。

我不知道当前或未来的编译器是否会使用 UB 的检测来删除错误的类型来对 UB 分支进行逻辑反向传播,但编译器会做一些与 other[ 类似的事情=84=] 种 UB,甚至像有符号整数溢出这样看似无害的事情。

为确保不会发生这种情况,您需要审核每个编译器的每个编译器中的每个优化,这些编译器将编译您的代码。

运行离开