web-dev-qa-db-ja.com

queue :: push後の二重解放または破損

#include <queue>
using namespace std;

class Test{
    int *myArray;

        public:
    Test(){
        myArray = new int[10];
    }

    ~Test(){
        delete[] myArray;
    }

};


int main(){
    queue<Test> q
    Test t;
    q.Push(t);
}

これを実行した後、ランタイムエラー「ダブルフリーまたは破損」が発生します。デストラクタのコンテンツ(delete)を取り除くと、うまく機能します。どうしましたか?

49
Mihai Neacsu

C++でのオブジェクトのコピーについて話しましょう。

Test t;は、整数の新しい配列を割り当てるデフォルトのコンストラクターを呼び出します。これは正常であり、期待される動作です。

q.Push(t)を使用してtをキューにプッシュすると問題が発生します。 Java、C#、または他のほぼすべてのオブジェクト指向言語に精通している場合、先ほど作成したオブジェクトがキューに追加されることを期待するかもしれませんが、C++はそのようには機能しません。

std::queue::Push method を見ると、キューに追加される要素が「xのコピーに初期化されている」ことがわかります。実際には、コピーコンストラクターを使用して元のTestオブジェクトのすべてのメンバーを複製し、新しいTestを作成するまったく新しいオブジェクトです。

C++コンパイラは、デフォルトでコピーコンストラクタを生成します!これは非常に便利ですが、ポインターメンバーで問題が発生します。この例では、int *myArrayは単なるメモリアドレスであることに注意してください。 myArrayの値が古いオブジェクトから新しいオブジェクトにコピーされると、メモリ内の同じ配列を指す2つのオブジェクトができます。これは本質的に悪いことではありませんが、デストラクタは同じ配列を2回削除しようとするため、「二重解放または破損」ランタイムエラーが発生します。

どうすれば修正できますか?

最初のステップは、copy constructorを実装することです。これにより、あるオブジェクトから別のオブジェクトにデータを安全にコピーできます。簡単にするために、次のようになります。

Test(const Test& other){
    myArray = new int[10];
    memcpy( myArray, other.myArray, 10 );
}

これで、Testオブジェクトをコピーするときに、新しいオブジェクトに新しい配列が割り当てられ、配列の値もコピーされます。

ただし、まだ完全に問題が解決しているわけではありません。同様の問題を引き起こす可能性のあるコンパイラーが生成する別の方法があります-割り当て。違いは、割り当てでは、メモリを適切に管理する必要がある既存のオブジェクトが既に存在することです。基本的な代入演算子の実装は次のとおりです。

Test& operator= (const Test& other){
    if (this != &other) {
        memcpy( myArray, other.myArray, 10 );
    }
    return *this;
}

ここで重要なのは、他の配列からこのオブジェクトの配列にデータをコピーし、各オブジェクトのメモリを分離したままにすることです。自己割り当てのチェックもあります。そうしないと、私たちは自分自身から自分自身にコピーすることになり、エラーがスローされる可能性があります(何をすべきかわからない)。より多くのメモリを削除して割り当てる場合、自己割り当てチェックにより、コピー元のメモリを削除できなくなります。

84
derekerdmann

問題は、クラスにマネージRAWポインターが含まれているが、3つの規則(C++ 11では5つ)が実装されていないことです。その結果、コピーのために(予想どおり)二重削除が発生します。

学習している場合、 (5)のルールを実装する の方法を学習する必要があります。しかし、それはこの問題の正しい解決策ではありません。独自の内部コンテナを管理しようとするのではなく、標準のコンテナオブジェクトを使用する必要があります。正確なコンテナは、何をしようとしているかによって異なりますが、std :: vectorが適切なデフォルトです(また、最適でない場合は後書きを変更できます)。

#include <queue>
#include <vector>

class Test{
    std::vector<int> myArray;

    public:
    Test(): myArray(10){
    }    
};

int main(){
    queue<Test> q
    Test t;
    q.Push(t);
}

標準コンテナを使用する必要がある理由は、separation of concernsです。クラスは、ビジネスロジックまたはリソース管理(両方ではなく)のいずれかを考慮する必要があります。 Testがプログラムに関する何らかの状態を維持するために使用しているクラスであると仮定すると、それはビジネスロジックであり、リソース管理を行うべきではありません。一方、Testが配列を管理することになっている場合は、おそらく標準ライブラリ内で利用可能なものについてさらに学習する必要があります。

15
Martin York

最初のデストラクタはオブジェクト用ですqこの場合、newによって割り当てられたメモリは解放されるため、二重解放または破損になります。オブジェクトに対してdetructorが呼び出されますtその時点でメモリはすでに解放されています(qに対して行われます)。したがって、デストラクタではdelete [] myArray;が実行され、スローされます- 二重解放または破損。理由は、両方のオブジェクトが同じメモリを共有しているため、上記の回答で述べたように、\ copy、代入、および等価演算子を定義するからです。

4
Astro - Amit

コピーコンストラクタ、割り当て、演算子を定義する必要があります。

class Test {
   Test(const Test &that); //Copy constructor
   Test& operator= (const Test &rhs); //assignment operator
}

キューにプッシュされるコピーは、元のメモリと同じメモリを指します。最初のものが破壊されると、メモリが削除されます。 2番目は同じメモリを破棄して削除しようとします。

3
Ryan Guthrie

また、削除する前にnullをチェックしてみてください

if(myArray) { delete[] myArray; myArray = NULL; }

または、次のような安全な方法ですべての削除操作を定義できます。

#ifndef SAFE_DELETE
#define SAFE_DELETE(p) { if(p) { delete (p); (p) = NULL; } }
#endif

#ifndef SAFE_DELETE_ARRAY
#define SAFE_DELETE_ARRAY(p) { if(p) { delete[] (p); (p) = NULL; } }
#endif

そして使用する

SAFE_DELETE_ARRAY(myArray);
0
tolgayilmaz