何时使用虚析构函数?

1876

我对大部分的对象导向编程(OOP)理论有扎实的了解,但有一件事情经常让我困惑,那就是虚析构函数。

我曾以为无论如何,析构函数都会被调用,并且每个对象都要调用一次。

什么时候需要将析构函数声明为虚函数?为什么需要这样做呢?


8
请看:虚析构函数这篇文章讨论了在使用C++的时候,是否每个类都应该有一个虚析构函数。如果类被设计成应该被继承,那么最好给它一个虚析构函数,以避免可能的内存泄漏问题。但是,如果类不会被继承或者是一个简单的数据容器,那么就没有必要给它一个虚析构函数。 - Naveen
204
无论如何,每个析构函数 _down_ 都会被调用。 virtual 确保它从顶部开始而不是中间开始。 - Mooing Duck
19
相关问题:何时不应使用虚析构函数?当一个类没有任何虚函数时,通常不需要为其定义虚析构函数。但如果该类会被继承,并且在派生类中使用了动态内存分配,则必须为基类定义虚析构函数,以确保正确释放内存。否则,当删除指向派生类对象的基类指针时,可能会导致未定义的行为。 - Eitan T
5
我也对@MooingDuck的回答感到困惑。如果使用子类(下方)和超类(上方)的概念,它不应该是“up”而不是“down”吗? - Nibor
4
@Nibor:是的,_如果你使用那个概念的话_。我与之交谈的人中有一半认为超类是“上方”的,另一半则认为超类是“下方”的,因此两种标准存在冲突,这使得一切都很混乱。我认为将超类视为“上方”略微更常见,但我并没有被教导成这样。 - Mooing Duck
显示剩余5条评论
20个回答

5

虚基类析构函数是"最佳实践" - 你应该始终使用它们以避免(难以检测的)内存泄漏。使用虚基类析构函数,您可以确保调用了类继承链中所有析构函数(按正确顺序)。继承一个使用虚析构函数的基类会使派生类的析构函数自动成为虚函数,因此您不必在派生类析构函数声明中重新输入'virtual'。


我建议不要使用C++中大量的暗示行为。在你自己的项目中可以这样做,但在其他地方,显式的代码传达了意图而不仅仅是行为,此外,其他人可能并不完全了解C++。例如,你知道const全局变量和非const全局变量的默认链接行为吗?即使你知道,我保证大多数人都不知道这两种链接类型的存在。 - user904963

5
我认为这个问题的核心是虚函数和多态性,而不是特定的析构函数。下面是一个更清晰的示例:
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.

现在您应该明白何时使用虚析构函数。


不,这只是重新阐述了虚函数的基础知识,完全忽略了析构函数何时/为什么应该是一个 - 这并不直观,因此OP才会问这个问题。(另外,为什么要进行不必要的动态分配?只需执行 B b{}; A& a{b}; a.foo();。在 delete 之前检查 NULL(应该是 nullptr)- 带有不正确的缩进 - 是不必要的:delete nullptr; 被定义为无操作。如果有什么问题,你应该在调用 ->foo() 之前检查它,否则如果 new 失败,则可能会发生未定义行为。) - underscore_d
2
在空指针上调用 delete 是安全的(即,您不需要使用 if (a != NULL) 守卫)。 - James Adkison
@SaileshD 是的,我知道。这就是我在我的评论中所说的。 - James Adkison
@underscore_d 通常人们使用指针来展示这种行为,因为最常见的用例使用指针,例如拥有 std::vector<Base*>。当然,std::vector<Base&> 并不存在。 - user904963

5
如果您使用的是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

2
虽然这是可能的,但我不建议任何人使用它。虚析构函数的开销微不足道,这只会让事情变得混乱,特别是对于那些不知道这一点的经验较少的程序员。那个小小的“virtual”关键字可以为你节省很多痛苦。 - DexterHaxxor
2
出于好奇 - 为什么在 shared_ptr 的情况下会调用 Base 析构函数,但在 unique_ptr 的情况下不会? - Gr-Disarray
1
@Gr-Disarray 引用计数块具有指向资源的指针,该资源是一个带有虚析构函数的模板类类型。shared_ptr具有以其参数为模板的构造函数。它使用从其参数类继承的类实例化引用计数块。因此,在销毁引用计数块时,它会调用指针上的delete操作符。从这里开始,一切都按预期工作。我知道这简化了数组和内置类型的情况。 - Uri Raz
使用C++14或更新的版本时,这仍然正确吗?我认为unique_ptr在C++14之后发生了变化,例如添加了make_unique。也许委员会改进了unique_ptr吗? - r0n9

4

什么是虚析构函数或如何使用虚析构函数

类析构函数是一个与类同名的函数,前面加上 ~ ,它将重新分配由类分配的内存。为什么我们需要虚析构函数

看下面的示例,其中包含一些虚函数

该示例还说明了如何将字母转换为大写或小写

#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


3
我认为讨论“未定义”行为是有益的,或者至少讨论在没有虚析构函数或更准确地说没有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;
*/
}

我并不是在建议您是否需要虚析构函数,但我认为通常拥有它们是一个好习惯。我只是指出一个原因,如果您的基类/结构体没有vtable而派生类/结构体有vtable,并且您通过基类/结构体指针删除对象,则可能会导致崩溃。在这种情况下,您传递给堆的free例程的地址无效,这就是崩溃的原因。
如果您运行上面的代码,您将清楚地看到何时发生问题。当基类/结构体的this指针与派生类/结构体的this指针不同时,您将遇到此问题。在上面的示例中,结构体a和b没有vtable。结构体c和d具有vtable。因此,指向c或d对象实例的a或b指针将被修正以考虑vtable。如果您传递此a或b指针以进行删除,由于地址对堆的free例程无效,因此会崩溃。
如果您计划从基类指针删除具有vtable的派生实例,则需要确保基类具有vtable。其中一种方法是添加虚析构函数,您可能希望这样做以正确清理资源。

2
当你需要从基类调用派生类的析构函数时,你需要在基类中声明虚拟基类析构函数。

2
我认为这里的大多数回答都没有抓住重点,除了被接受的那个回答,这是一件好事。然而,让我补充一个不同观点的问题:如果您想对此类的实例进行多态删除,则需要虚拟析构函数。
这有点绕,所以让我详细说明一下:正如许多人指出的那样,如果您调用delete base_ptr并且析构函数不是虚拟的,则会产生不希望的行为。然而,有几个假设需要明确说明:
  • 如果您的类不是基类,希望您不要编写这样的代码。在这种情况下,我指的不是手动内存管理本身,而是公开从该类派生。一个不设计为基类的类不应该被继承,例如std::string。C++允许您自食其果。然而,这是您的责任,而不是基类没有虚析构函数的责任。
  • 如果析构函数不可访问(受保护或私有),则此代码将无法编译,因此不会发生不良行为。拥有受保护的析构函数很有用,特别是对于混合类,但也(在较小程度上)适用于接口。除非实际使用了虚函数,否则您不想承担虚函数的开销。将析构函数设置为受保护可以防止不良行为,但不会限制其他操作。
  • 如果您实际编写了一个应该派生的类,通常会有虚函数。作为使用者,您通常只会通过指向基类的指针来使用它们。当这种使用包括处理它们时,它也需要是多态的。这就是您应该使析构函数成为虚函数的情况。

如果您想了解该主题的不同观点,请阅读何时不应使用虚析构函数?


1

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

否则会发生像这样的问题:

假设你有一个包含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

(注:为简洁起见,我在此处使用了结构体,通常应使用类并指定为公共。)

0

virtual 的基本定义是它确定了一个类的成员函数是否可以在其派生类中被覆盖。

一个类的析构函数基本上在作用域的末尾被调用,但有一个问题,例如当我们在堆上(动态分配)定义一个实例时,我们应该手动删除它。

一旦指令被执行,基类析构函数被调用,但不是派生类的析构函数。

一个实际的例子是,在控制领域中,您必须操作效应器、执行器等。

在作用域的末尾,如果一个电源元素(执行器)的析构函数没有被调用,将会有致命后果。

#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;
}

-1
任何公开继承的类,无论是否具有多态性,都应该有一个虚析构函数。换句话说,如果它可以被基类指针指向,那么它的基类应该有一个虚析构函数。
如果是虚的,则先调用派生类的析构函数,然后再调用基类的析构函数。如果不是虚的,则只调用基类的析构函数。

我认为只有在“可以由基类指针指向”并且可以公开删除时才需要这样做。但是,我想养成添加虚拟析构函数的习惯也无妨,以防以后可能会需要它们。 - underscore_d

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接