我对大多数面向对象理论有扎实的了解,但令我困惑的一件事是虚拟析构函数。
我以为无论链中的每个对象是什么,析构函数总是被调用。
您打算什么时候将它们虚拟化?为什么?
virtual
确保它从顶部开始而不是从中间开始。
我对大多数面向对象理论有扎实的了解,但令我困惑的一件事是虚拟析构函数。
我以为无论链中的每个对象是什么,析构函数总是被调用。
您打算什么时候将它们虚拟化?为什么?
virtual
确保它从顶部开始而不是从中间开始。
Answers:
当您可能通过指向基类的指针删除派生类的实例时,虚拟析构函数很有用:
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
是Base*
指向Derived
对象的指针,因此delete b
具有未定义的行为:
[在
delete b
]中,如果要删除的对象的静态类型与其动态类型不同,则静态类型应为要删除的对象的动态类型的基类,并且静态类型应具有虚拟析构函数或行为是不确定的。
在大多数实现中,将像处理任何非虚拟代码一样解决对析构函数的调用,这意味着将调用基类的析构函数,但不会调用派生类之一,从而导致资源泄漏。
综上所述,在需要对基类virtual
进行多态操作时,请始终使其成为析构函数。
如果要防止通过基类指针删除实例,则可以使基类的析构函数受保护且非虚拟;这样,编译器将不允许您调用delete
基类指针。
您可以从Herb Sutter的本文中了解有关虚拟性和虚拟基类析构函数的更多信息。
Base
并且Derived
具有所有自动存储变量怎么办?也就是说,在析构函数中没有“特殊”或附加的自定义代码要执行。那可以不用编写任何析构函数了吗?还是派生类仍然存在内存泄漏?
虚拟构造函数是不可能的,但虚拟析构函数是可能的。让我们尝试一下.......
#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项。迈尔斯继续总结,如果一个类有任何虚函数,它应该有一个虚析构函数,而不是类设计为基类或不是设计用于多态应不声明虚析构函数。
const Base& = make_Derived();
。在这种情况下,Derived
将调用prvalue 的析构函数,即使它不是虚拟的也是如此,因此可以节省vtables / vpointers引入的开销。当然范围很有限。Andrei Alexandrescu在他的《现代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
。
我喜欢考虑接口和接口的实现。在C ++中,接口是纯虚拟类。析构函数是接口的一部分,有望实现。因此,析构函数应该是纯虚拟的。构造函数呢?构造函数实际上不是接口的一部分,因为对象总是显式实例化的。
virtual
在基类中声明了析构函数(或任何函数),则virtual
即使未声明它也自动在派生类中。
当您希望通过基类指针删除对象时,不同的析构函数应遵循正确的顺序时,必须使用析构函数的虚拟关键字。例如:
Base *myObj = new Derived();
// Some code which is using myObj object
myObj->fun();
//Now delete the object
delete myObj ;
如果您的基类析构函数是虚拟的,则对象将按顺序被破坏(首先是派生的对象,然后是base)。如果您的基类析构函数不是虚拟的,则仅基类对象将被删除(因为指针是基类“ Base * myObj”的指针)。因此,派生对象将发生内存泄漏。
简单来说,当您删除指向派生类对象的基类指针时,虚拟析构函数将以适当的顺序破坏资源。
#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
基本指针会导致未定义的行为。
如果您使用 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;
}
虚拟析构函数将显式调用类的最派生的运行时析构函数,以便能够以适当的方式清除对象。
或访问链接
我认为这个问题的核心是关于虚拟方法和多态性,而不是专门的析构函数。这是一个更清晰的示例:
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
输入之前-具有不正确的意图- delete nullptr;
定义为无操作。如果有的话,您应该在调用之前进行检查->foo()
,否则,如果new
某种原因失败,则会发生未定义的行为。)
delete
一个NULL
指针(即你不需要if (a != NULL)
后卫)。
我认为,讨论通过基类(/ struct)删除而没有虚拟析构函数,或更确切地说没有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)有vtable并且您通过基类(/ struct)删除对象,则可能导致崩溃的原因指针。在这种情况下,您传递给堆的空闲例程的地址无效,从而导致崩溃。
如果运行上述代码,则在发生问题时您将清楚看到。当基类(/ struct)的this指针与派生类(/ struct)的this指针不同时,您将遇到此问题。在上面的示例中,结构a和b没有vtable。结构c和d确实具有vtable。因此,指向ac或d对象实例的a或b指针将被固定以说明vtable。如果传递此a或b指针删除,则该地址将崩溃,因为该地址对于堆的免费例程无效。
如果计划从基类指针中删除具有vtable的派生实例,则需要确保基类具有vtable。一种方法是添加一个虚拟析构函数,无论如何您都可能想要适当地清理资源。
一个基本的定义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;
}
任何公开继承的类(无论是否具有多态性)都应具有虚拟析构函数。换句话说,如果基类指针可以指向它,则其基类应具有虚拟析构函数。
如果是虚拟的,则调用派生类的析构函数,然后调用基类的构造函数。如果不是虚拟的,则仅基类析构函数被调用。