C++ コア ガイドライン:割り当てと解放のルール

ガイドラインには、明示的なメモリ割り当てと割り当て解除に関する 6 つのルールがあります。六!現代の C++ には単純なルールがあるため、驚かれるかもしれません:new と delete を使用しないでください。明らかに、話はそれほど単純ではありません。

ここに 6 つのルールがあります。

  • R.10:malloc() を避ける と free()
  • R.11:new の呼び出しを避ける と delete 明示的に
  • R.12:明示的なリソース割り当ての結果をすぐにマネージャー オブジェクトに与える
  • R.13:1 つの式ステートメントで最大 1 つの明示的なリソース割り当てを実行します
  • R.14:???配列 vs. ポインタ パラメータ
  • R.15:一致する割り当て/割り当て解除のペアを常にオーバーロードする

最後の 2 つのルールについては書きません。第一に、ルール R.14 は十分に焼き固められておらず、第二に、ルール R.15 は非常に特殊です。 new と delete のオーバーロードについて詳しく知りたい場合は、メモリの割り当てと解放に関する私の投稿を読んでください。

ルールについて詳しく説明する前に、ルールを理解するために必要な背景を少し説明します。 new を使用して C++ でオブジェクトを作成するには、2 つの手順があります。

<オール>
  • オブジェクトにメモリを割り当てる
  • オブジェクトを割り当てられたメモリに構築します
  • operator new または operator new [] が最初のステップになります。コンストラクターの 2 番目のステップ。

    同じ戦略が破壊にも適用されますが、その逆です。最初にデストラクタが呼び出され (存在する場合)、メモリは operator delete または operator delete [] で割り当て解除されます。この 2 段階の作成と破棄が、4 つのルールの理由です。それでは始めましょう。

    R.10:malloc() を避ける および free()

    new と malloc、delete と free の違いは何ですか? C 関数 malloc と free は、仕事の半分しか行いません。 malloc はメモリを割り当て、free はメモリの割り当てを解除するだけです。 malloc はコンストラクタを呼び出しませんし、free はデストラクタを呼び出しません。

    つまり、作成したばかりのオブジェクトを使用する場合 malloc を使用すると、未定義の動作が発生します。

    // mallocVersusNew.cpp
    
    #include <iostream>
    #include <string>
    
    struct Record{
     Record(std::string na = "Record"): name(na){} // (4)
     std::string name;
    };
    
    int main(){
     
     std::cout << std::endl;
     
     Record* p1 = static_cast<Record*>(malloc(sizeof(Record))); // (1)
     std::cout << p1->name << std::endl; // (3)
    
     auto p2 = new Record; // (2)
     std::cout << p2->name << std::endl; 
     
     std::cout << std::endl;
     
    }
    

    (1) Record オブジェクトのメモリのみを割り当てます。その結果、(3) の出力 p1->name は未定義の動作になります。対照的に、呼び出し (2) は、行 (4) でコンストラクターを呼び出します。未定義の動作とは、プログラムの出力について何も仮定できないことを意味します。

    使用するプラットフォームと使用する GCC によって、プログラムの結果はまったく異なります。

    • GCC 4.8.5 がローカル PC でコア ダンプを生成する

    • GCC 4.9 (cppreference.com) は出力を生成しません

    • GCC 7.1 (cppreference.com) は 期待される を生成します 出力

    R.11:new の呼び出しを避ける と delete 明示的に

    このルールを覚えておく必要があります。このルールで強調されているのは、明示的にという言葉です。 標準テンプレート ライブラリのスマート ポインタまたはコンテナを使用すると、暗黙的にを使用するオブジェクトが得られるためです。 新規および削除。

    R.12:マネージャー オブジェクトへの明示的なリソース割り当て

    これは std::unique_ptr upInt(new int()) などのスマート ポインターの重要なアイデアであり、ガイドラインの反例には当てはまりません。バッファの割り当てに失敗すると、ファイル ハンドルが失われます。

    void f(const std::string& name)
    {
     FILE* f = fopen(name, "r"); // open the file
     std::vector<char> buf(1024);
     fclose(f); // close the file
    }
    

    R.13:最大 1 つの明示的なリソース割り当てを実行します単一の式ステートメント

    このルールは少しトリッキーです。

    void func(std::shared_ptr<Widget> sp1, std::shared_ptr<Widget> sp2){
     ...
    }
    
    func(std::shared_ptr<Widget>(new Widget(1)), std::shared_ptr<Widget>(new Widget(2)));
    

    この関数呼び出しは例外セーフではないため、メモリ リークが発生する可能性があります。なんで?その理由は、共有ポインターを初期化するために 4 つの操作を実行する必要があるためです。

    <オール>
  • Widget(1) にメモリを割り当てる
  • ウィジェットの作成(1)
  • Widget(2) にメモリを割り当てる
  • ウィジェットの作成(2)
  • コンパイラは、最初に Widget(1) と Widget(2) に自由にメモリを割り当ててから、両方を構築できます。

    <オール>
  • Widget(1) にメモリを割り当てる
  • Widget(2) にメモリを割り当てる
  • ウィジェットの作成(1)
  • ウィジェットの作成(2)
  • コンストラクターの 1 つが例外をスローすると、他のオブジェクトのメモリは自動的に解放されず、メモリ リークが発生します。

    std::shared_ptr を作成するためのファクトリ関数 std::make_shared を使用することで、この問題を簡単に克服できます。

    func(std::make_shared<Widget>(1), std::make_shared<Widget>(2));
    

    std::make_shared は、例外がスローされた場合に関数が影響を与えないことを保証します。 std::unique_ptr を作成するためのペンダント関数 std::make_unique は、同じことを保証します。

    次は?

    リソース管理の次のルールはルール R.11 に従います。明示的に new と delete を呼び出すことは避けてください。したがって、次の投稿では、スマート ポインター std::unique_ptr、std::shared_ptr、および std::weak_ptr について説明します。