我对大多数OOP理论都有很好的理解,但最让我困惑的是虚拟析构函数。
我以为析构函数总是被调用,不管是什么,也不管是链中的每个对象。
你打算什么时候让它们虚拟化?为什么?
我对大多数OOP理论都有很好的理解,但最让我困惑的是虚拟析构函数。
我以为析构函数总是被调用,不管是什么,也不管是链中的每个对象。
你打算什么时候让它们虚拟化?为什么?
当前回答
当您可能通过指向基类的指针删除派生类的实例时,虚拟析构函数非常有用:
class Base
{
// some virtual methods
};
class Derived : public Base
{
~Derived()
{
// Do some important cleanup
}
};
在这里,您会注意到我没有将Base的析构函数声明为虚拟。现在,让我们看一下以下片段:
Base *b = new Derived();
// use b
delete b; // Here's the problem!
由于Base的析构函数不是虚拟的,而b是指向派生对象的Base*,因此删除b具有未定义的行为:
[在delete b]中,如果要删除的对象与其动态类型不同,静态类型应为对象的动态类型的基类删除,静态类型应具有虚拟析构函数或行为未定义。
在大多数实现中,对析构函数的调用将像任何非虚拟代码一样被解析,这意味着将调用基类的析构函数,而不是派生类的析构器,从而导致资源泄漏。
总之,当基类的析构函数要以多态方式操作时,请始终将其设为虚拟。
如果要防止通过基类指针删除实例,可以使基类析构函数受保护且非虚拟;通过这样做,编译器不会允许您在基类指针上调用delete。
在本文中,您可以从HerbSutter了解更多关于虚拟性和虚拟基类析构函数的信息。
其他回答
什么是虚拟析构函数或如何使用虚拟析构器
类析构函数是一个与~前面的类同名的函数,它将重新分配该类分配的内存。为什么我们需要虚拟析构函数
请参见以下示例中的一些虚拟函数
该示例还说明了如何将字母转换为大写或小写
#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;
}
虚拟析构函数将显式调用类的最派生的运行时析构函数,以便能够以正确的方式清除对象。
或访问链接
https://web.archive.org/web/20130822173509/http://www.programminggallery.com/article_details.php?article_id=138
我认为这里的大多数答案都没有抓住重点,除了公认的答案,这是一件好事。然而,让我再补充一个对这个问题有不同看法的问题:如果你想多态地删除这个类的实例,你需要一个虚拟析构函数。
这种方式回避了这个问题,所以让我详细说明一下:正如许多人所指出的,如果调用delete base_ptr并且析构函数不是虚拟的,就会出现不希望的行为。然而,有几个假设需要明确:
如果您的类不是基类,那么希望您不会编写这样的代码。在本例中,我不是指手动内存管理,它本身就很糟糕,而是从这个类中公开派生出来的。不应继承未设计为基类的类,例如std::string。C++可以让你射自己的脚。这是你的错,而不是基类没有虚拟析构函数。如果析构函数不可访问(受保护的或私有的),则此代码不会编译,因此不会出现不希望的行为。有一个受保护的析构函数是有用的,特别是对于mixin,但对于接口(在较小程度上)也是有用的。除非您实际使用了虚拟函数,否则您不希望产生虚拟函数的开销。相反,使析构函数受到保护可以防止不期望的行为,但不会限制您的行为。如果您实际上编写了一个应该派生自的类,那么通常都会有虚拟函数。作为它们的用户,通常只能通过指向基类的指针来使用它们。当这种使用包括处理它们时,它也需要是多态的。当您应该将析构函数设为虚拟时,就会出现这种情况。
对于这个主题的一个类似的不同观点,也可以阅读“什么时候不应该使用虚拟析构函数?”?
通过指向基类的指针调用析构函数
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()。
具有非虚拟析构函数的抽象类
若您不打算通过指向其基类的指针删除对象,那个么就不需要使用虚拟析构函数。只需保护它,使其不会被意外调用:
// 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.
}
关于virtual的一个基本定义是它确定类的成员函数是否可以在其派生类中被覆盖。
类的D-tor基本上在作用域的末尾被调用,但存在一个问题,例如,当我们在堆(动态分配)上定义一个实例时,我们应该手动删除它。
一旦指令被执行,就会调用基类析构函数,但不会调用派生的析构函数。
一个实际的例子是,在控制场中,你必须操纵效应器和致动器。
在范围结束时,如果没有调用其中一个动力元件(执行器)的析构函数,将产生致命的后果。
#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;
}
我建议这样做:如果类或结构不是最终的,那么应该为其定义虚拟析构函数。
我知道这看起来像是一种过度警惕的过度杀戮,成为一种经验法则。但是,这是确保从类派生的人在使用基指针删除时不会使用UB的唯一方法。
Scott Meyers在下面引用的有效C++中的建议很好,但不足以确定。
如果一个类有任何虚函数,它应该有一个虚函数析构函数,并且类是否设计为基类设计用于多态性的不应声明虚拟析构函数。
例如,在下面的程序中,基类B没有任何虚拟函数,因此根据Meyer的说法,您不需要编写虚拟析构函数。然而,如果您没有以下UB:
#include <iostream>
struct A
{
~A()
{
std::cout << "A::~A()" << std::endl;
}
};
struct B
{
};
struct C : public B
{
A a;
};
int main(int argc, char *argv[])
{
B *b = new C;
delete b; // UB, and won't print "A::~A()"
return 0;
}