私は最初にC#を学び、今ではC++から始めています。私が理解しているように、C++の演算子new
はC#の演算子と似ていません。
このサンプルコードでメモリリークの理由を説明できますか?
class A { ... };
struct B { ... };
A *object1 = new A();
B object2 = *(new B());
何が起こっているのか
_T t;
_と書くと、自動ストレージ期間でT
型のオブジェクトを作成します。範囲外になると、自動的にクリーンアップされます。
new T()
と書くと、dynamic storage durationでT
型のオブジェクトを作成しています。自動的にクリーンアップされません。
クリーンアップするために、delete
にポインターを渡す必要があります。
ただし、2番目の例はさらに悪い例です。ポインタを逆参照し、オブジェクトのコピーを作成しています。この方法では、new
で作成されたオブジェクトへのポインターが失われるため、必要な場合でも削除できません。
あなたがすべきこと
自動保存期間を優先する必要があります。新しいオブジェクトが必要です。書くだけです:
_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
_
これはあまり説明的ではない名前RAII(Resource Acquisition Is Initialization)で通じる一般的なイディオムです。クリーンアップが必要なリソースを取得すると、自動ストレージ期間のオブジェクトに固定するため、クリーンアップを心配する必要はありません。これは、メモリ、開いているファイル、ネットワーク接続など、あらゆるリソースに適用されます。
この_automatic_pointer
_のことはすでにさまざまな形式で存在しているので、例を挙げて説明しました。 _std::unique_ptr
_と呼ばれる非常に類似したクラスが標準ライブラリに存在します。
_auto_ptr
_という名前の古いもの(C++ 11以前)もありますが、奇妙なコピー動作があるため、現在では非推奨になっています。
さらに、_std::shared_ptr
_のような、よりスマートな例もあります。これは、同じオブジェクトへの複数のポインターを許可し、最後のポインターが破棄されたときにのみクリーンアップします。
ステップごとの説明:
_// 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
_でアドレスを取得し、ポインターを削除すると、メモリーが解放されます。
しかし、私はこれを十分に強調することはできません、これをしないでください。重要なのはここだけです。
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())
と同等ではないため無効です。
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++のポインターがどのように機能するかについて多くのことを読むことをお勧めします。これらは高度なトピックであり、把握するのに時間がかかる場合がありますが、その使用は非常に貴重です。
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」オブジェクトから必ず呼び出されるようにする必要があります。
デストラクタも例外をスローしないでください。
これを行うと、メモリリークがほとんどなくなります。
それが簡単になったら、コンピューターのメモリをホテルのようなものと考え、プログラムは必要なときに部屋を借りる顧客です。
このホテルが機能する方法は、部屋を予約し、出発するときにポーターに伝えることです。
あなたがプログラムして部屋を予約し、ポーターに告げずに部屋を出ると、ポーターはその部屋がまだ使用中であると考え、他の人がそれを使用できないようにします。この場合、部屋の漏れがあります。
プログラムがメモリを割り当てて削除しない場合(単に使用を停止する場合)、コンピュータはメモリがまだ使用中であるとみなし、他のユーザーがそれを使用できないようにします。これはメモリリークです。
これは正確なアナロジーではありませんが、役に立つかもしれません。
object2
を作成すると、newで作成したオブジェクトのコピーが作成されますが、(割り当てられていない)ポインターも失われます(したがって、後で削除する方法はありません)。これを回避するには、object2
を参照する必要があります。
すぐに漏れているのはこの行です:
B object2 = *(new B());
ここでは、ヒープ上に新しいB
オブジェクトを作成してから、スタック上にコピーを作成しています。ヒープに割り当てられたものにはアクセスできなくなるため、リークが発生します。
この行はすぐには漏れません:
A *object1 = new A();
delete
d object1
しかし。
new
演算子を使用して割り当てたメモリを何らかの時点で解放しないと、そのメモリへのポインタをdelete
演算子に渡すことでメモリリークが発生します。
上記の2つの場合:
_A *object1 = new A();
_
ここでは、メモリを解放するためにdelete
を使用していないため、_object1
_ポインタが範囲外になった場合、ポインタを失い、メモリリークが発生します。そのため、delete
演算子を使用できません。
そしてここ
_B object2 = *(new B());
_
new B()
によって返されたポインターを破棄しているため、そのポインターをdelete
に渡してメモリを解放することはできません。したがって、別のメモリリーク。