'new'を使用するとメモリリークが発生するのはなぜですか?


131

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

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

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

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

回答:


464

何が起こっている

書き込むT t;ときはT自動ストレージ期間を持つタイプのオブジェクトを作成しています。範囲外になると、自動的にクリーンアップされます。

書き込むnew T()ときはT動的ストレージ期間を持つタイプのオブジェクトを作成しています。自動的にクリーンアップされません。

クリーンアップなしの新しい

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

削除による新規作成

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

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

automatic_pointerによる新規作成

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

このautomatic_pointerことはすでにさまざまな形で存在していますが、例を示すために提供しました。と呼ばれる非常に類似したクラスが標準ライブラリに存在しstd::unique_ptrます。

古い(C ++ 11より前の)名前の付いauto_ptrたものもありますが、奇妙なコピー動作があるため、非推奨になりました。

そしてstd::shared_ptr、同じオブジェクトへの複数のポインタを許可し、最後のポインタが破棄されたときにのみそれをクリーンアップするような、よりスマートな例がいくつかあります。


4
@ user1131997:別の質問をしてくれてうれしいです。ご覧のとおり、コメントで説明するのはそれほど簡単ではありません:)
R. Martinho Fernandes

@ R.MartinhoFernandes:すばらしい答え。一つだけ質問。operator *()関数で参照による戻りを使用したのはなぜですか?
デストラクタ

@Destructorの遅い返信:D。参照で戻ると、指示先を変更できるため*p += 2、通常のポインタの場合と同じようにを実行できます。参照によって返されない場合は、通常のポインターの動作を模倣しません。これは、ここでの意図です。
R.マルティーニョフェルナンデス

「割り当てられたオブジェクトへのポインタを、それを自動的に削除する自動保存期間オブジェクトに保存する」ようにアドバイスしてくれてありがとう。C ++をコンパイルする前にこのパターンを学習するようにプログラマーに要求する方法があった場合のみ!
アンディ

35

ステップバイステップの説明:

// 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

編集

考えて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てポインタを削除すると、メモリが解放されます。

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


2
私は同じことを考えていました:リークしないようにハッキングできますが、あなたはそれをしたくないでしょう。object1もリークする必要はありません。そのコンストラクターは、ある時点でそれを削除するある種のデータ構造にそれ自体をアタッチできるからです。
CashCow 2012年

2
「これを行うことは可能であるが、できない」という回答を書くのは、たいへんとても魅力的です。:-)私は気持ちを知っています
コス

11

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())ので無効です。


9

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つは、「deleter」ロジックを呼び出すshared_ptrですが、オブジェクトを共有しているshared_ptrのすべてのインスタンスが破棄された場合のみです。

一般に、コードにはnewへの多くの呼び出しがある可能性がありますが、deleteへの呼び出しは制限する必要があり、これらがスマートポインターに入れられるデストラクターまたは「deleter」オブジェクトから呼び出されることを常に確認する必要があります。

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

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


4
automaticおよび以外にもありdynamicます。もありstaticます。
Mooing Duck

9
B object2 = *(new B());

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

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

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

実際にそのポインターを使用可能な場所に保存したことはありません。この割り当てが完了すると、ポインタ(address2へのアクセスに使用したaddress3のメモリ)はスコープ外になり、到達できなくなります。削除を呼び出すことができなくなったため、address2のメモリをクリーンアップできません。残っているのは、address1のaddress2からのデータのコピーです。記憶にある同じものの2つ。1つはアクセスでき、もう1つはアクセスできません(パスを失ったため)。これがメモリリークの原因です。

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


8

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

このホテルの仕組みは、部屋を予約し、出発するときにポーターに伝えることです。

プログラムして部屋を予約し、ポーターに知らせずに退出すると、ポーターは部屋がまだ使用中であると見なし、他の誰にもそれを使用させません。この場合、部屋に漏れがあります。

プログラムがメモリを割り当て、メモリを削除しない場合(使用を停止するだけ)、コンピュータはメモリがまだ使用中であると見なし、他の人がメモリを使用することを許可しません。これはメモリリークです。

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


5
私はその類推をとても気に入っていますが、完璧ではありませんが、これは、初めての人にメモリリークを説明する良い方法です。
AdamM

1
私はこれをロンドンのブルームバーグの上級エンジニアのインタビューで使用して、HRガールにメモリリークを説明しました。プログラマー以外の人に、メモリリーク(およびスレッドの問題)を彼女が理解した方法で実際に説明することができたので、私はそのインタビューを通り抜けました。
Stefan

7

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


3
オブジェクトを削除するために参照のアドレスを取得することは信じられないほど悪い習慣です。スマートポインターを使用します。
トムウィトック

3
信じられないほど悪い習慣ですね スマートポインターは裏で何を使用していると思いますか?
ブリンディ

3
@Blindyスマートポインター(少なくとも適切に実装されたポインター)は、直接ポインターを使用します。
Luchian Grigore

2
まあ、完全に正直に言うと、アイデア全体はそれほど素晴らしいものではありませんよね。実際、OPで試行されたパターンが実際にどこで役立つかはわかりません。
Mario

7

まあ、メモリnewポインターをオペレーターに渡して、オペレーターを使用して割り当てたメモリを解放しないと、メモリリークが発生しますdelete

上記の2つの場合:

A *object1 = new A();

ここでdeleteはメモリを解放するために使用していないため、object1ポインタがスコープから外れると、メモリリークが発生します。ポインタを失い、そのdelete上で演算子を使用できないためです。

そしてここ

B object2 = *(new B());

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


7

すぐにリークしているのは次の行です。

B object2 = *(new B());

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

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

A *object1 = new A();

あなたが決してdeletedをしなければリークがありobject1ます。


4
動的/自動ストレージを説明する場合は、ヒープ/スタックを使用しないでください。
Pubby

2
@Pubbyなぜ使用しないのですか?動的/自動ストレージのため、スタックではなく常にヒープですか?そして、それがスタック/ヒープについて詳細に説明する必要がない理由です、そうですか?

4
@ user1131997ヒープ/スタックは実装の詳細です。知っておくことが重要ですが、この質問には関係ありません。
Pubby

2
うーん、私はそれに対する別の答えを求めています。つまり、私のものと同じですが、ヒープ/スタックをあなたが最もよく考えるものに置き換えます。あなたがそれをどのように説明したいのか知りたいです。
mattjgalloway 2012年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.