私はほとんどのOO理論をしっかりと理解していますが、私をとても混乱させているのは仮想デストラクタです。
チェーン内のすべてのオブジェクトに対して、デストラクタは常に呼び出されると思いました。
いつ仮想化するつもりなのか、そしてその理由は?
仮想デストラクタは、基本クラスへのポインタを通じて派生クラスのインスタンスを削除する可能性がある場合に便利です。
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
はDerived
オブジェクトを指すBase*
なので、delete b
は 未定義の動作 を持ちます。
[
delete b
]において、削除されるオブジェクトの静的型がその動的型と異なる場合、静的型は削除されるオブジェクトの動的型の基本クラスとなり、静的型は仮想型となります)デストラクタまたは動作は未定義です。
ほとんどの実装では、デストラクタの呼び出しは非仮想コードと同じように解決されます。つまり、基本クラスのデストラクタは呼び出されますが派生クラスのデストラクタは呼び出されず、リソースリークが発生します。
まとめると、基底クラスのデストラクタは多相的に操作されることを意図している場合は常にvirtual
にしてください。
基本クラスのポインタを介してインスタンスが削除されないようにしたい場合は、基本クラスのデストラクタを保護され非仮想にすることができます。そうすることによって、コンパイラは基本クラスポインタでdelete
を呼び出せません。
仮想性と仮想基本クラスのデストラクタの詳細については、 Herb Sutterによるこの記事 を参照してください。
仮想コンストラクタは不可能ですが、仮想デストラクタは可能です。試してみましょう....
#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"ポインタ(ベースポインタ)を削除すると、ベースデストラクタだけがcallであることがわかりました。適切なことをするには、基本デストラクタを仮想にする必要があります。では、次のようになります。
#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++ のItem 7です。 Meyersは続けて、クラスが any 仮想関数を持つ場合、それは仮想デストラクタを持つべきであり、基底クラスになるように設計されていないまたは多態的に使用されるように設計されていないクラスは not 仮想デストラクタを宣言します。
仮想デストラクタがないときに基本クラスのポインタを削除すると、未定義の動作になることにも注意してください。私がつい最近学んだこと
私は何年も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.
}
私はインターフェースとインターフェースの実装について考えるのが好きです。 C++では、話すインタフェースは純粋な仮想クラスです。デストラクタはインタフェースの一部であり、実装が期待されています。したがって、デストラクタは純粋に仮想的でなければなりません。コンストラクタはどうですか?オブジェクトは常に明示的にインスタンス化されるため、コンストラクタは実際にはインタフェースの一部ではありません。
簡単に言うと、Virtual destructorは、派生クラスオブジェクトを指す基本クラスポインタを削除したときに、リソースを正しい順序で破壊することです。
#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
オブジェクトが基本クラスのポインタを介して削除されている間に異なるデストラクタが正しい順序に従うようにするには、デストラクタ用の仮想キーワードが必要です。例えば:
Base *myObj = new Derived();
// Some code which is using myObj object
myObj->fun();
//Now delete the object
delete myObj ;
派生クラスのデストラクタが仮想の場合、オブジェクトは順番に破壊されます(最初に派生オブジェクト、次に基本)。派生クラスのデストラクタが仮想でない場合は、基本クラスのオブジェクトのみが削除されます(ポインタは基本クラス "Base * myObj"のためです)。だから派生オブジェクトのメモリリークがあるでしょう。
仮想ベースクラスのデストラクタは「ベストプラクティス」です - メモリリークを(検出するのが難しい)回避するためにあなたはいつもそれらを使うべきです。それらを使用して、あなたはあなたのクラスの継承チェーンの中のすべてのデストラクタが(正しい順序で)呼ばれていることを確認することができます。仮想デストラクタを使用して基本クラスから継承すると、継承クラスのデストラクタも自動的に仮想になります(したがって、継承クラスのデストラクタ宣言で「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;
}
仮想デストラクタは、クラスの最も派生したランタイムデストラクタを明示的に呼び出します。これにより、オブジェクトを適切な方法で消去できるようになります。
またはリンクをご覧ください
"未定義"の振る舞い、または仮想デストラクタなしで基本クラス(/ struct)を介して削除するときに発生する可能性がある "クラッシュ"の未定義の振る舞いについて議論することは有益であると思いました。以下のコードはいくつかの単純な構造体をリストしています(クラスについても同じことが言えます)。
#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)があり、基本クラス(/ struct)を介してオブジェクトを削除すると、クラッシュする可能性がある理由を指摘しているだけです。ポインタこの場合、ヒープの空きルーチンに渡すアドレスが無効であるため、クラッシュの原因となります。
上記のコードを実行すると、問題がいつ発生するかがはっきりわかります。基本クラスのthisポインタ(/ struct)が派生クラスのthisポインタ(/ struct)と異なる場合、この問題に遭遇することになります。上記のサンプルでは、構造体aとbにはvtableがありません。構造体cとdにはvtableがあります。したがって、cまたはdオブジェクトインスタンスへのaまたはbポインタは、vtableを考慮して固定されます。削除するためにこのaまたはbポインタを渡すと、アドレスがヒープの空きルーチンに対して無効であるためにクラッシュします。
基本クラスのポインタからvtableを持つ派生インスタンスを削除する予定の場合は、基本クラスにvtableがあることを確認する必要があります。そのための1つの方法は、仮想デストラクタを追加することです。仮想デストラクタは、とにかくリソースを適切にクリーンアップするために必要な場合があります。
この質問の中心は、仮想メソッドとポリモーフィズムについてであり、デストラクタではありません。これがより明確な例です。
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.
そして今、あなたはいつ仮想デストラクタを使うべきか理解するべきです。
shared_ptr
(unique_ptrではなくshared_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
基本クラスから派生クラスのデストラクタを呼び出す必要があるとき。仮想基底クラスデストラクタを基底クラスで宣言する必要があります。