关于C++:何时使用虚拟析构函数virtual destructors?

When to use virtual destructors?

我对大多数OO理论都有很好的理解,但是有一件事让我很困惑,那就是虚拟析构函数。

我认为无论链中的任何对象是什么,都会调用析构函数。

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


当您可能通过指向基类的指针删除派生类的实例时,虚拟析构函数非常有用:

1
2
3
4
5
6
7
8
9
10
11
12
class Base
{
    // some virtual methods
};

class Derived : public Base
{
    ~Derived()
    {
        // Do some important cleanup
    }
};

在这里,您会注意到我没有声明base的析构函数是virtual。现在,让我们看看下面的片段:

1
2
3
Base *b = new Derived();
// use b
delete b; // Here's the problem!

由于base的析构函数不是virtualbBase*指向Derived对象,因此delete b具有未定义的行为:

[In delete b], if the static type of the
object to be deleted is different from its dynamic type, the static
type shall be a base class of the dynamic type of the object to be
deleted and the static type shall have a virtual destructor or the
behavior is undefined.

在大多数实现中,对析构函数的调用将像任何非虚拟代码一样被解析,这意味着将调用基类的析构函数,而不是派生类的析构函数,从而导致资源泄漏。

综上所述,当基类的析构函数要被多态地操纵时,总是要使它们的析构函数virtual

如果要防止通过基类指针删除实例,可以使基类析构函数受保护且非虚化;这样,编译器就不会允许您在基类指针上调用delete

在本文中,您可以从Herb Sutter了解更多关于虚拟性和虚拟基类析构函数的信息。


虚拟构造函数不可能,但虚拟析构函数是可能的。让我们试验一下……

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <iostream>

using namespace std;

class Base
{
public:
    Base(){
        cout <<"Base Constructor Called
"
;
    }
    ~Base(){
        cout <<"Base Destructor called
"
;
    }
};

class Derived1: public Base
{
public:
    Derived1(){
        cout <<"Derived constructor called
"
;
    }
    ~Derived1(){
        cout <<"Derived destructor called
"
;
    }
};

int main()
{
    Base *b = new Derived1();
    delete b;
}

上述代码输出以下内容:

1
2
3
Base Constructor Called
Derived constructor called
Base Destructor called

派生对象的构造遵循构造规则,但是当我们删除"b"指针(基指针)时,我们发现只有基析构函数是调用的,但不能这样做。为了做适当的事情,我们必须使基析构函数成为虚拟的。现在让我们看看下面发生了什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <iostream>

using namespace std;

class Base
{
public:
    Base(){
        cout <<"Base Constructor Called
"
;
    }
    virtual ~Base(){
        cout <<"Base Destructor called
"
;
    }
};

class Derived1: public Base
{
public:
    Derived1(){
        cout <<"Derived constructor called
"
;
    }
    ~Derived1(){
        cout <<"Derived destructor called
"
;
    }
};

int main()
{
    Base *b = new Derived1();
    delete b;
}

输出变化如下:

1
2
3
4
Base Constructor Called
Derived constructor called
Derived destructor called
Base Destructor called

所以基本指针的销毁(它对派生对象进行分配!)遵循破坏规则,即首先派生,然后是基。另一方面,对于构造函数来说,没有什么类似于虚拟构造函数。


在多态基类中声明析构函数为虚拟的。这是Scott Meyers的"有效C++"中的第7项。迈耶斯接着总结说,如果一个类有任何虚拟函数,它应该有一个虚拟析构函数,而那些没有设计成基类或没有设计成多态使用的类不应该声明虚拟析构函数。


还要注意,在没有虚拟析构函数的情况下删除基类指针将导致未定义的行为。我最近学到的东西:

C++中如何重写删除行为?

我已经使用C++多年了,我还是设法把自己挂起来。


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


通过指向基类的指针调用析构函数

1
2
3
4
5
6
7
8
9
10
11
12
13
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()

具有非虚拟析构函数的抽象类

如果您不打算通过指向对象基类的指针删除对象,那么就不需要使用虚拟析构函数。你只要把它改成1号[9号],这样它就不会被意外地称为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 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.
}


我喜欢考虑接口和接口的实现。在C++语言中,接口是纯虚拟类。析构函数是接口的一部分,应该实现。因此,析构函数应该是纯虚拟的。建造师怎么样?构造函数实际上不是接口的一部分,因为对象总是显式实例化的。


简单地说,虚拟析构函数是在删除指向派生类对象的基类指针时,按正确的顺序销毁资源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
 #include<iostream>
 using namespace std;
 class B{
    public:
       B(){
          cout<<"B()
"
;
       }
       virtual ~B(){
          cout<<"~B()
"
;
       }
 };
 class D: public B{
    public:
       D(){
          cout<<"D()
"
;
       }
       ~D(){
          cout<<"~D()
"
;
       }
 };
 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


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

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

如果派生类析构函数是虚拟的,那么对象将按顺序(先是派生对象,然后是基对象)进行分解。如果派生类析构函数不是虚拟的,那么只会删除基类对象(因为指针是基类"base*myobj")。所以派生对象会有内存泄漏。


虚拟基类析构函数是"最佳实践"——您应该始终使用它们来避免(难以检测)内存泄漏。使用它们,可以确保调用类继承链中的所有析构函数(按照适当的顺序)。使用虚析构函数从基类继承会使继承类的析构函数也自动虚化(因此您不必在继承类析构函数声明中重新键入"virtual")。


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

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

请参阅以下带有一些虚拟函数的示例

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
#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()"<<"
"
;
    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()"<<"
"
;
    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类的析构函数。

使用虚拟析构函数查看下一个示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
#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()"<<"
"
;
      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()"<<"
"
;
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;
}

虚拟析构函数将显式调用类的最派生的运行时析构函数,以便它能够以正确的方式清除对象。

或者访问链接

https://web.archive.org/web/20130822173509/http://www.programminggallery.com/article_details.php?第138条


我认为这个问题的核心是关于虚拟方法和多态性,而不是具体的析构函数。下面是一个更清楚的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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;
}

将打印出来:

1
This is B.

如果没有virtual,它将打印出:

1
This is A.

现在您应该了解何时使用虚拟析构函数。


我认为讨论"未定义"行为是有益的,或者至少讨论在没有虚拟析构函数或更确切地说没有vtable的情况下通过基类(/struct)删除时可能发生的"崩溃"未定义行为。下面的代码列出了几个简单的结构(类也是如此)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
#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)没有vtable,并且您通过基类(/struct)指针删除了一个对象,则可能导致崩溃的原因。在这种情况下,传递给堆的空闲例程的地址是无效的,因此是崩溃的原因。

如果您运行上述代码,您将在问题发生时清楚地看到。当基类的这个指针(/struct)与派生类的这个指针(/struct)不同时,就会遇到这个问题。在上面的示例中,结构A和B没有vtables。结构C和D没有vtables。因此,指向C或D对象实例的A或B指针将固定,以说明vtable。如果您将这个a或b指针传递给delete,它将崩溃,因为地址对堆的空闲例程无效。

如果计划从基类指针中删除具有vtables的派生实例,则需要确保基类具有vtable。一种方法是添加一个虚拟析构函数,您可能无论如何都希望它正确地清理资源。


如果使用shared_ptr(仅共享ptr,而不是唯一ptr),则不必使用基类析构函数virtual:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <iostream>
#include <memory>

using namespace std;

class Base
{
public:
    Base(){
        cout <<"Base Constructor Called
"
;
    }
    ~Base(){ // not virtual
        cout <<"Base Destructor called
"
;
    }
};

class Derived: public Base
{
public:
    Derived(){
        cout <<"Derived constructor called
"
;
    }
    ~Derived(){
        cout <<"Derived destructor called
"
;
    }
};

int main()
{
    shared_ptr<Base> b(new Derived());
}

输出:

1
2
3
4
Base Constructor Called
Derived constructor called
Derived destructor called
Base Destructor called

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


任何公开继承的类,不管是否具有多态性,都应该有一个虚拟析构函数。换句话说,如果它可以由基类指针指向,那么它的基类应该有一个虚拟析构函数。

如果是虚拟的,则调用派生类析构函数,然后调用基类构造函数。如果不是虚拟的,则只调用基类析构函数。