C++プログラムでメモリがリークしないようにするための一般的なヒントは何ですか?動的に割り当てられたメモリを誰が解放すべきかをどのように判断するのですか?
RAIIとスマートポインターに関するすべてのアドバイスを徹底的に支持しますが、もう少し高レベルのヒントを追加したいと思います。管理するのが最も簡単なメモリは、割り当てたことのないメモリです。ほとんどすべてが参照であるC#やJavaなどの言語とは異なり、C++では、できる限りスタックにオブジェクトを配置する必要があります。 Stroustrup博士を含む複数の人々が指摘しているように、ガベージコレクションがC++で人気がなかった主な理由は、最初からよく書かれたC++がガベージをあまり生成しないことです。
書かないで
Object* x = new Object;
あるいは
shared_ptr<Object> x(new Object);
あなたが書くことができるとき
Object x;
この投稿は繰り返しのように見えますが、C++では、知っておくべき最も基本的なパターンは RAII です。
ブースト、TR1、または低レベル(ただし十分に効率的)なauto_ptrの両方からスマートポインターを使用する方法を学びます(ただし、その制限を知っておく必要があります)。
RAIIは、C++の例外安全性とリソース処理の両方の基盤であり、他のパターン(サンドイッチなど)が両方を提供することはありません(ほとんどの場合、何も提供しません)。
以下に、RAIIコードと非RAIIコードの比較を示します。
void doSandwich()
{
T * p = new T() ;
// do something with p
delete p ; // leak if the p processing throws or return
}
void doRAIIDynamic()
{
std::auto_ptr<T> p(new T()) ; // you can use other smart pointers, too
// do something with p
// WON'T EVER LEAK, even in case of exceptions, returns, breaks, etc.
}
void doRAIIStatic()
{
T p ;
// do something with p
// WON'T EVER LEAK, even in case of exceptions, returns, breaks, etc.
}
要約するには(からのコメントの後 オーガ詩sal33)、RAIIは3つの概念に依存しています。
つまり、正しいC++コードでは、ほとんどのオブジェクトはnew
で構築されず、代わりにスタック上で宣言されます。そして、new
を使用して構築されたものについては、すべてが何らかの形で スコープ付き (例:スマートポインターに接続)。
開発者として、これは非常に強力です。手動のリソース処理(Cで行われるように、またはJavaのオブジェクトでtry
/を集中的に使用する必要がないためです。 finally
その場合)...
「スコープ付きオブジェクトは...出口に関係なく破壊されます」それは完全に真実ではありません。 RAIIをごまかす方法があります。 terminate()のフレーバーは、クリーンアップをバイパスします。この点で、exit(EXIT_SUCCESS)は矛盾表現です。
wilhelmtell はそれについてまったく正しいです:RAIIをチートするexceptional方法があり、すべてプロセスの突然の停止につながります。
これらは exceptional の方法です。C++コードには終了、終了などが散らばっていないか、例外がある場合は、 未処理の例外が必要だからです プロセスをクラッシュさせ、クリーニング後ではなく、メモリイメージをそのままコアダンプします。
しかし、まれにしか発生しませんが、まだ発生する可能性があるため、これらのケースについてはまだ知っておく必要があります。
(誰がカジュアルなC++コードでterminate
またはexit
を呼び出しますか?... GLUT で遊んでいるときにその問題に対処しなければならなかったことを覚えています。非常にC指向で、積極的に設計することで、C++開発者が スタックに割り当てられたデータ を気にしない、または neverについて「興味深い」決定を下すなどの困難を引き起こすメインループから戻る ...それについてはコメントしません)。
boostのスマートポインター などのスマートポインターを確認する必要があります。
の代わりに
int main()
{
Object* obj = new Object();
//...
delete obj;
}
boost :: shared_ptrは、参照カウントがゼロになると自動的に削除します:
int main()
{
boost::shared_ptr<Object> obj(new Object());
//...
// destructor destroys when reference count is zero
}
私の最後のメモ、「参照カウントがゼロのとき、これが最もクールな部分です。したがって、オブジェクトのユーザーが複数いる場合、オブジェクトがまだ使用されているかどうかを追跡する必要はありません。共有ポインタ、それは破壊されます。
ただし、これは万能薬ではありません。ベースポインターにアクセスすることはできますが、その動作に自信がない限り、サードパーティのAPIに渡す必要はありません。多くの場合、スコープの作成が完了した後に作業を行うために、他のスレッドに「ポスト」するもの。これは、Win32のPostThreadMessageと共通です。
void foo()
{
boost::shared_ptr<Object> obj(new Object());
// Simplified here
PostThreadMessage(...., (LPARAM)ob.get());
// Destructor destroys! pointer sent to PostThreadMessage is invalid! Zohnoes!
}
いつものように、あらゆるツールで思考キャップを使用してください...
RAII を読み、理解してください。
ほとんどのメモリリークは、オブジェクトの所有権と有効期間が明確でないために発生します。
最初にすることは、できる限りスタックに割り当てることです。これは、何らかの目的で単一のオブジェクトを割り当てる必要がある場合のほとんどを処理します。
オブジェクトを「新規」にする必要がある場合、ほとんどの場合、そのオブジェクトの存続期間中、1人の明白な所有者がいます。この状況では、ポインターによって格納されたオブジェクトを「所有」するために設計されたコレクションテンプレートの束を使用する傾向があります。それらはSTLベクトルおよびマップコンテナーで実装されますが、いくつかの違いがあります。
私のSTLの欠点は、Valueオブジェクトに非常に焦点を合わせていることです。ほとんどのアプリケーションでは、オブジェクトは、これらのコンテナーで使用するのに必要な意味のあるコピーセマンティクスを持たない一意のエンティティです。
ああ、あなたの若い子供たちとあなたの新しいおかしなガベージコレクター...
「所有権」に関する非常に強力なルール-オブジェクトまたはソフトウェアの一部がオブジェクトを削除する権利を持っています。コメントと賢明な変数名を消去して、ポインターが「所有」しているのか、「ただ見て、触らない」のかを明確にします。誰が何を所有しているかを判断しやすくするために、すべてのサブルーチンまたはメソッド内で可能な限り「サンドイッチ」パターンに従ってください。
create a thing
use that thing
destroy that thing
時には、さまざまな場所で作成および破壊する必要があります。それを避けるのは難しいと思います。
複雑なデータ構造を必要とするプログラムでは、「所有者」ポインタを使用して、他のオブジェクトを含むオブジェクトの厳密な明確なツリーを作成します。このツリーは、アプリケーションドメインの概念の基本的な階層をモデル化します。 3Dシーンがオブジェクト、ライト、テクスチャを所有している例。プログラムが終了するレンダリングの最後に、すべてを破壊する明確な方法があります。
他の多くのポインターは、あるエンティティが別のエンティティにアクセスする必要があるとき、必要に応じて定義されます。これらは「見ているだけ」です。 3Dシーンの例では、オブジェクトはテクスチャを使用しますが、所有していません。他のオブジェクトは同じテクスチャを使用する場合があります。オブジェクトの破壊はnotのテクスチャの破壊を呼び出します。
はい、時間がかかりますが、それは私がやっていることです。メモリリークやその他の問題はめったにありません。しかし、その後、高性能科学、データ収集、グラフィックソフトウェアの限られた領域で仕事をしています。銀行やeコマース、イベント駆動型GUI、高度なネットワーク非同期カオスなどのトランザクションを扱うことはあまりありません。たぶん、新しい方法が利点を持っているでしょう!
いい質問です!
c ++を使用していて、リアルタイムのCPUおよびメモリバウドアプリケーション(ゲームなど)を開発している場合は、独自のメモリマネージャを作成する必要があります。
あなたができることは、さまざまな著者の興味深い作品をマージすることです。私はあなたにいくつかのヒントを与えることができると思います:
固定サイズのアロケーターは、ネットの至る所で議論されています
Small Object Allocationは、2001年にAlexandrescuによって彼の完璧な本「Modern c ++ design」で紹介されました。
ソースコードが配布された大きな進歩は、Dimitar Lazarovによって書かれた "High Performance Heap allocator"という名前のGame Programming Gem 7(2008)のすばらしい記事にあります。
リソースのすばらしいリストは this の記事にあります
自分でnoobの役に立たないアロケーターを書き始めないでください...最初に自分でドキュメントを作成してください。
C++のメモリ管理で一般的になったテクニックの1つは、 RAII です。基本的に、コンストラクター/デストラクターを使用してリソース割り当てを処理します。もちろん、例外の安全性のためにC++には他にも不快な詳細がいくつかありますが、基本的な考え方は非常に単純です。
通常、問題は所有権の問題になります。 Scott MeyersによるEffective C++シリーズとAndrei AlexandrescuによるModern C++ Designを読むことを強くお勧めします。
リークしない方法についてはすでに多くのことがありますが、リークを追跡するのに役立つツールが必要な場合は、以下をご覧ください。
どこでもできるユーザースマートポインター!あらゆるクラスのメモリリークはなくなります。
プロジェクト全体でメモリ所有権のルールを共有し、把握します。 COMルールを使用すると、最高の一貫性が得られます([in]パラメーターは呼び出し元が所有し、呼び出し先がコピーする必要があります。[out]パラメーターは呼び出し元が所有します。
valgrindは、実行時にプログラムのメモリリークもチェックするための優れたツールです。
ほとんどの種類のLinux(Androidを含む)およびDarwinで使用できます。
プログラムの単体テストの作成に使用する場合、テストでvalgrindを体系的に実行する習慣を身に付ける必要があります。初期段階で多くのメモリリークを回避できる可能性があります。また、通常、完全なソフトウェアでの簡単なテストでそれらを特定するのが簡単です。
もちろん、このアドバイスは他のメモリチェックツールでも有効です。
また、stdライブラリクラス(ベクターなど)がある場合は、手動で割り当てられたメモリを使用しないでください。仮想デストラクターがあるというルールに違反していないか確認してください。
何かにスマートポインターを使用できない/使用できない場合(それは大きな赤い旗であるはずですが)、次のようにコードを入力します。
allocate
if allocation succeeded:
{ //scope)
deallocate()
}
それは明らかですが、必ず入力してくださいbeforeスコープにコードを入力します
重要度の順のヒント:
-Tip#1デストラクタを「仮想」と宣言することを常に忘れないでください。
-Tip#2 RAIIを使用する
-Tip#3 Boostのスマートポインターを使用する
-ヒント#4バグのある独自のSmartpointersを記述しないで、ブーストを使用してください(現在使用中のプロジェクトでは、ブーストを使用できません。再び同じルートですが、今もまた依存関係にブーストを追加することはできません)
ヒント#5カジュアル/非パフォーマンスクリティカル(数千のオブジェクトを含むゲームのように)動作する場合は、Thorsten Ottosenのブーストポインターコンテナーを見てください。
-Tip#6 Visual Leak Detectionの「vld」ヘッダーなど、選択したプラットフォームのリーク検出ヘッダーを検索します
これらのバグの頻繁な原因は、オブジェクトへの参照またはポインターを受け入れるが、所有権が不明のままであるメソッドがある場合です。スタイルとコメントの規則により、これが起こりにくくなります。
関数がオブジェクトの所有権を取得するケースを特別なケースとします。これが発生するすべての状況で、これを示すヘッダーファイルの関数の横にコメントを必ず書いてください。ほとんどの場合、オブジェクトを割り当てるモジュールまたはクラスがオブジェクトの割り当てを解除する責任があることを確認するよう努力する必要があります。
Constを使用すると、場合によっては非常に役立ちます。関数がオブジェクトを変更せず、戻り後も持続するオブジェクトへの参照を保存しない場合は、const参照を受け入れます。呼び出し元のコードを読むと、関数がオブジェクトの所有権を受け入れていないことが明らかになります。同じ関数が非constポインターを受け入れるようにすることもできますし、呼び出し元は呼び出し先が所有権を受け入れたと仮定する場合もしない場合もありますが、const参照があれば問題はありません。
引数リストで非const参照を使用しないでください。呼び出し元のコードを読み取るときに、呼び出し先がパラメーターへの参照を保持している可能性があることは非常に不明確です。
参照カウントポインターを推奨するコメントに同意しません。これは通常は正常に機能しますが、バグがあり、それが機能しない場合、特にデストラクタがマルチスレッドプログラムなどで重要なことを行う場合は機能しません。それほど難しくない場合は、参照カウントを必要としないように設計を調整してください。
可能であれば、boost shared_ptrと標準C++ auto_ptrを使用します。これらは所有権のセマンティクスを伝えます。
Auto_ptrを返すとき、呼び出し元にメモリの所有権を与えていることを伝えています。
Shared_ptrを返すとき、あなたはそれを参照していることを呼び出し側に伝え、所有者の一部になりますが、それは彼らの責任だけではありません。
これらのセマンティクスはパラメーターにも適用されます。呼び出し元がauto_ptrを渡すと、所有権が付与されます。
メモリを手動で管理する場合、次の2つのケースがあります。
これらの規則を破る必要がある場合は、文書化してください。
ポインターの所有権がすべてです。
valgrind(* nixプラットフォームでのみ利用可能)は非常に素晴らしいメモリチェッカーです
他の人は、最初にメモリリークを回避する方法に言及しています(スマートポインターなど)。しかし、プロファイリングおよびメモリ分析ツールは、多くの場合、問題が発生したらメモリの問題を追跡する唯一の方法です。
Valgrind memcheck は無料の優れたものです。
MSVCの場合のみ、各.cppファイルの先頭に次を追加します。
#ifdef _DEBUG
#define new DEBUG_NEW
#endif
次に、VS2003以降を使用してデバッグする場合、プログラムの終了時にリークが通知されます(新規/削除を追跡します)。それは基本ですが、過去に私を助けてくれました。
C++はRAIIを念頭に設計されています。 C++でメモリを管理するより良い方法は本当にないと思います。ただし、ローカルスコープに非常に大きなチャンク(バッファオブジェクトなど)を割り当てないように注意してください。スタックオーバーフローが発生する可能性があり、そのチャンクの使用中に境界チェックに欠陥がある場合、他の変数を上書きしたり、アドレスを返したりすることができ、あらゆる種類のセキュリティホールにつながります。
異なる場所での割り当てと破棄に関する唯一の例の1つは、スレッドの作成(渡すパラメーター)です。しかし、この場合でも簡単です。スレッドを作成する関数/メソッドは次のとおりです。
struct myparams {
int x;
std::vector<double> z;
}
std::auto_ptr<myparams> param(new myparams(x, ...));
// Release the ownership in case thread creation is successfull
if (0 == pthread_create(&th, NULL, th_func, param.get()) param.release();
...
ここでは代わりにスレッド関数
extern "C" void* th_func(void* p) {
try {
std::auto_ptr<myparams> param((myparams*)p);
...
} catch(...) {
}
return 0;
}
かなり簡単ですよね?スレッドの作成に失敗した場合、リソースはauto_ptrによって解放(削除)されます。それ以外の場合、所有権はスレッドに渡されます。スレッドが非常に高速で、作成後にリソースを解放する場合
param.release();
メイン関数/メソッドで呼び出されますか?何もない!割り当て解除を無視するようにauto_ptrに「伝える」からです。 C++のメモリ管理は簡単ですか?乾杯、
えま!
他のリソース(ハンドル、ファイル、データベース接続、ソケットなど)を管理するのと同じ方法でメモリを管理します。 GCもそれらを支援しません。
メモリ割り当て関数をインターセプトして、プログラムの終了時に解放されていないメモリゾーンがあるかどうかを確認できます(ただし、allアプリケーションには適していません)。
また、演算子newとdeleteおよびその他のメモリ割り当て関数を置き換えることにより、コンパイル時に実行できます。
たとえば、これをチェックしてください site [C++でのメモリ割り当てのデバッグ]注:delete演算子には、次のようなトリックもあります。
#define DEBUG_DELETE PrepareDelete(__LINE__,__FILE__); delete
#define delete DEBUG_DELETE
いくつかの変数にファイルの名前を格納し、オーバーロードされた削除演算子がいつどこから呼び出されたかを知ることができます。このようにして、プログラムからのすべての削除およびmallocのトレースを取得できます。メモリチェックシーケンスの最後に、割り当てられたメモリブロックが「削除」されていないことをファイル名と行番号で識別して報告できるはずです。
Visual Studioで BoundsChecker のようなものを試すこともできます。これは非常に興味深く、使いやすいです。
すべての割り当て関数を、前に短い文字列を追加し、最後にセンチネルフラグを追加するレイヤーでラップします。たとえば、「myalloc(pszSomeString、iSize、iAlignment);またはnew( "description"、iSize)MyObject();を呼び出して、指定されたサイズに加えて、ヘッダーとセンチネルに十分なスペースを内部的に割り当てます。 、デバッグ以外のビルドの場合はこれをコメントアウトすることを忘れないでください!これを行うには少しメモリが必要ですが、メリットはコストをはるかに上回ります。
これには3つの利点があります。まず、特定の「ゾーン」に割り当てられているが、それらのゾーンが解放されるべきときにクリーンアップされないコードをすばやく検索することにより、漏れているコードを簡単かつ迅速に追跡できます。また、すべてのセンチネルが無傷であることを確認することにより、境界が上書きされたことを検出することも役立ちます。これにより、よく隠されたクラッシュやアレイのミスステップを見つけようとする際に何度も節約できました。 3番目の利点は、メモリの使用を追跡して大きなプレーヤーが誰であるかを確認することです。たとえば、MemDumpの特定の説明の照合は、「サウンド」が予想よりも多くのスペースを占有していることを示します。