C++ でのメモリ リークを回避するための一般的なガイドライン [終了]
-
09-06-2019 - |
質問
C++ プログラムでメモリ リークを確実に防ぐための一般的なヒントは何ですか?動的に割り当てられたメモリを誰が解放すべきかを特定するにはどうすればよいですか?
他のヒント
私は RAII とスマート ポインターに関するすべてのアドバイスを全面的に支持しますが、もう少し高度なヒントも追加したいと思います。管理が最も簡単なメモリは、一度も割り当てなかったメモリです。ほとんどすべてが参照である C# や Java のような言語とは異なり、C++ では可能な限りオブジェクトをスタックに置く必要があります。Stroustrup 博士を含む何人かの人々が指摘しているように、C++ でガベージ コレクションが普及していない主な理由は、よく書かれた C++ ではそもそもガベージがあまり生成されないことです。
書かないでください
Object* x = new Object;
あるいは
shared_ptr<Object> x(new Object);
ただ書けるとき
Object x;
使用 ライ
- ガベージコレクションを忘れる (代わりに RAII を使用してください)。ガベージ コレクターでもリークする可能性があることに注意してください (Java/C# で一部の参照を "null" にし忘れた場合)、ガベージ コレクターはリソースの破棄には役立ちません (ハンドルを取得したオブジェクトがある場合)。ファイルの場合、Java で手動で実行するか、C# で「dispose」パターンを使用しない限り、オブジェクトがスコープ外になるとファイルは自動的に解放されません。
- 「関数ごとに 1 つの戻り値」というルールを忘れてください. 。これはリークを避けるための C の良いアドバイスですが、例外を使用するため (代わりに RAII を使用してください)、C++ では時代遅れです。
- そしてその間 「サンドイッチパターン」 これは C 言語の良いアドバイスです。 C++ では古い 例外を使用するためです (代わりに RAII を使用してください)。
この投稿は繰り返しのように見えますが、C++ で知っておくべき最も基本的なパターンは次のとおりです。 ライ.
boost、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.
}
について ライ
要約すると(からのコメントの後) オーガ詩篇33)、RAII は 3 つの概念に依存しています。
- オブジェクトが構築されたら、あとは機能するだけです。 コンストラクターでリソースを取得してください。
- オブジェクト破壊だけで十分です! デストラクターでリソースを解放します。
- すべてはスコープに関するものです。 スコープ付きオブジェクト (上記の doRAIIStatic の例を参照) は、その宣言時に構築され、終了方法 (リターン、ブレーク、例外など) に関係なく、実行がスコープを抜けた瞬間に破棄されます。
これは、正しい C++ コードでは、ほとんどのオブジェクトが次のように構築されないことを意味します。 new
, 、代わりにスタック上で宣言されます。そして、それを使用して構築されたものについては、 new
, 、すべては何とかなるだろう 範囲指定された (例えば。スマートポインターに接続されています)。
開発者にとって、これは実際に非常に強力です。手動のリソース処理 (C で行われる場合や、Java の一部のオブジェクトの場合のように、 try
/finally
その場合のために)...
編集 (2012-02-12)
「スコープ指定されたオブジェクト ...破壊されます...出口は関係なく」というのは完全に真実ではありません。RAIIを騙す方法があります。terminate() のどのフレーバーでもクリーンアップはバイパスされます。この点において、 exit(EXIT_SUCCESS) は矛盾しています。
– ウィルヘルムテル
ウィルヘルムテル それについてはまったく正しいです:がある 例外的な RAII を不正行為する方法、すべてがプロセスの突然の停止につながります。
それらは 例外的な C++ コードには、terminate、exit などが散りばめられていないため、または例外がある場合には、 未処理の例外 プロセスをクラッシュし、クリーニング後ではなく、そのメモリ イメージをそのままコア ダンプします。
しかし、それらのケースはめったに起こりませんが、依然として発生する可能性があるため、私たちは依然としてそれらのケースについて知っておく必要があります。
(誰が電話したのか terminate
または exit
カジュアルな C++ コードで?...で遊んでいたときにその問題に対処しなければならなかったのを覚えています 過食症:このライブラリは非常に C 指向であり、C++ 開発者にとって困難になるように積極的に設計されています。 スタックに割り当てられたデータ, 、または「興味深い」決定を下す メインループから決して戻らない...それについてはコメントしません).
次のようなスマート ポインターを確認するとよいでしょう。 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!
}
いつものように、どんなツールでも思考力を発揮してください...
よく読んで ライ そしてそれを必ず理解してください。
ほとんどのメモリ リークは、オブジェクトの所有権と有効期間が明確でないことが原因で発生します。
最初に行うことは、可能な限りスタックに割り当てることです。これは、何らかの目的で単一のオブジェクトを割り当てる必要があるほとんどの場合に対処します。
オブジェクトを「新規」にする必要がある場合、ほとんどの場合、オブジェクトには残りの存続期間にわたって単一の明確な所有者が存在することになります。この状況に対して、私は、ポインタによって格納されているオブジェクトを「所有」するように設計されたコレクション テンプレートを多数使用する傾向があります。これらは STL ベクターおよびマップ コンテナーを使用して実装されますが、いくつかの違いがあります。
- これらのコレクションをコピーしたり割り当てたりすることはできません。(オブジェクトが含まれている場合。)
- オブジェクトへのポインタが挿入されます。
- コレクションが削除されると、まずコレクション内のすべてのオブジェクトに対してデストラクターが呼び出されます。(私は別のバージョンを持っていますが、そこでは破棄され空ではない場合にアサートされます。)
- これらのコンテナにはポインタが格納されるため、継承されたオブジェクトをこれらのコンテナに格納することもできます。
STL に関する私の欠点は、ほとんどのアプリケーションではオブジェクトは固有のエンティティであり、それらのコンテナでの使用に必要な意味のあるコピー セマンティクスを持たないという点です。
ああ、若い子供たちと新手のガベージコレクター...
「所有権」に関する非常に強力なルール、つまりどのオブジェクトまたはソフトウェアの一部がオブジェクトを削除する権利を持っているかということです。コメントと賢明な変数名を明確にして、ポインターが「所有」されているのか、「見るだけで触らない」のかを明確にします。誰が何を所有するかを決定しやすくするために、すべてのサブルーチンまたはメソッド内の「サンドイッチ」パターンにできる限り従うようにしてください。
create a thing
use that thing
destroy that thing
場合によっては、さまざまな場所で作成したり破壊したりする必要があります。それを避けるのは難しいと思います。
複雑なデータ構造を必要とするプログラムでは、「所有者」ポインターを使用して、他のオブジェクトを含むオブジェクトの厳密に明確なツリーを作成します。このツリーは、アプリケーション ドメインの概念の基本的な階層をモデル化します。たとえば、3D シーンはオブジェクト、ライト、テクスチャを所有します。レンダリングの最後にプログラムが終了するとき、すべてを破棄する明確な方法があります。
配列などをスキャンするために、あるエンティティが別のエンティティにアクセスする必要があるときは常に、他の多くのポインタが必要に応じて定義されます。これらは「ただ見るだけ」です。3D シーンの例では、オブジェクトはテクスチャを使用しますが、所有していません。他のオブジェクトも同じテクスチャを使用する場合があります。オブジェクトの破壊は、 ない あらゆるテクスチャの破壊を引き起こします。
はい、時間はかかりますが、それが私がやっていることです。メモリリークやその他の問題が発生することはほとんどありません。しかし、私は高性能科学、データ収集、グラフィック ソフトウェアという限られた分野で働いています。私は、銀行取引や e コマース、イベント駆動型 GUI、または高度なネットワーク化された非同期カオスなどのトランザクションを扱うことはあまりありません。もしかしたら、新しい方法には利点があるかもしれません。
素晴らしい質問です!
C++ を使用していて、リアルタイムの CPU とメモリを使用するアプリケーション (ゲームなど) を開発している場合は、独自のメモリ マネージャーを作成する必要があります。
さまざまな作者の興味深い作品をいくつか統合する方が良いと思います。ヒントをいくつか教えます。
固定サイズ アロケータについては、ネットのあらゆるところで頻繁に議論されています
Small Object Allocation は、2001 年に Alexandrescu によって彼の完璧な著書『Modern C++ design』で紹介されました。
大きな進歩 (配布されたソース コード付き) は、Dimitar Lazarov によって書かれた Game Programming Gem 7 (2008) の「High Performance Heap allocator」という素晴らしい記事にあります。
リソースの優れたリストは次の場所にあります。 これ 記事
役に立たない初心者のアロケータを自分で書き始めないでください...まず自分自身を記録してください。
C++ のメモリ管理で一般的になっている手法の 1 つは次のとおりです。 ライ. 。基本的に、リソース割り当てを処理するにはコンストラクター/デストラクターを使用します。もちろん、C++ には例外の安全性のために他にも不快な詳細がいくつかありますが、基本的な考え方は非常に単純です。
問題は通常、所有権の問題に帰着します。Scott Meyers の『Effective C++』シリーズと Andrei Alexandrescu の『Modern C++ Design』を読むことを強くお勧めします。
リークを防ぐ方法についてはすでにたくさんありますが、リークを追跡するためのツールが必要な場合は、以下を参照してください。
- 境界チェッカー VSの下で
- FluidStudio の MMGR C/C++ ライブラリhttp://www.paulnettle.com/pub/FluidStudios/MemoryManagers/Fluid_Studios_Memory_Manager.zip (割り当てメソッドをオーバーライドし、割り当て、リークなどのレポートを作成します)
スマート ポインターを可能な限りどこでも使用してください。クラス全体のメモリ リークが解消されます。
プロジェクト全体でメモリ所有権ルールを共有し、把握します。COM ルールを使用すると、最高の一貫性が得られます ([in] パラメーターは呼び出し元が所有し、呼び出し先はコピーする必要があります。[out] パラメータは呼び出し元が所有しており、参照を保持している場合、呼び出し先はコピーを作成する必要があります。等。)
ヴァルグリンド は、実行時にプログラムのメモリ リークをチェックするのにも適したツールです。
これは、ほとんどの Linux (Android を含む) と Darwin で利用できます。
プログラムの単体テストを作成する場合は、テストで valgrind を体系的に実行する習慣を身に付ける必要があります。これにより、多くのメモリ リークを初期段階で回避できる可能性があります。また、通常は、完全なソフトウェアを使用するよりも単純なテストで問題を特定する方が簡単です。
もちろん、このアドバイスは他のメモリ チェック ツールにも当てはまります。
また、std ライブラリ クラス (例:ベクター)。このルールに違反する場合は、仮想デストラクターがあることを確認してください。
何かにスマート ポインターを使用できない/使用しない場合 (ただし、それは重大な危険信号のはずです)、次のようにコードを入力します。
allocate
if allocation succeeded:
{ //scope)
deallocate()
}
当たり前のことですが、必ず入力してください 前に スコープ内に任意のコードを入力します
これらのバグの原因としてよくあるのは、オブジェクトへの参照またはポインタを受け入れるメソッドがあるものの、所有権が不明瞭なままになっている場合です。スタイルとコメントの規則により、この可能性が低くなります。
関数がオブジェクトの所有権を取得する場合を特殊なケースとします。これが発生するすべての状況において、ヘッダー ファイル内の関数の隣に、これを示すコメントを必ず書き込んでください。ほとんどの場合、オブジェクトを割り当てるモジュールまたはクラスがその割り当てを解除する責任も負っていることを確認するように努める必要があります。
const を使用すると、場合によっては非常に役立つことがあります。関数がオブジェクトを変更せず、関数が戻った後も保持されるオブジェクトへの参照を保存しない場合は、const 参照を受け入れます。呼び出し元のコードを読むと、関数がオブジェクトの所有権を受け入れていないことが明らかです。同じ関数に非 const ポインタを受け入れることもでき、呼び出し元は呼び出し先が所有権を受け入れたと想定しているかどうかはわかりませんが、const 参照の場合は問題ありません。
引数リストでは非 const 参照を使用しないでください。呼び出し元のコードを読むとき、呼び出し先がパラメーターへの参照を保持しているかどうかは非常に不明確です。
私は、参照カウントされたポインターを推奨するコメントには同意しません。これは通常は正常に機能しますが、バグがあり、特にマルチスレッド プログラムなどでデストラクターが重要な処理を行う場合には機能しません。それほど難しくない場合は、参照カウントを必要としないように設計を調整するようにしてください。
重要度順に並べたヒント:
-ヒント#1 常にデストラクターを「仮想」と宣言することを忘れないでください。
-ヒント#2 RAII を使用する
-ヒント#3 boost のスマートポインターを使用する
-ヒント#4 バグの多いスマートポインタを自分で作成しないで、ブーストを使用してください (現在取り組んでいるプロジェクトではブーストを使用できません。また、自分でスマート ポインタをデバッグしなければならないことに苦労しています。私なら絶対に使用しません)もう一度同じルートを進みますが、今は依存関係にブーストを追加できません)
-ヒント#5 カジュアル/非パフォーマンスクリティカルな作業 (数千のオブジェクトを使用するゲームなど) の場合は、Thorsten Ottosen のブースト ポインタ コンテナを見てください。
-ヒント#6 Visual Leak Detection の「vld」ヘッダーなど、選択したプラットフォームのリーク検出ヘッダーを見つけます。
可能であれば、boostshared_ptr と標準の C++ auto_ptr を使用してください。これらは所有権のセマンティクスを伝えます。
auto_ptr を返すと、呼び出し元にメモリの所有権を与えていることを伝えます。
shared_ptr を返すと、呼び出し元にそれへの参照があり、所有権の一部を取得することを通知することになりますが、それは呼び出し元だけの責任ではありません。
これらのセマンティクスはパラメータにも適用されます。呼び出し元が auto_ptr を渡した場合、呼び出し元はあなたに所有権を与えたことになります。
他の人は、そもそもメモリ リークを回避する方法 (スマート ポインターなど) について言及しています。しかし、多くの場合、メモリの問題が発生した場合、それを追跡するにはプロファイリングおよびメモリ分析ツールが唯一の方法です。
Valgrind memcheck 優れた無料のものです。
MSVC の場合のみ、各 .cpp ファイルの先頭に次の行を追加します。
#ifdef _DEBUG
#define new DEBUG_NEW
#endif
その後、VS2003 以降でデバッグすると、プログラムの終了時にリークが通知されます (新規/削除が追跡されます)。基本的なことですが、以前は役に立ちました。
valgrind (*nix プラットフォームでのみ利用可能) は非常に優れたメモリチェッカーです
メモリを手動で管理する場合は、次の 2 つのケースがあります。
- オブジェクトを作成し (おそらく、新しいオブジェクトを割り当てる関数を呼び出して間接的に)、それを使用し (または呼び出した関数がそれを使用し)、それを解放します。
- 誰かが私にリファレンスをくれたので、それを解放すべきではありません。
これらのルールに違反する必要がある場合は、それを文書化してください。
すべてはポインターの所有権に関するものです。
- オブジェクトを動的に割り当てることは避けてください。クラスに適切なコンストラクターとデストラクターがある限り、それを指すポインターではなく、クラス型の変数を使用します。動的割り当てと割り当て解除はコンパイラーが自動的に行うため、動的割り当てと割り当て解除を回避できます。
実際、これは「スマート ポインター」で使用されるメカニズムでもあり、他のライターによって RAII と呼ばれています ;-) 。 - オブジェクトを他の関数に渡すときは、ポインターよりも参照パラメーターを優先します。これにより、起こり得るいくつかのエラーが回避されます。
- 可能であればパラメータ、特にオブジェクトへのポインタを const として宣言します。そうすれば、オブジェクトが「誤って」解放されることはなくなります (const をキャストした場合を除く ;-)))。
- プログラム内でメモリの割り当てと割り当て解除を行う場所の数を最小限に抑えます。E.g.同じ型を数回割り当てたり解放したりする場合は、そのための関数 (またはファクトリ メソッド ;-)) を作成してください。
この方法により、必要に応じてデバッグ出力 (どのアドレスが割り当てられ、割り当て解除されるかなど) を簡単に作成できます。 - ファクトリ関数を使用して、単一の関数から複数の関連クラスのオブジェクトを割り当てます。
- クラスに仮想デストラクターを持つ共通の基本クラスがある場合は、同じ関数 (または静的メソッド) を使用してそれらをすべて解放できます。
- purify などのツールを使用してプログラムをチェックしてください (残念なことに、多くの $/€/...)。
メモリ割り当て関数をインターセプトして、プログラムの終了時に解放されていないメモリ ゾーンがあるかどうかを確認できます (ただし、これは、 全て アプリケーション)。
また、new 演算子や delete 演算子、その他のメモリ割り当て関数を置き換えることにより、コンパイル時に実行することもできます。
たとえば、これをチェックインします サイト C ++でのメモリ割り当てのデバッグ]注:削除演算子には次のようなトリックもあります。
#define DEBUG_DELETE PrepareDelete(__LINE__,__FILE__); delete
#define delete DEBUG_DELETE
いくつかの変数にファイルの名前と、オーバーロードされた削除演算子がファイルが呼び出された場所をいつ認識するかを格納できます。このようにして、プログラムからのすべての削除と malloc のトレースを取得できます。メモリチェックシーケンスの最後に、ファイル名と行番号でそれを特定して、割り当てられたメモリのどのブロックが「削除」されなかったのかを報告できるはずです。これが私が望むものだと思います。
次のようなことを試すこともできます 境界チェッカー これは非常に面白くて使いやすい Visual Studio です。
すべての割り当て関数を、先頭に短い文字列を追加し、最後にセンチネル フラグを追加するレイヤーでラップします。たとえば、「myalloc( pszSomeString, iSize, iAlignment );」を呼び出すことになります。または new( "説明", iSize ) MyObject();これにより、指定されたサイズに加えて、ヘッダーとセンチネル用に十分なスペースが内部的に割り当てられます。もちろん、デバッグ以外のビルドの場合は、これをコメントアウトすることを忘れないでください。これを行うにはもう少し多くのメモリが必要ですが、そのメリットはコストをはるかに上回ります。
これには 3 つの利点があります。1 つ目は、特定の「ゾーン」に割り当てられているが、それらのゾーンが解放されるべきときにクリーンアップされていないコードを迅速に検索することで、リークしているコードを簡単かつ迅速に追跡できるようになります。すべてのセンチネルが損なわれていないことを確認することで、境界が上書きされたことを検出することもできます。これにより、隠れたクラッシュや配列の間違いを見つけようとするときに、何度も救われました。3 番目の利点は、メモリの使用状況を追跡して、誰が有力者であるかを確認できることです。たとえば、MemDump 内の特定の記述を照合することで、「サウンド」が予想よりもはるかに多くのスペースを占有していることがわかります。
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();
main 関数/メソッドで呼び出されますか?何もない!auto_ptr に割り当て解除を無視するように「指示」するからです。C++ のメモリ管理は簡単ですよね?乾杯、
えま!
他のリソース (ハンドル、ファイル、DB 接続、ソケットなど) を管理するのと同じ方法でメモリを管理します。GC もそれらについては役に立ちません。
関数からの戻り値は 1 つだけです。そうすれば、そこで割り当て解除を行うことができ、見逃すことはありません。
そうしないと、間違いを犯しやすくなります。
new a()
if (Bad()) {delete a; return;}
new b()
if (Bad()) {delete a; delete b; return;}
... // etc.