web-dev-qa-db-ja.com

デストラクタを定義する必要があるのはいつですか?

ポインターメンバーがある場合、および基本クラスを定義する場合、デストラクタを定義する必要があると読みましたが、完全に理解しているかどうかはわかりません。既定のコンストラクターが常に既定で提供されるため、既定のコンストラクターを定義しても意味がないかどうかはわかりません。また、RAII原則を実装するためにデフォルトのコンストラクターを定義する必要があるかどうかもわかりません(デストラクターを定義せずに、コンストラクターにリソース割り当てを配置するだけでよいのでしょうか?)。

class A
{

public:
    ~Account()
    {
        delete [] brandname;
        delete b;

        //do we need to define it?

    };

    something(){} =0; //virtual function (reason #1: base class)

private:
    char *brandname; //c-style string, which is a pointer member (reason #2: has a pointer member)
    B* b; //instance of class B, which is a pointer member (reason #2)
    vector<B*> vec; //what about this?



}

class B: public A
{
    public something()
    {
    cout << "nothing" << endl;
    }

    //in all other cases we don't need to define the destructor, nor declare it?
}
22
user3435009

3つのルールとゼロのルール

リソースを処理する昔ながらの方法は、 Rule of Three (現在はMove SemanticによるRule of Five)でしたが、最近別のルールが引き継いでいます: ゼロの規則

アイデアは、実際に記事を読む必要がありますが、リソース管理は他の特定のクラスに任せるべきです。

この点に関して、標準ライブラリには、std::vectorstd::stringstd::unique_ptr、およびstd::shared_ptrのような素敵なツールセットが用意されており、カスタムデストラクター、移動/コピーコンストラクター、移動/コピー割り当て、デフォルトコンストラクターの必要性を効果的に削除します。

コードに適用する方法

コードには多くの異なるリソースがあり、これは素晴らしい例になります。

文字列

brandnameが事実上「動的な文字列」であることに気付いた場合、標準ライブラリはCスタイルの文字列を保存するだけでなく、 std::string

動的に割り当てられたB

2番目のリソースは、動的に割り当てられたBのようです。 「オプションのメンバーが必要」以外の理由で動的に割り当てる場合は、リソースを自動的に処理する(適切な場合に割り当てを解除する) std::unique_ptr を必ず使用する必要があります。一方、オプションのメンバーにしたい場合は、代わりに std::optional を使用できます。

Bsのコレクション

最後のリソースは、単にBsの配列です。これは std::vector で簡単に管理できます。標準ライブラリを使用すると、さまざまなニーズに合わせてさまざまなコンテナから選択できます。それらのいくつかに言及するだけです: std::dequestd::list および std::array .

結論

すべての提案を追加するには、次のようになります。

class A {
private:
    std::string brandname;
    std::unique_ptr<B> b;
    std::vector<B> vec;
public:
    virtual void something(){} = 0;
};

これは安全で読みやすいものです。

27
Shoe

@nonsensickleが指摘しているように、質問は広すぎます...だから私は知っていることすべてに取り組むようにしようとしています...

デストラクタを再定義する最初の理由は The Rule of Three にあり、これはScott Meyers Effective C++のitem 6ですが、完全ではありません。 3つのルールは、デストラクタ、コピーコンストラクタ、またはコピー割り当て操作を再定義する場合、3つすべてを書き換える必要があることを意味します。その理由は、自分のバージョンを1つに書き換えなければならない場合、コンパイラのデフォルトは残りのバージョンでは無効になるためです。

別の例は Scott Meyers in Effective C++ によって指摘されたものです。

基本クラスポインターを介して派生クラスオブジェクトを削除しようとして、基本クラスに非仮想デストラクタがある場合、結果は未定義です。

そして彼は続けます

クラスに仮想関数が含まれていない場合、それは多くの場合、基本クラスとして使用することを意図していないことを示しています。クラスが基本クラスとして使用されることを意図していない場合、デストラクタを仮想化することは通常悪い考えです。

仮想のデストラクタに関する彼の結論は

肝心なのは、すべてのデストラクタを仮想的に宣言することは、仮想的に宣言しないことと同じくらい間違っているということです。実際、多くの人々はこの状況をこのように要約します。クラスに少なくとも1つの仮想関数が含まれる場合にのみ、クラスで仮想デストラクタを宣言します。

そして、もしそれが3つのケースのルールでなければ、多分あなたはあなたのオブジェクトの中にポインタメンバを持ち、多分あなたはあなたのオブジェクトの中にメモリを割り当てた、そしてあなたはそのメモリを管理する必要があるデストラクタでは、これは彼の本の項目6です

ゼロの規則に関する@Jefffreyの回答を必ずチェックしてください。

11
Claudiordgz

デストラクタを定義する必要があるのは、正確に次の2つです。

  1. オブジェクトが破壊されると、すべてのクラスメンバーを破壊する以外のアクションを実行する必要があります。

    これらのアクションの大部分はかつてメモリを解放していましたが、RAIIの原則により、これらのアクションはRAIIコンテナのデストラクタに移動し、コンパイラが呼び出しを処理します。しかし、これらのアクションは、ファイルを閉じる、ログにデータを書き込むなど、何でも可能です... RAIIの原則に厳密に従う場合は、これらの他のすべてのアクションに対してRAIIコンテナを記述し、RAIIコンテナのみにデストラクタが定義されるようにします。

  2. 基本クラスポインターを介してオブジェクトを破棄する必要がある場合。

    これを行う必要がある場合、must基本クラス内でデストラクタをvirtualに定義します。そうしないと、派生デストラクタは、定義されているかどうか、およびvirtualであるかどうかに関係なく、呼び出されません。以下に例を示します。

    _#include <iostream>
    
    class Foo {
        public:
            ~Foo() {
                std::cerr << "Foo::~Foo()\n";
            };
    };
    
    class Bar : public Foo {
        public:
            ~Bar() {
                std::cerr << "Bar::~Bar()\n";
            };
    };
    
    int main() {
        Foo* bar = new Bar();
        delete bar;
    }
    _

    このプログラムはFoo::~Foo()のみを出力し、Barのデストラクタは呼び出されません。警告またはエラーメッセージはありません。すべての結果を伴う、部分的にのみ破壊されたオブジェクト。そのため、この条件が発生したときに自分で見つけてください(または、定義する派生しないクラスにvirtual ~Foo() = default;を追加するように注意してください。

これら2つの条件のいずれも満たされない場合、デストラクタを定義する必要はありません。デフォルトのコンストラクタで十分です。


サンプルコードに:
メンバーが何かへのポインターである場合(ポインターまたは参照として)、コンパイラーは知りません...

  • ...このオブジェクトへの他のポインタがあるかどうか。

  • ...ポインタが1つのオブジェクトを指すのか、配列を指すのか。

したがって、コンパイラーは、ポインターが指すものを破壊するかどうか、または破壊する方法を推測できません。したがって、デフォルトのデストラクタは、ポインタの背後にあるものを決して破壊しません。

これは、brandnamebの両方に適用されます。その結果、自分で割り当てを解除する必要があるため、デストラクタが必要です。または、RAIIコンテナを使用できます(_std::string_、およびスマートポインターバリアント)。

この変数は_std::vector<>_withinオブジェクトを直接含むため、この推論はvecには適用されません。その結果、コンパイラはvecを破棄する必要があることを認識します。これにより、すべての要素が破棄されます(結局RAIIコンテナです)。

2
cmaster

デストラクタが提供されない場合、コンパイラはデストラクタを生成することを知っています。

つまり、プリミティブ型などの単純なクリーンアップを超えるものにはデストラクタが必要です。

多くの場合、建設中の動的な割り当てまたはリソースの取得には、クリーンアップフェーズがあります。たとえば、動的に割り当てられたメモリを削除する必要がある場合があります。

クラスがハードウェア要素を表す場合、要素をオフにするか、安全な状態にする必要があります。

コンテナは、すべての要素を削除する必要がある場合があります。

要約すると、クラスがリソースを取得するか、特殊なクリーンアップが必要な場合(決まった順序で言う)、デストラクタが必要です。

1
Thomas Matthews

メモリを動的に割り当て、オブジェクト自体が「終了」した場合にのみこのメモリの割り当てを解除する場合は、デストラクタが必要です。

オブジェクトは、次の2つの方法で「終了」できます。

  1. 静的に割り当てられた場合、(コンパイラによって)暗黙的に「終了」されます。
  2. 動的に割り当てられた場合、(deleteを呼び出すことにより)明示的に「終了」されます。

明示的に「終了」した場合ベースクラス型のポインタを使用、デストラクタはvirtualでなければなりません。

1
barak manos