我对大多数OOP理论都有很好的理解,但最让我困惑的是虚拟析构函数。

我以为析构函数总是被调用,不管是什么,也不管是链中的每个对象。

你打算什么时候让它们虚拟化?为什么?


当前回答

我认为这里的大多数答案都没有抓住重点,除了公认的答案,这是一件好事。然而,让我再补充一个对这个问题有不同看法的问题:如果你想多态地删除这个类的实例,你需要一个虚拟析构函数。

这种方式回避了这个问题,所以让我详细说明一下:正如许多人所指出的,如果调用delete base_ptr并且析构函数不是虚拟的,就会出现不希望的行为。然而,有几个假设需要明确:

如果您的类不是基类,那么希望您不会编写这样的代码。在本例中,我不是指手动内存管理,它本身就很糟糕,而是从这个类中公开派生出来的。不应继承未设计为基类的类,例如std::string。C++可以让你射自己的脚。这是你的错,而不是基类没有虚拟析构函数。如果析构函数不可访问(受保护的或私有的),则此代码不会编译,因此不会出现不希望的行为。有一个受保护的析构函数是有用的,特别是对于mixin,但对于接口(在较小程度上)也是有用的。除非您实际使用了虚拟函数,否则您不希望产生虚拟函数的开销。相反,使析构函数受到保护可以防止不期望的行为,但不会限制您的行为。如果您实际上编写了一个应该派生自的类,那么通常都会有虚拟函数。作为它们的用户,通常只能通过指向基类的指针来使用它们。当这种使用包括处理它们时,它也需要是多态的。当您应该将析构函数设为虚拟时,就会出现这种情况。

对于这个主题的一个类似的不同观点,也可以阅读“什么时候不应该使用虚拟析构函数?”?

其他回答

当您希望不同的析构函数在通过基类指针删除对象时遵循正确的顺序时,析构函数的虚拟关键字是必需的。例如:

Base *myObj = new Derived();
// Some code which is using myObj object
myObj->fun();
//Now delete the object
delete myObj ; 

如果基类析构函数是虚拟的,那么对象将按顺序被析构函数(首先是派生对象,然后是基)。如果基类析构函数不是虚拟的,那么只有基类对象会被删除(因为指针是基类“base*myObj”)。因此派生对象将存在内存泄漏。

只要类是多态的,就将析构函数设为虚拟。

我建议这样做:如果类或结构不是最终的,那么应该为其定义虚拟析构函数。

我知道这看起来像是一种过度警惕的过度杀戮,成为一种经验法则。但是,这是确保从类派生的人在使用基指针删除时不会使用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;
}

将所有析构函数都设为虚拟,除非你有充分的理由不这样做。

否则会发生这样的邪恶:

假设您有一个包含Apple和Orange对象的Fruit指针数组。

从Fruit对象集合中删除时,除非~Fruit()是虚拟的,否则无法调用~Apple()和~Orange()。

正确完成示例:

#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)

关于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;
}