假设我们有一个BST_Node类:
struct BST_Node {
BST_Node* left;
BST_Node* right;
}
和一个AVL_Node类:
struct AVL_Node : BST_Node {
int height;
}
并在某些功能
void destroyTree() {
BST_Node *mynode = new AVL_Node;
delete mynode; // Is it ok ?
}
当析构函数是非虚拟的但在派生中只有基元类型时,在基类上调用delete是否安全? (会不会有内存泄漏?)
在派生类中声明析构函数虚拟时的规则是什么?据我所知,所有的析构函数都是相同的函数,我们可以将其称为析构函数(),然后当我们删除基本指针时,析构函数仅针对基类调用,但是当删除派生类时,析构函数也会被派遣到子派生类。
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 ?)
你可能没有意识到这一点,但这是两个不同的问题。
后一个答案是:不,这个具体示例不会有任何内存泄漏,但可能还有其他示例。
原因在于前一个问题的答案:不,这样做是不安全的。这构成了未定义的行为,即使几乎所有编译器都很好地理解了这种行为 - 并且“理解”并不是“可以安全做”的系统,只是为了清楚。
当你编写像delete mynode;
这样的代码时,编译器必须弄清楚要调用哪个析构函数。如果mynode
的析构函数不是虚拟的,那么它将始终使用基础析构函数,执行基本析构函数需要执行的任何操作,但不执行派生析构函数需要执行的任何操作。
在这种情况下,这不是什么大问题:AVL_Node
添加的唯一东西是本地分配的int
变量,它将作为清理整个指针的同一过程的一部分进行清理。
但是,如果您的代码是这样的:
struct AVL_Node : public BST_Node {
std::unique_ptr<int> height = std::make_unique<int>();
};
那么这段代码肯定会导致内存泄漏,即使我们在派生对象的构造中明确地使用了智能指针!智能指针并没有使我们免受delete
ing与非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 ?
如果您从未计划存储指向基类的指针,那么这很好。
除非您计划还创建派生类的其他派生实例,否则它也是不必要的。
所以这是我们为了清楚起见而使用的示例:
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
};
现在这里是使用这三个类安全且不安全的所有代码:
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
析构函数的类)继承自A
(没有virtual
析构函数的类),但作为程序员,你保证你永远不会用B
指针引用A
的实例,那么你没什么可担心的关于。因此,在这种情况下,就像我的示例试图显示的那样,可能有正当理由声明派生类virtual
的析构函数,同时保留超类'析构函数非virtual
。
当没有虚拟析构函数时,通过指向base的指针删除派生对象是未定义的行为。无论派生类型有多简单,都是如此。
现在,在运行时,每个编译器都将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;
}
这是一个玩具类型的玩具。
在其中,我们基于Bar
创建指向Foo
或cond1
的指针。
那我们可能做一些危险的事情。
最后,如果cond2
是真的,我们清理Foo* foo
。
问题是,如果我们称delete foo
和foo
不是Foo
,那就是未定义的行为。编译器可以合理地推理“ok,所以我们调用delete foo
,因此*foo
是Foo
类型的对象”。
但如果foo
指向实际的Foo
,那么显然cond1
必须是假的,因为只有当它是假的时候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分支的逻辑反向传播,但是编译器确实做了类似于其他类型的UB的事情,甚至看起来像签名一样无害的东西整数溢出。
为了确保不会发生这种情况,您需要从编译代码的每个编译器中审核每个编译器中的每个优化。