Javaには、時々Stop The Worldを停止する自動GCがありますが、ヒープ上のゴミを処理します。現在、C/C++アプリケーションにはこれらのSTWのフリーズがなく、メモリ使用量が無限に増加することもありません。この動作はどのようにして達成されますか?死んだオブジェクトはどのように処理されますか?
プログラマは、new
を介して作成したオブジェクトがdelete
を介して削除されるようにする責任があります。オブジェクトが作成されたが、最後のポインターまたはオブジェクトへの参照が範囲外になる前に破棄されなかった場合、オブジェクトはクラックを通り抜けて Memory Leak になります。
残念ながら、GCを含まないC、C++、およびその他の言語では、これは単純に時間の経過とともに蓄積されます。これにより、アプリケーションまたはシステムのメモリが不足し、新しいメモリブロックを割り当てることができなくなります。この時点で、ユーザーはアプリケーションを終了して、オペレーティングシステムが使用されたメモリを解放できるようにする必要があります。
この問題を緩和する限り、プログラマーの生活をはるかに楽にするいくつかのことがある。これらは主に scope の性質によってサポートされています。
int main()
{
int* variableThatIsAPointer = new int;
int variableInt = 0;
delete variableThatIsAPointer;
}
ここでは、2つの変数を作成しました。 {}
中括弧で定義されているように、Block Scopeに存在します。実行がこのスコープ外に移動すると、これらのオブジェクトは自動的に削除されます。この場合、variableThatIsAPointer
は、その名前が示すように、メモリ内のオブジェクトへのポインタです。スコープ外になると、ポインターは削除されますが、ポインターが指すオブジェクトは残ります。ここでは、スコープ外になる前にこのオブジェクトをdelete
して、メモリリークがないことを確認します。ただし、このポインターを別の場所に渡し、後で削除されることを期待することもできます。
このスコープの性質はクラスにまで及びます。
class Foo
{
public:
int bar; // Will be deleted when Foo is deleted
int* otherBar; // Still need to call delete
}
ここでも同じ原則が当てはまります。 bar
が削除されても、Foo
について心配する必要はありません。ただし、otherBar
の場合は、ポインターのみが削除されます。 otherBar
がそれが指しているオブジェクトへの唯一の有効なポインタである場合、delete
のデストラクタでFoo
にする必要があります。これが背後にある推進コンセプトです [〜#〜] raii [〜#〜]
リソースの割り当て(取得)はオブジェクトの作成(特に初期化)中にコンストラクターによって行われ、リソースの割り当て解除(解放)はオブジェクトの破棄(特にファイナライズ)中にデストラクタによって行われます。したがって、リソースは、初期化が完了してからファイナライズが開始されるまで保持され(リソースの保持はクラス不変です)、オブジェクトが生きている場合にのみ保持されることが保証されます。したがって、オブジェクトリークがない場合、リソースリークはありません。
RAIIも スマートポインター の背後にある典型的な原動力です。 C++標準ライブラリでは、これらはstd::shared_ptr
、std::unique_ptr
、およびstd::weak_ptr
です。同じ概念に従う他のshared_ptr
/weak_ptr
実装を見て使用しましたが。これらの場合、参照カウンターは、特定のオブジェクトへのポインターの数を追跡し、オブジェクトへの参照がなくなると、オブジェクトを自動的にdelete
sします。
それを超えて、プログラマーがコードがオブジェクトを適切に処理することを保証することは、すべて適切な実践と規律に帰着します。
C++にはガベージコレクションはありません。
C++アプリケーションは、自分のゴミを処分する必要があります。
C++アプリケーションプログラマは、これを理解する必要があります。
彼らが忘れるとき、結果は「メモリリーク」と呼ばれます。
C、C++、およびガベージコレクタのないその他のシステムでは、開発者は言語とそのライブラリによって、メモリを再利用できる時期を示す機能を提供されます。
最も基本的な機能は自動ストレージです。多くの場合、言語自体がアイテムの廃棄を保証します。
int global = 0; // automatic storage
int foo(int a, int b) {
static int local = 1; // automatic storage
int c = a + b; // automatic storage
return c;
}
この場合、コンパイラーは、それらの値がいつ使用されないかを認識し、それらに関連付けられたストレージを再利用します。
動的ストレージを使用する場合、Cでは通常、メモリはmalloc
で割り当てられ、free
で再利用されます。 C++では、メモリは従来new
で割り当てられ、delete
で再利用されます。
Cは何年も変わっていませんが、最近のC++はnew
とdelete
を完全に避け、代わりにライブラリ機能に依存しています(これらはnew
とdelete
を使用しています)適切に):
std::unique_ptr
およびstd::shared_ptr
std::string
、std::vector
、std::map
、...すべて動的に割り当てられたメモリを内部的に透過的に管理しますshared_ptr
と言えば、リスクがあります。参照のサイクルが形成され、壊れていない場合、メモリリークが発生する可能性があります。この状況を回避するかどうかは開発者次第です。最も簡単な方法はshared_ptr
を完全に回避することであり、2番目に簡単な方法は型レベルでのサイクルを回避することです。
結果として、新規ユーザーがnew
、delete
またはstd::shared_ptr
を使用しない限り、C++ではメモリリークは問題ではありません 。これは、頑固な規律が必要であり、一般に不十分であるCとは異なります。
ただし、この回答は、メモリリークの姉妹関係に言及しなければ完全ではありません。dangling pointers。
ぶら下がりポインタ(またはぶら下がり参照)は、死んでいるオブジェクトへのポインタまたは参照を保持することによって作成されるハザードです。例えば:
int main() {
std::vector<int> vec;
vec.Push_back(1); // vec: [1]
int& a = vec.back();
vec.pop_back(); // vec: [], "a" is now dangling
std::cout << a << "\n";
}
ダングリングポインターまたは参照の使用はUndefined Behaviorです。一般的に、幸いなことに、これは即時のクラッシュです。非常に多くの場合、残念ながら、これが最初にメモリの破損を引き起こします...そして、コンパイラーが本当に奇妙なコードを出力するため、奇妙な動作が時々発生します。
未定義の動作は、プログラムのセキュリティ/正確性の観点から、今日までのCおよびC++の最大の問題です。 Rustでガーベッジコレクターがなく、未定義の動作がない言語をチェックしてください。
C++には [〜#〜] raii [〜#〜] と呼ばれるものがあります。基本的には、ゴミを山に置いたままにするのではなく、ゴミを片付けて掃除することを意味します。 (サッカーを見ている私の部屋を想像してみてください-私はビールの缶を飲み、新しいものを必要とするとき、C++の方法は、冷蔵庫に行く途中で空の缶を箱に入れることです、C#の方法はそれを床にチャックすることです彼女が掃除をしに来るとき、メイドがそれらを拾うのを待ちます)。
これで、C++でメモリリークが発生する可能性がありますが、そのためには、通常の構成を残して、Cの方法に戻す必要があります。メモリのブロックを割り当て、そのブロックがどこにあるかを追跡するために、言語支援は必要ありません。一部の人はこのポインタを忘れてしまい、ブロックを削除できません。
C++の場合、「手動でメモリ管理を行う必要がある」という一般的な誤解があることに注意してください。実際、通常、コードではメモリ管理を行いません。
オブジェクトが必要なほとんどの場合、オブジェクトはプログラム内で定義された有効期間を持ち、スタック上に作成されます。これはすべての組み込みプリミティブデータ型で機能しますが、クラスや構造体のインスタンスでも機能します。
_class MyObject {
public: int x;
};
int objTest()
{
MyObject obj;
obj.x = 5;
return obj.x;
}
_
スタックオブジェクトは、関数が終了すると自動的に削除されます。 Javaでは、オブジェクトは常にヒープ上に作成されるため、ガベージコレクションなどのメカニズムによって削除する必要があります。これはスタックオブジェクトでは問題になりません。
スタック上のスペースの使用は、固定サイズのオブジェクトに対して機能します。配列などの可変量のスペースが必要な場合は、別のアプローチが使用されます。リストは、動的メモリを管理する固定サイズのオブジェクトにカプセル化されます。オブジェクトが特別なクリーンアップ関数、デストラクタを持つことができるので、これは機能します。オブジェクトがスコープ外に出て、コンストラクターの反対を行うときに呼び出されることが保証されています。
_class MyList {
public:
// a fixed-size pointer to the actual memory.
int* listOfInts;
// constructor: get memory
MyList(size_t numElements) { listOfInts = new int[numElements]; }
// destructor: free memory
~MyList() { delete[] listOfInts; }
};
int listTest()
{
MyList list(1024);
list.listOfInts[200] = 5;
return list.listOfInts[200];
// When MyList goes off stack here, its destructor is called and frees the memory.
}
_
メモリが使用されるコードには、メモリ管理はまったくありません。確認する必要があるのは、作成したオブジェクトに適切なデストラクタがあることだけです。どのようにlistTest
のスコープを離れても、例外を介して、または単にそれから戻ることによって、デストラクタ~MyList()
が呼び出され、メモリを管理する必要はありません。
(バイナリNOT演算子_~
_を使用してデストラクタを指定することは、面白いデザインの決定だと思います。数値で使用すると、これはビットを反転します;同様に、ここでは、コンストラクターが行ったことが反転していることを示しています。)
基本的に、動的メモリを必要とするすべてのC++オブジェクトは、このカプセル化を使用します。これはRAII(「リソース獲得は初期化」)と呼ばれています。これは、オブジェクトが自分のコンテンツを気にするという単純なアイデアを表現するのに非常に奇妙な方法です。彼らが獲得するのは、片付けをする彼らのものです。
現在、これらの両方のケースは、明確に定義された有効期間を持つメモリに関するものでした。有効期間はスコープと同じです。スコープを離れたときにオブジェクトを期限切れにしたくない場合は、メモリを管理できる3つ目のメカニズム、スマートポインターがあります。スマートポインターは、実行時に型が変化するが、共通のインターフェイスまたは基本クラスを持つオブジェクトのインスタンスがある場合にも使用されます。
_class MyDerivedObject : public MyObject {
public: int y;
};
std::unique_ptr<MyObject> createObject()
{
// actually creates an object of a derived class,
// but the user doesn't need to know this.
return std::make_unique<MyDerivedObject>();
}
int dynamicObjTest()
{
std::unique_ptr<MyObject> obj = createObject();
obj->x = 5;
return obj->x;
// At scope end, the unique_ptr automatically removes the object it contains,
// calling its destructor if it has one.
}
_
複数のクライアント間でオブジェクトを共有するための別の種類のスマートポインタ_std::shared_ptr
_があります。最後のクライアントがスコープから外れたときにのみ、含まれているオブジェクトを削除するため、クライアントの数とオブジェクトを使用する期間が完全に不明な場合に使用できます。
要約すると手動でメモリ管理を実際に行っていないことがわかります。すべてがカプセル化され、完全に自動化されたスコープベースのメモリ管理によって処理されます。これでは不十分な場合は、生のメモリをカプセル化するスマートポインタが使用されます。
例外が発生したときに管理することはほとんど不可能であるため、C++コードの任意の場所でリソースオーナーとして生のポインターを使用し、コンストラクターの外で生の割り当てを使用し、デストラクター外で生のdelete
呼び出しを使用することは非常に悪い習慣と考えられています。安全に使用するのは難しい。
RAIIの最大の利点の1つは、メモリに限定されないことです。実際には、ファイルやソケット(開閉)などのリソースや、ミューテックス(ロック/ロック解除)などの同期メカニズムを管理するための非常に自然な方法を提供します。基本的に、取得できて解放する必要のあるすべてのリソースは、C++ではまったく同じ方法で管理され、この管理はユーザーに任されません。これはすべて、コンストラクタで取得し、デストラクタで解放するクラスにカプセル化されます。
たとえば、ミューテックスをロックする関数は通常、C++では次のように記述されます。
_void criticalSection() {
std::scoped_lock lock(myMutex); // scoped_lock locks the mutex
doSynchronizedStuff();
} // myMutex is released here automatically
_
他の言語では、これを手動で(例:finally
句で)要求するか、この問題を解決する特別なメカニズムを生成しますが、特にエレガントな方法では生成しません(通常、後の方で)人生、十分な人々が欠点に苦しんでいるとき)。このようなメカニズムは、try-with-resourcesin Java andusingC#のステートメント。どちらもC++のRAIIの近似です。
要約すると、これらすべてはC++でのRAIIの非常に表面的な説明でしたが、C++でのメモリ管理やリソース管理でさえ通常は「手動」ではなく、実際にはほとんど自動であることを読者が理解するのに役立つことを願っています。
特にCに関しては、この言語は動的に割り当てられたメモリを管理するツールを提供しません。すべての*alloc
は対応するfree
をどこかに持っています。
物事が本当に厄介なのは、リソース割り当てが途中で失敗したときです。もう一度やり直しますか、ロールバックして最初からやり直しますか、ロールバックしてエラーで終了しますか?完全にベイルしてOSに処理させますか?
たとえば、非連続の2D配列を割り当てる関数は次のとおりです。ここでの動作は、プロセスの途中で割り当てエラーが発生した場合、すべてをロールバックし、NULLポインターを使用してエラーを返します。
/**
* Allocate space for an array of arrays; returns NULL
* on error.
*/
int **newArr( size_t rows, size_t cols )
{
int **arr = malloc( sizeof *arr * rows );
size_t i;
if ( arr ) // malloc returns NULL on failure
{
for ( i = 0; i < rows; i++ )
{
arr[i] = malloc( sizeof *arr[i] * cols );
if ( !arr[i] )
{
/**
* Whoopsie; we can't allocate any more memory for some reason.
* We can't just return NULL at this point since we'll lose access
* to the previously allocated memory, so we branch to some cleanup
* code to undo the allocations made so far.
*/
goto cleanup;
}
}
}
goto done;
/**
* We encountered a failure midway through memory allocation,
* so we roll back all previous allocations and return NULL.
*/
cleanup:
while ( i ) // this is why we didn't limit the scope of i to the for loop
free( arr[--i] ); // delete previously allocated rows
free( arr ); // delete arr object
arr = NULL;
done:
return arr;
}
このコードはbutt-uglyとgoto
sを組み合わせたものですが、構造化された例外処理メカニズムが存在しない場合、これは問題を解決する唯一の方法です。完全に特にリソース割り当てコードが複数のループの深さでネストされている場合。これは、goto
が実際に魅力的なオプションである非常に数少ない時間の1つです。それ以外の場合は、多数のフラグと追加のif
ステートメントを使用しています。
次のようなリソースごとに専用のアロケータ/デアロケータ関数を書くことで、自分自身の生活を楽にすることができます
Foo *newFoo( void )
{
Foo *foo = malloc( sizeof *foo );
if ( foo )
{
foo->bar = newBar();
if ( !foo->bar ) goto cleanupBar;
foo->bletch = newBletch();
if ( !foo->bletch ) goto cleanupBletch;
...
}
goto done;
cleanupBletch:
deleteBar( foo->bar );
// fall through to clean up the rest
cleanupBar:
free( foo );
foo = NULL;
done:
return foo;
}
void deleteFoo( Foo *f )
{
deleteBar( f->bar );
deleteBletch( f->bletch );
free( f );
}
メモリの問題をさまざまなカテゴリに分類する方法を学びました。
一度は滴る。プログラムが起動時に100バイトをリークし、二度とリークしないと仮定します。これらの1回限りのリークを追跡して排除することは素晴らしいことです(私は、リーク検出機能によってクリーンなレポートを作成するのが好きです)が、必須ではありません。攻撃する必要があるより大きな問題がある場合があります。
繰り返しリーク。大きな問題を定期的にリークするプログラムの存続期間中に繰り返し呼び出される関数。これらのしずくは、プログラムを、場合によってはOSを拷問して死に至らせます。
相互参照。オブジェクトAとBが共有ポインターを介して相互に参照している場合、それらのクラスの設計またはそれらのクラスを実装/使用するコードのいずれかで、循環性を打破するために特別なことを行う必要があります。 (これはガベージコレクションされた言語では問題になりません。)
覚えすぎ。これはゴミ/メモリリークの邪悪な従兄弟です。 RAIIはここでは役に立ちませんし、ガベージコレクションも行いません。これはどの言語でも問題です。アクティブな変数にランダムなメモリのチャンクに接続する経路がある場合、そのランダムなメモリのチャンクはガベージではありません。プログラムを忘れっぽくして数日間実行できるようにするのは難しい作業です。数か月間(たとえば、ディスクが故障するまで)実行できるプログラムを作成することは、非常にトリッキーです。
私は長い間、漏れの深刻な問題を抱えていませんでした。 C++でRAIIを使用すると、これらの点滴やリークに対処できます。 (ただし、共有ポインターには注意する必要があります。)さらに重要なのは、メモリへの接続が切断されなくなったことにより、メモリの使用が増え続けるアプリケーションに問題が生じたことです。