我对大多数 OOP
理论都有深刻的理解,但让我很困惑的一件事是虚拟析构函数。
我认为析构函数总是被调用,不管是什么,对于链中的每个对象。
你打算什么时候把它们变成虚拟的,为什么?
virtual
确保它从顶部而不是中间开始。
当您可能通过指向基类的指针删除派生类的实例时,虚拟析构函数很有用:
class Base
{
// some virtual methods
};
class Derived : public Base
{
~Derived()
{
// Do some important cleanup
}
};
在这里,您会注意到我没有将 Base 的析构函数声明为 virtual
。现在,让我们看一下以下代码段:
Base *b = new Derived();
// use b
delete b; // Here's the problem!
由于 Base 的析构函数不是 virtual
而 b
是一个指向 Derived
对象的 Base*
,所以 delete b
有 undefined behaviour:
[在delete b中],如果要删除的对象的静态类型与其动态类型不同,则静态类型应为要删除对象的动态类型的基类,且静态类型应具有虚析构函数或行为未定义。
在大多数实现中,对析构函数的调用将像任何非虚拟代码一样被解析,这意味着将调用基类的析构函数而不是派生类的析构函数,从而导致资源泄漏。
总而言之,当基类的析构函数要被多态操作时,总是将它们设为virtual
。
如果要防止通过基类指针删除实例,可以使基类析构函数受保护且非虚拟;通过这样做,编译器将不允许您在基类指针上调用 delete
。
您可以在 this article from Herb Sutter 中了解有关虚拟性和虚拟基类析构函数的更多信息。
虚拟构造函数是不可能的,但虚拟析构函数是可能的。让我们进行实验......
#include <iostream>
using namespace std;
class Base
{
public:
Base(){
cout << "Base Constructor Called\n";
}
~Base(){
cout << "Base Destructor called\n";
}
};
class Derived1: public Base
{
public:
Derived1(){
cout << "Derived constructor called\n";
}
~Derived1(){
cout << "Derived destructor called\n";
}
};
int main()
{
Base *b = new Derived1();
delete b;
}
上面的代码输出如下:
Base Constructor Called
Derived constructor called
Base Destructor called
派生对象的构造遵循构造规则,但是当我们删除“b”指针(基指针)时,我们发现只调用了基析构函数。但这绝不能发生。为了做适当的事情,我们必须将基本析构函数设为虚拟。现在让我们看看下面会发生什么:
#include <iostream>
using namespace std;
class Base
{
public:
Base(){
cout << "Base Constructor Called\n";
}
virtual ~Base(){
cout << "Base Destructor called\n";
}
};
class Derived1: public Base
{
public:
Derived1(){
cout << "Derived constructor called\n";
}
~Derived1(){
cout << "Derived destructor called\n";
}
};
int main()
{
Base *b = new Derived1();
delete b;
}
输出更改如下:
Base Constructor Called
Derived Constructor called
Derived destructor called
Base destructor called
所以基指针的销毁(它在派生对象上进行分配!)遵循销毁规则,即首先是派生,然后是基。另一方面,没有什么能比得上虚拟构造函数。
在多态基类中声明析构函数为虚拟的。这是 Scott Meyers 的 Effective C++ 中的第 7 项。 Meyers 继续总结说,如果一个类有任何虚函数,它应该有一个虚析构函数,并且那些不是被设计为基类或不是被设计为多态使用的类不应该不< /em> 声明虚拟析构函数。
const Base& = make_Derived();
。在这种情况下,将调用 Derived
prvalue 的析构函数,即使它不是虚拟的,因此可以节省 vtables/vpointers 引入的开销。当然范围是相当有限的。 Andrei Alexandrescu 在他的书 Modern C++ Design 中提到了这一点。
另请注意,在没有虚拟析构函数时删除基类指针将导致未定义的行为。我最近学到的东西:
How should overriding delete in C++ behave?
多年来我一直在使用 C++,但我仍然设法上吊。
只要您的类是多态的,就使析构函数成为虚拟的。
通过指向基类的指针调用析构函数
struct Base {
virtual void f() {}
virtual ~Base() {}
};
struct Derived : Base {
void f() override {}
~Derived() override {}
};
Base* base = new Derived;
base->f(); // calls Derived::f
base->~Base(); // calls Derived::~Derived
虚拟析构函数调用与任何其他虚拟函数调用没有什么不同。
对于 base->f()
,调用将被分派到 Derived::f()
,对于 base->~Base()
也是相同的 - 它的覆盖函数 - Derived::~Derived()
将被调用。
当间接调用析构函数时也会发生同样的情况,例如 delete base;
。 delete
语句将调用 base->~Base()
,后者将被分派到 Derived::~Derived()
。
具有非虚拟析构函数的抽象类
如果您不打算通过指向其基类的指针来删除对象 - 那么就不需要虚拟析构函数。只需将其设为 protected
,以免被意外调用:
// library.hpp
struct Base {
virtual void f() = 0;
protected:
~Base() = default;
};
void CallsF(Base& base);
// CallsF is not going to own "base" (i.e. call "delete &base;").
// It will only call Base::f() so it doesn't need to access Base::~Base.
//-------------------
// application.cpp
struct Derived : Base {
void f() override { ... }
};
int main() {
Derived derived;
CallsF(derived);
// No need for virtual destructor here as well.
}
~Derived()
,即使它只是 ~Derived() = default
?还是语言暗示了这一点(使其可以安全省略)?
protected
部分,或使用 override
确保它是虚拟的。
base->~Base()
是否合适?根据您所说,不会调用 Base::~Base()
,然后会发生内存泄漏。我对吗?
简单来说,虚拟析构函数就是在删除指向派生类对象的基类指针时,按正确的顺序销毁资源。
#include<iostream>
using namespace std;
class B{
public:
B(){
cout<<"B()\n";
}
virtual ~B(){
cout<<"~B()\n";
}
};
class D: public B{
public:
D(){
cout<<"D()\n";
}
~D(){
cout<<"~D()\n";
}
};
int main(){
B *b = new D();
delete b;
return 0;
}
OUTPUT:
B()
D()
~D()
~B()
==============
If you don't give ~B() as virtual. then output would be
B()
D()
~B()
where destruction of ~D() is not done which leads to leak
delete
会导致未定义的行为。
当您希望不同的析构函数在通过基类指针删除对象时遵循正确的顺序时,析构函数的虚拟关键字是必要的。例如:
Base *myObj = new Derived();
// Some code which is using myObj object
myObj->fun();
//Now delete the object
delete myObj ;
如果您的基类析构函数是虚拟的,那么对象将按顺序销毁(首先是派生对象,然后是基类)。如果您的基类析构函数不是虚拟的,那么只有基类对象将被删除(因为指针属于基类“Base *myObj”)。所以派生对象会有内存泄漏。
我喜欢思考接口和接口的实现。在 C++ 中,接口是纯虚拟类。析构函数是接口的一部分,并有望实现。因此析构函数应该是纯虚拟的。构造函数呢?构造函数实际上不是接口的一部分,因为对象总是被显式实例化。
virtual
,则它在派生类中自动为 virtual
,即使它没有这样声明。
虚拟基类析构函数是“最佳实践”——您应该始终使用它们来避免(难以检测)内存泄漏。使用它们,您可以确保类的继承链中的所有析构函数都被调用(以正确的顺序)。使用虚拟析构函数从基类继承会使继承类的析构函数也自动成为虚拟的(因此您不必在继承类析构函数声明中重新键入“虚拟”)。
const
全局变量与非 const
全局变量的默认链接行为吗?即使您这样做,我也保证大多数人都不知道,甚至不知道存在两种类型的链接。
我认为这个问题的核心是关于虚拟方法和多态性,而不是具体的析构函数。这是一个更清晰的例子:
class A
{
public:
A() {}
virtual void foo()
{
cout << "This is A." << endl;
}
};
class B : public A
{
public:
B() {}
void foo()
{
cout << "This is B." << endl;
}
};
int main(int argc, char* argv[])
{
A *a = new B();
a->foo();
if(a != NULL)
delete a;
return 0;
}
将打印出:
This is B.
如果没有 virtual
,它将打印出:
This is A.
现在您应该了解何时使用虚拟析构函数。
B b{}; A& a{b}; a.foo();
。检查 NULL
- 应该是 nullptr
- 在 delete
ing 之前 - 不正确的缩进 - 不需要:delete nullptr;
被定义为无操作。如果有的话,您应该在调用 ->foo()
之前检查这一点,否则如果 new
以某种方式失败,可能会发生未定义的行为。)
NULL
指针上调用 delete
是安全的(即,您不需要 if (a != NULL)
保护)。
std::vector<Base*>
。当然,std::vector<Base&>
不是一个东西。
如果您使用 shared_ptr
(仅 shared_ptr,而不是 unique_ptr),则不必将基类析构函数设为虚拟:
#include <iostream>
#include <memory>
using namespace std;
class Base
{
public:
Base(){
cout << "Base Constructor Called\n";
}
~Base(){ // not virtual
cout << "Base Destructor called\n";
}
};
class Derived: public Base
{
public:
Derived(){
cout << "Derived constructor called\n";
}
~Derived(){
cout << "Derived destructor called\n";
}
};
int main()
{
shared_ptr<Base> b(new Derived());
}
输出:
Base Constructor Called
Derived constructor called
Derived destructor called
Base Destructor called
virtual
关键字可以让您免于痛苦。
什么是虚拟析构函数或如何使用虚拟析构函数
类析构函数是与前面的类同名的函数 ~ 它将重新分配由类分配的内存。为什么我们需要一个虚拟析构函数
请参阅以下带有一些虚拟功能的示例
该示例还告诉您如何将字母转换为大写或小写
#include "stdafx.h"
#include<iostream>
using namespace std;
// program to convert the lower to upper orlower
class convertch
{
public:
//void convertch(){};
virtual char* convertChar() = 0;
~convertch(){};
};
class MakeLower :public convertch
{
public:
MakeLower(char *passLetter)
{
tolower = true;
Letter = new char[30];
strcpy(Letter, passLetter);
}
virtual ~MakeLower()
{
cout<< "called ~MakeLower()"<<"\n";
delete[] Letter;
}
char* convertChar()
{
size_t len = strlen(Letter);
for(int i= 0;i<len;i++)
Letter[i] = Letter[i] + 32;
return Letter;
}
private:
char *Letter;
bool tolower;
};
class MakeUpper : public convertch
{
public:
MakeUpper(char *passLetter)
{
Letter = new char[30];
toupper = true;
strcpy(Letter, passLetter);
}
char* convertChar()
{
size_t len = strlen(Letter);
for(int i= 0;i<len;i++)
Letter[i] = Letter[i] - 32;
return Letter;
}
virtual ~MakeUpper()
{
cout<< "called ~MakeUpper()"<<"\n";
delete Letter;
}
private:
char *Letter;
bool toupper;
};
int _tmain(int argc, _TCHAR* argv[])
{
convertch *makeupper = new MakeUpper("hai");
cout<< "Eneterd : hai = " <<makeupper->convertChar()<<" ";
delete makeupper;
convertch *makelower = new MakeLower("HAI");;
cout<<"Eneterd : HAI = " <<makelower->convertChar()<<" ";
delete makelower;
return 0;
}
从上面的示例中,您可以看到 MakeUpper 和 MakeLower 类的析构函数都没有被调用。
使用虚拟析构函数查看下一个示例
#include "stdafx.h"
#include<iostream>
using namespace std;
// program to convert the lower to upper orlower
class convertch
{
public:
//void convertch(){};
virtual char* convertChar() = 0;
virtual ~convertch(){}; // defined the virtual destructor
};
class MakeLower :public convertch
{
public:
MakeLower(char *passLetter)
{
tolower = true;
Letter = new char[30];
strcpy(Letter, passLetter);
}
virtual ~MakeLower()
{
cout<< "called ~MakeLower()"<<"\n";
delete[] Letter;
}
char* convertChar()
{
size_t len = strlen(Letter);
for(int i= 0;i<len;i++)
{
Letter[i] = Letter[i] + 32;
}
return Letter;
}
private:
char *Letter;
bool tolower;
};
class MakeUpper : public convertch
{
public:
MakeUpper(char *passLetter)
{
Letter = new char[30];
toupper = true;
strcpy(Letter, passLetter);
}
char* convertChar()
{
size_t len = strlen(Letter);
for(int i= 0;i<len;i++)
{
Letter[i] = Letter[i] - 32;
}
return Letter;
}
virtual ~MakeUpper()
{
cout<< "called ~MakeUpper()"<<"\n";
delete Letter;
}
private:
char *Letter;
bool toupper;
};
int _tmain(int argc, _TCHAR* argv[])
{
convertch *makeupper = new MakeUpper("hai");
cout<< "Eneterd : hai = " <<makeupper->convertChar()<<" \n";
delete makeupper;
convertch *makelower = new MakeLower("HAI");;
cout<<"Eneterd : HAI = " <<makelower->convertChar()<<"\n ";
delete makelower;
return 0;
}
虚拟析构函数将显式调用类的最派生的运行时析构函数,以便能够以适当的方式清除对象。
或访问链接
我认为讨论“未定义”行为是有益的,或者至少是在通过没有虚拟析构函数或更准确地说没有 vtable 的基类(/结构)删除时可能发生的“崩溃”未定义行为。下面的代码列出了一些简单的结构(类也是如此)。
#include <iostream>
using namespace std;
struct a
{
~a() {}
unsigned long long i;
};
struct b : a
{
~b() {}
unsigned long long j;
};
struct c : b
{
~c() {}
virtual void m3() {}
unsigned long long k;
};
struct d : c
{
~d() {}
virtual void m4() {}
unsigned long long l;
};
int main()
{
cout << "sizeof(a): " << sizeof(a) << endl;
cout << "sizeof(b): " << sizeof(b) << endl;
cout << "sizeof(c): " << sizeof(c) << endl;
cout << "sizeof(d): " << sizeof(d) << endl;
// No issue.
a* a1 = new a();
cout << "a1: " << a1 << endl;
delete a1;
// No issue.
b* b1 = new b();
cout << "b1: " << b1 << endl;
cout << "(a*) b1: " << (a*) b1 << endl;
delete b1;
// No issue.
c* c1 = new c();
cout << "c1: " << c1 << endl;
cout << "(b*) c1: " << (b*) c1 << endl;
cout << "(a*) c1: " << (a*) c1 << endl;
delete c1;
// No issue.
d* d1 = new d();
cout << "d1: " << d1 << endl;
cout << "(c*) d1: " << (c*) d1 << endl;
cout << "(b*) d1: " << (b*) d1 << endl;
cout << "(a*) d1: " << (a*) d1 << endl;
delete d1;
// Doesn't crash, but may not produce the results you want.
c1 = (c*) new d();
delete c1;
// Crashes due to passing an invalid address to the method which
// frees the memory.
d1 = new d();
b1 = (b*) d1;
cout << "d1: " << d1 << endl;
cout << "b1: " << b1 << endl;
delete b1;
/*
// This is similar to what's happening above in the "crash" case.
char* buf = new char[32];
cout << "buf: " << (void*) buf << endl;
buf += 8;
cout << "buf after adding 8: " << (void*) buf << endl;
delete buf;
*/
}
我并不是建议您是否需要虚拟析构函数,尽管我认为一般来说拥有它们是一个好习惯。我只是指出如果您的基类(/struct)没有 vtable 而您的派生类(/struct)有并且您通过基类(/struct)删除对象,您最终可能会崩溃的原因指针。在这种情况下,您传递给堆的空闲例程的地址是无效的,因此是崩溃的原因。
如果你运行上面的代码,你会在问题发生时清楚地看到。当基类(/struct)的this指针与派生类(/struct)的this指针不同时,你就会遇到这个问题。在上面的示例中,结构 a 和 b 没有 vtable。结构 c 和 d 确实有 vtables。因此,指向 ac 或 d 对象实例的 a 或 b 指针将被修复以解释 vtable。如果你将这个 a 或 b 指针传递给 delete,它会因为地址对堆的空闲例程无效而崩溃。
如果您计划从基类指针中删除具有 vtable 的派生实例,则需要确保基类具有 vtable。一种方法是添加一个虚拟析构函数,无论如何您都可能希望正确清理资源。
当您需要从基类调用派生类析构函数时。您需要在基类中声明虚拟基类析构函数。
我认为这里的大多数答案都没有抓住重点,除了接受的答案,这是一件好事。然而,让我再添加一个关于这个问题的不同观点:如果你想多态地删除这个类的实例,你需要一个虚拟析构函数。
这种回避了这个问题,所以让我详细说明一下:正如许多人指出的那样,如果您调用 delete base_ptr
并且析构函数不是虚拟的,则会出现不良行为。但是,有几个假设需要明确:
如果您的类不是基类,那么您希望不要编写这样的代码。在这种情况下,我不是指手动内存管理,它本身就是不好的,而是从这个类公开派生的。不应该从不被设计为基类的类继承,例如 std::string。 C++ 允许你在脚下射击自己。这是你的错,而不是没有虚拟析构函数的基类。
如果析构函数不可访问(受保护或私有),则此代码将无法编译,因此不会发生不希望的行为。拥有一个受保护的析构函数是很有用的,特别是对于 mixin,而且(在较小程度上)对于接口也是如此。除非您实际使用它们,否则您不想招致虚函数的开销。相反,使析构函数受保护可以防止不良行为,但不会限制您。
如果您实际上编写了一个应该派生的类,那么无论如何您通常都会有虚函数。作为它们的用户,您通常只会通过指向基类的指针来使用它们。当这种用途包括处置它们时,它也需要是多态的。这就是你应该将析构函数设为虚拟的情况。
有关该主题的类似不同观点,另请阅读When should you not use virtual destructors?
关于 virtual
的基本定义是它确定类的成员函数是否可以在其派生类中被覆盖。
一个类的 D-tor 基本上是在作用域的末尾调用的,但是有一个问题,比如我们在 Heap 上定义一个实例(动态分配),我们应该手动删除它。
指令一执行,就会调用基类析构函数,但不会调用派生的析构函数。
一个实际的例子是,在控制领域,您必须操纵效应器、执行器。
在作用域结束时,如果不调用其中一个动力元素(Actuator)的析构函数,将会产生致命的后果。
#include <iostream>
class Mother{
public:
Mother(){
std::cout<<"Mother Ctor"<<std::endl;
}
virtual~Mother(){
std::cout<<"Mother D-tor"<<std::endl;
}
};
class Child: public Mother{
public:
Child(){
std::cout<<"Child C-tor"<<std::endl;
}
~Child(){
std::cout<<"Child D-tor"<<std::endl;
}
};
int main()
{
Mother *c = new Child();
delete c;
return 0;
}
除非您有充分的理由不这样做,否则将所有析构函数设为虚拟。
否则会发生这样的邪恶:
假设您有一个包含 Apple 和 Orange 对象的 Fruit 指针数组。
当您从 Fruit 对象的集合中删除时,~Apple() 和 ~Orange() 将无法调用,除非 ~Fruit() 是虚拟的。
正确完成的示例:
#include <iostream>
using namespace std;
struct Fruit { // good
virtual ~Fruit() { cout << "peel or core should have been tossed" << endl; }
};
struct Apple: Fruit { virtual ~Apple() {cout << "toss core" << endl; } };
struct Orange: Fruit { virtual ~Orange() {cout << "toss peel" << endl; } };
int main() {
Fruit *basket[]={ new Apple(), new Orange() };
for (auto fruit: basket) delete fruit;
};
良好的输出
toss core
peel or core should have been tossed
toss peel
peel or core should have been tossed
例子做错了:
#include <iostream>
using namespace std;
struct Fruit { // bad
~Fruit() { cout << "peel or core should have been tossed" << endl; }
};
struct Apple: Fruit { virtual ~Apple() {cout << "toss core" << endl; } };
struct Orange: Fruit { virtual ~Orange() {cout << "toss peel" << endl; } };
int main() {
Fruit *basket[]={ new Apple(), new Orange() };
for (auto fruit: basket) delete fruit;
};
输出不良
peel or core should have been tossed
peel or core should have been tossed
(注意:为了简洁起见,我使用 struct 的地方,通常使用 class 并指定 public)
任何公开继承的类,无论是否多态,都应该有一个虚拟析构函数。换句话说,如果它可以被基类指针指向,那么它的基类应该有一个虚析构函数。
如果是虚拟的,则调用派生类析构函数,然后调用基类析构函数。如果不是虚拟的,则仅调用基类析构函数。
Base
和Derived
有 all 个自动存储变量怎么办?即在析构函数中没有要执行的“特殊”或额外的自定义代码。那么完全不写任何析构函数可以吗?还是派生类仍然有内存泄漏?