Javaには自動GCがあり、たまに世界を停止しますが、ヒープ上のゴミを処理します。現在、C / C ++アプリケーションにはこれらのSTWフリーズがなく、メモリ使用量も無限に増加しません。この動作はどのように達成されますか?死んだオブジェクトはどのように処理されますか?
new
。
What happens to garbage in C++?
通常、実行可能ファイルにコンパイルされていませんか?
Javaには自動GCがあり、たまに世界を停止しますが、ヒープ上のゴミを処理します。現在、C / C ++アプリケーションにはこれらのSTWフリーズがなく、メモリ使用量も無限に増加しません。この動作はどのように達成されますか?死んだオブジェクトはどのように処理されますか?
new
。
What happens to garbage in C++?
通常、実行可能ファイルにコンパイルされていませんか?
回答:
プログラマは、を介して作成したオブジェクトがを介しnew
て削除されるようにする責任がありますdelete
。オブジェクトが作成されたが、最後のポインターまたはオブジェクトへの参照が範囲外になる前に破壊されなかった場合、そのオブジェクトはクラックを通過してメモリリークになります。
残念ながら、C、C ++、およびGCを含まない他の言語では、これは単純に時間の経過とともに積み上げられます。アプリケーションまたはシステムのメモリが不足し、新しいメモリブロックを割り当てることができなくなる可能性があります。この時点で、ユーザーはオペレーティングシステムがその使用済みメモリを再利用できるように、アプリケーションを終了する必要があります。
この問題を軽減する限り、プログラマーの生活をずっと楽にするいくつかのことがあります。これらは主に範囲の性質によってサポートされます。
int main()
{
int* variableThatIsAPointer = new int;
int variableInt = 0;
delete variableThatIsAPointer;
}
ここでは、2つの変数を作成しました。それらは、中括弧で定義されているように、ブロックスコープに存在し{}
ます。実行がこのスコープ外に移動すると、これらのオブジェクトは自動的に削除されます。この場合、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します。
それを超えると、プログラマーがコードがオブジェクトを適切に処理することを保証するための適切な実践と規律にすべて帰着します。
delete
-それは私が探していたものです。驚くばかり。
delete
自動であなたのために呼び出されたスマートポインタをあなたがそれらを使用する場合、自動ストレージが使用できない場合、あなたがそれらを毎回使用することを検討する必要がありますので。
delete
、アプリケーションコード(およびC ++ 14以降、と同じnew
)に実際に含める必要はありませんが、代わりにスマートポインターとRAIIを使用してヒープオブジェクトを削除する必要があります。std::unique_ptr
型とstd::make_unique
関数は、アプリケーションコードレベルの直接new
かつ最も単純な置換delete
です。
C ++にはガベージコレクションがありません。
C ++アプリケーションは、ごみを処理する必要があります。
これを理解するには、C ++アプリケーションプログラマが必要です。
彼らが忘れるとき、結果は「メモリリーク」と呼ばれます。
new
とdelete
。
malloc
およびfree
、またはnew[]
およびdelete[]
、またはその他のアロケータ(のWindowsのようにGlobalAlloc
、LocalAlloc
、SHAlloc
、CoTaskMemAlloc
、VirtualAlloc
、HeapAlloc
、...)、そしてあなたのために割り当てられたメモリ(例えば経由fopen
)。
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番目に単純な方法は型レベルでのサイクルを回避することです。
その結果、メモリリークは、C ++での問題ではない限り、彼らは使用を控えて、でも新しいユーザーのために、new
、delete
またはstd::shared_ptr
。これは、堅固な規律が必要なCとは異なり、通常は不十分です。
ただし、メモリリークの双子の姉妹、ダングリングポインターについて言及しなければ、この答えは完全ではありません。
宙ぶらりんのポインター(または宙ぶらりんの参照)は、死んだオブジェクトへのポインターまたは参照を保持することによって作成される危険です。例えば:
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";
}
ダングリングポインターまたは参照の使用は、未定義の動作です。一般的に、幸いなことに、これは即時のクラッシュです。非常に頻繁に、残念ながら、これは最初にメモリ破損を引き起こします...そしてコンパイラが本当に奇妙なコードを出力するため、時々奇妙な動作が発生します。
未定義の動作は、プログラムのセキュリティ/正確性の点で、今日までのCおよびC ++の最大の問題です。ガベージコレクターも未定義の動作もない言語のRustをチェックすることをお勧めします。
new
、delete
およびshared_ptr
」の使用を控えている限り、それは修飾されています。なしでnew
、shared_ptr
あなたは直接所有権を持っているので、漏れはありません。もちろん、あなたはぶら下がりポインタなどを持っている可能性があります...しかし、私はあなたがそれらを取り除くためにC ++を残す必要があるのではないかと心配しています。
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
句で)か、この問題を解決する特別なメカニズムが生成されますが、特にエレガントな方法ではありません(通常、十分な人々が欠点に苦しんだ)。このようなメカニズムは、Javaのtry-with-resourcesおよびC#のusingステートメントであり、どちらもC ++のRAIIの近似です。
要約すると、これはすべてC ++のRAIIの非常に表面的な説明でしたが、C ++のメモリやリソース管理も通常は「手動」ではなく、実際はほとんど自動であることを読者が理解できるように願っています。
delete
、あなたは死んでいる」という答えが30ポイントを超えて急上昇し、受け入れられていますが、これには5つあります。誰もが実際にここで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;
}
このコードは、バット醜いものとgoto
これはちょうど、完全に救済することなく、かなりの問題に対処する唯一の方法である、存在しない状態で、構造化例外処理メカニズムの任意の並べ替えのが、特にあなたの資源配分コードがネストされている場合より1ループよりも深く。これは、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 );
}
goto
ステートメントがあっても良い答えです。これは、一部の領域で推奨されるプラクティスです。これは、Cの例外に相当するものから保護するために一般的に使用されるスキームです。Linuxカーネルコードを見てくださいgoto
。
goto
は無関係です。およびに変更goto done;
するreturn arr;
と読みやすくなります。より複雑なケースでは、実際には複数のs が存在する可能性がありますが、異なるレベルの準備状態で展開し始めます(C ++での例外スタックのアンワインドによって行われること)。arr=NULL;done:return arr;
return NULL;
goto
私は、記憶の問題をさまざまなカテゴリーに分類することを学びました。
一度滴る。プログラムが起動時に100バイトをリークし、二度とリークしないと仮定します。これらの1回限りのリークを追いかけて排除するのは良いことです(リーク検出機能によってクリーンなレポートを作成するのが好きです)が、必須ではありません。時には、攻撃する必要がある大きな問題があります。
繰り返しの漏れ。プログラムの有効期間中に繰り返し呼び出される関数で、定期的にメモリをリークすることは大きな問題です。これらのしずくは、プログラムと、場合によってはOSを拷問して死に至らしめるでしょう。
相互参照。オブジェクトAとBが共有ポインターを介して相互に参照する場合、それらのクラスの設計、またはこれらのクラスを実装/使用して循環性を破るコードのいずれかで、特別なことを行う必要があります。(これは、ガベージコレクションされた言語の問題ではありません。)
覚えすぎ。これは、ガベージ/メモリリークの邪悪な従兄弟です。RAIIはここでは役に立ちませんし、ガベージコレクションも行いません。これはどの言語でも問題です。アクティブな変数に、ランダムなメモリチャンクに接続する経路がある場合、そのメモリのランダムチャンクはガベージではありません。プログラムを忘れっぽくして数日間実行できるようにするのは難しい。数か月間(たとえば、ディスクが故障するまで)実行できるプログラムを作成するのは、非常に難しい作業です。
私は長い間、リークに関して深刻な問題を抱えていませんでした。C ++でRAIIを使用すると、これらのしずくと漏れに対処するのに非常に役立ちます。(しかし、共有ポインターには注意する必要があります。)さらに重要なことに、メモリの使用が不要になったため、メモリへの接続が切断されるため、メモリの使用量が増え続けます。