web-dev-qa-db-ja.com

「新しい」を使用するとメモリリークが発生するのはなぜですか?

私は最初にC#を学び、今ではC++から始めています。私が理解しているように、C++の演算子newはC#の演算子と似ていません。

このサンプルコードでメモリリークの理由を説明できますか?

class A { ... };
struct B { ... };

A *object1 = new A();
B object2 = *(new B());
129
user1131997

何が起こっているのか

_T t;_と書くと、自動ストレージ期間T型のオブジェクトを作成します。範囲外になると、自動的にクリーンアップされます。

new T()と書くと、dynamic storage durationT型のオブジェクトを作成しています。自動的にクリーンアップされません。

new without cleanup

クリーンアップするために、deleteにポインターを渡す必要があります。

newing with delete

ただし、2番目の例はさらに悪い例です。ポインタを逆参照し、オブジェクトのコピーを作成しています。この方法では、newで作成されたオブジェクトへのポインターが失われるため、必要な場合でも削除できません。

newing with deref

あなたがすべきこと

自動保存期間を優先する必要があります。新しいオブジェクトが必要です。書くだけです:

_A a; // a new object of type A
B b; // a new object of type B
_

動的な保存期間が必要な場合は、割り当てられたオブジェクトへのポインタを、それを自動的に削除する自動保存期間オブジェクトに保存します。

_template <typename T>
class automatic_pointer {
public:
    automatic_pointer(T* pointer) : pointer(pointer) {}

    // destructor: gets called upon cleanup
    // in this case, we want to use delete
    ~automatic_pointer() { delete pointer; }

    // emulate pointers!
    // with this we can write *p
    T& operator*() const { return *pointer; }
    // and with this we can write p->f()
    T* operator->() const { return pointer; }

private:
    T* pointer;

    // for this example, I'll just forbid copies
    // a smarter class could deal with this some other way
    automatic_pointer(automatic_pointer const&);
    automatic_pointer& operator=(automatic_pointer const&);
};

automatic_pointer<A> a(new A()); // acts like a pointer, but deletes automatically
automatic_pointer<B> b(new B()); // acts like a pointer, but deletes automatically
_

newing with automatic_pointer

これはあまり説明的ではない名前RAII(Resource Acquisition Is Initialization)で通じる一般的なイディオムです。クリーンアップが必要なリソースを取得すると、自動ストレージ期間のオブジェクトに固定するため、クリーンアップを心配する必要はありません。これは、メモリ、開いているファイル、ネットワーク接続など、あらゆるリソースに適用されます。

この_automatic_pointer_のことはすでにさまざまな形式で存在しているので、例を挙げて説明しました。 _std::unique_ptr_と呼ばれる非常に類似したクラスが標準ライブラリに存在します。

_auto_ptr_という名前の古いもの(C++ 11以前)もありますが、奇妙なコピー動作があるため、現在では非推奨になっています。

さらに、_std::shared_ptr_のような、よりスマートな例もあります。これは、同じオブジェクトへの複数のポインターを許可し、最後のポインターが破棄されたときにのみクリーンアップします。

462

ステップごとの説明:

_// creates a new object on the heap:
new B()
// dereferences the object
*(new B())
// calls the copy constructor of B on the object
B object2 = *(new B());
_

そのため、これまでにヒープへのポインタのないオブジェクトがヒープ上にあるため、削除することはできません。

他のサンプル:

_A *object1 = new A();
_

割り当てられたメモリをdeleteするのを忘れた場合のみ、メモリリークになります。

_delete object1;
_

C++には、自動ストレージを持つオブジェクト、スタックで作成されたオブジェクト、自動的に破棄されるヒープ、動的ストレージを持つオブジェクトがヒープ上にあり、newで割り当て、deleteで解放する必要があります。 (これはすべて大まかに言えば)

deleteで割り当てられたすべてのオブジェクトに対してnewが必要だと思います。

[〜#〜] edit [〜#〜]

考えてみると、_object2_はメモリリークである必要はありません。

次のコードはポイントを示すためのものです。これは悪い考えです。このようなコードは好きではありません:

_class B
{
public:
    B() {};   //default constructor
    B(const B& other) //copy constructor, this will be called
                      //on the line B object2 = *(new B())
    {
        delete &other;
    }
}
_

この場合、otherは参照によって渡されるため、new B()が指す正確なオブジェクトになります。したがって、_&other_でアドレスを取得し、ポインターを削除すると、メモリーが解放されます。

しかし、私はこれを十分に強調することはできません、これをしないでください。重要なのはここだけです。

34
Luchian Grigore

2つの「オブジェクト」がある場合:

obj a;
obj b;

メモリ内の同じ場所を占有しません。つまり、&a != &b

一方の値をもう一方に割り当てても、それらの場所は変わりませんが、その内容は変わります。

obj a;
obj b = a;
//a == b, but &a != &b

直感的に、ポインターの「オブジェクト」は同じように機能します。

obj *a;
obj *b = a;
//a == b, but &a != &b

それでは、例を見てみましょう。

A *object1 = new A();

これは、new A()の値をobject1に割り当てています。値はobject1 == new A()を意味するポインターですが、&object1 != &(new A())です。 (この例は有効なコードではなく、説明のためだけのものです)

ポインターの値が保持されるため、ポインターが指すメモリを解放できます。delete object1;ルールにより、これはリークのないdelete (new A());と同じ動作をします。


2番目の例では、ポイント先のオブジェクトをコピーしています。値はそのオブジェクトの内容であり、実際のポインターではありません。他のすべての場合と同様に、&object2 != &*(new A())

B object2 = *(new B());

割り当てられたメモリへのポインタが失われたため、解放できません。 delete &object2;は動作するように見えるかもしれませんが、&object2 != &*(new A())であるためdelete (new A())と同等ではないため無効です。

11
Pubby
B object2 = *(new B());

この行がリークの原因です。これを少し分けてみましょう。

object2はタイプ1の変数で、たとえばアドレス1に保存されています(はい、ここで任意の数字を選択しています)。右側で、新しいBまたはタイプBのオブジェクトへのポインターを要求しました。プログラムはこれを喜んで提供し、新しいBをアドレス2に割り当て、またアドレス3にポインターを作成します。アドレス2のデータにアクセスする唯一の方法は、アドレス3のポインターを使用することです。次に、*を使用してポインターを逆参照して、ポインターが指すデータ(アドレス2のデータ)を取得します。これにより、そのデータのコピーが効率的に作成され、アドレス1に割り当てられたobject2に割り当てられます。元のファイルではなく、コピーであることに注意してください。

さて、ここに問題があります:

そのポインターを使用できる場所に実際に保存したことはありません!この割り当てが完了すると、ポインター(address2にアクセスするために使用したaddress3のメモリー)は範囲外になり、手の届かないところにあります!これでdeleteを呼び出すことはできないため、address2のメモリをクリーンアップできません。残っているのは、address1のaddress2からのデータのコピーです。記憶にある同じもののうちの2つ。 1つはアクセスでき、もう1つはアクセスできません(パスを失ったため)。これがメモリリークである理由です。

C#の背景から、C++のポインターがどのように機能するかについて多くのことを読むことをお勧めします。これらは高度なトピックであり、把握するのに時間がかかる場合がありますが、その使用は非常に貴重です。

9
MGZero

C#およびJavaでは、newを使用して任意のクラスのインスタンスを作成し、後で破棄することを心配する必要はありません。

C++には、オブジェクトを作成するキーワード「new」もありますが、JavaまたはC#とは異なり、オブジェクトを作成する唯一の方法ではありません。

C++には、オブジェクトを作成する2つのメカニズムがあります。

  • 自動
  • 動的

自動作成では、スコープ環境でオブジェクトを作成します。-関数内または-クラス(または構造体)のメンバーとして。

関数では、次のように作成します。

int func()
{
   A a;
   B b( 1, 2 );
}

クラス内では、通常、次の方法で作成します。

class A
{
  B b;
public:
  A();
};    

A::A() :
 b( 1, 2 )
{
}

最初の場合、オブジェクトはスコープブロックが終了すると自動的に破棄されます。これは、関数または関数内のスコープブロックです。

後者の場合、オブジェクトbは、メンバーであるAのインスタンスとともに破棄されます。

オブジェクトの有効期間を制御する必要がある場合、オブジェクトにはnewが割り当てられます。オブジェクトを破棄するには、削除が必要です。 RAIIと呼ばれる手法を使用すると、オブジェクトを作成した時点でオブジェクトを自動オブジェクト内に配置して削除し、その自動オブジェクトのデストラクタが有効になるのを待ちます。

そのようなオブジェクトの1つにshared_ptrがあります。これは、「deleter」ロジックを呼び出しますが、オブジェクトを共有しているshared_ptrのすべてのインスタンスが破棄された場合のみです。

一般的に、コードにはnewの呼び出しが多数ある場合がありますが、deleteの呼び出しは制限する必要があり、これらはスマートポインターに配置されるデストラクタまたは「deleter」オブジェクトから必ず呼び出されるようにする必要があります。

デストラクタも例外をスローしないでください。

これを行うと、メモリリークがほとんどなくなります。

9
CashCow

それが簡単になったら、コンピューターのメモリをホテルのようなものと考え、プログラムは必要なときに部屋を借りる顧客です。

このホテルが機能する方法は、部屋を予約し、出発するときにポーターに伝えることです。

あなたがプログラムして部屋を予約し、ポーターに告げずに部屋を出ると、ポーターはその部屋がまだ使用中であると考え、他の人がそれを使用できないようにします。この場合、部屋の漏れがあります。

プログラムがメモリを割り当てて削除しない場合(単に使用を停止する場合)、コンピュータはメモリがまだ使用中であるとみなし、他のユーザーがそれを使用できないようにします。これはメモリリークです。

これは正確なアナロジーではありませんが、役に立つかもしれません。

8
Stefan

object2を作成すると、newで作成したオブジェクトのコピーが作成されますが、(割り当てられていない)ポインターも失われます(したがって、後で削除する方法はありません)。これを回避するには、object2を参照する必要があります。

7
Mario

すぐに漏れているのはこの行です:

B object2 = *(new B());

ここでは、ヒープ上に新しいBオブジェクトを作成してから、スタック上にコピーを作成しています。ヒープに割り当てられたものにはアクセスできなくなるため、リークが発生します。

この行はすぐには漏れません:

A *object1 = new A();

deleted object1しかし。

7
mattjgalloway

new演算子を使用して割り当てたメモリを何らかの時点で解放しないと、そのメモリへのポインタをdelete演算子に渡すことでメモリリークが発生します。

上記の2つの場合:

_A *object1 = new A();
_

ここでは、メモリを解放するためにdeleteを使用していないため、_object1_ポインタが範囲外になった場合、ポインタを失い、メモリリークが発生します。そのため、delete演算子を使用できません。

そしてここ

_B object2 = *(new B());
_

new B()によって返されたポインターを破棄しているため、そのポインターをdeleteに渡してメモリを解放することはできません。したがって、別のメモリリーク。

7
razlebe