C++ プログラムでメモリ リークを起こさないようにするための一般的なヒントは何ですか? 動的に割り当てられたメモリを誰が解放する必要があるかを知るにはどうすればよいですか?
29 に答える
私は RAII とスマート ポインターに関するすべてのアドバイスを全面的に支持しますが、少しレベルの高いヒントも追加したいと思います。ほとんどすべてが参照である C# や Java などの言語とは異なり、C++ では、可能な限りオブジェクトをスタックに配置する必要があります。何人かの人々 (Dr Stroustrup を含む) が指摘しているように、C++ でガベージ コレクションが普及していない主な理由は、適切に作成された C++ はそもそもあまりガベージを生成しないためです。
書かないで
Object* x = new Object;
あるいは
shared_ptr<Object> x(new Object);
あなたがただ書くことができるとき
Object x;
RAⅡを使う
- ガベージ コレクションは忘れてください (代わりに RAII を使用してください)。ガベージ コレクターでさえリークする可能性があることに注意してください (Java/C# で一部の参照を "null" するのを忘れた場合)。 Java で手動で解放しないか、C# で "dispose" パターンを使用しないと、オブジェクトがスコープ外になったときにファイルが自動的に解放されません)。
- 「関数ごとに 1 つのリターン」というルールは忘れてください。これは、リークを回避するための C の適切なアドバイスですが、C++ では例外が使用されているため (代わりに RAII を使用してください) 古くなっています。
- また、「サンドイッチ パターン」は C の優れたアドバイスですが、C++ では 例外を使用しているため (代わりに RAII を使用してください)、時代遅れになっています。
この投稿は繰り返しのようですが、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.
}
ライイについて
要約すると ( Ogre Psalm33からのコメントの後)、RAII は 3 つの概念に依存しています。
- オブジェクトが構築されると、それは機能します! コンストラクターでリソースを取得します。
- オブジェクトの破壊で十分です!デストラクタでリソースを解放します。
- それはすべてスコープについてです!スコープ付きオブジェクト (上記の doRAIIStatic の例を参照) は、宣言時に構築され、実行がスコープを終了した瞬間に、終了方法 (リターン、ブレーク、例外など) に関係なく破棄されます。
これは、正しい C++ コードでは、ほとんどのオブジェクトが で構築されずnew
、代わりにスタックで宣言されることを意味します。また、 を使用して構築されたものについてはnew
、すべて何らかの形でスコープが設定されます(たとえば、スマート ポインターにアタッチされます)。
開発者として、これは実際に非常に強力です。手動のリソース処理を気にする必要がないためです (C で行われるように、またはその場合にtry
/finally
を集中的に使用する Java の一部のオブジェクトに対して)...
編集 (2012-02-12)
「スコープオブジェクトは...破壊されます...出口に関係なく」それは完全に真実ではありません。RAII をだます方法があります。terminate() の任意のフレーバーは、クリーンアップをバイパスします。exit(EXIT_SUCCESS) は、この点で矛盾しています。
wilhelmtellはその点について非常に正しいです: RAII をだます例外的な方法があり、そのすべてがプロセスを突然停止させます。
C++ コードには終了、終了などが散らばっていないため、これらは例外的な方法です。例外がある場合、未処理の例外でプロセスをクラッシュさせ、クリーニング後ではなくメモリ イメージをそのままコア ダンプする必要があります。
しかし、これらのケースはめったに発生しませんが、まだ発生する可能性があるため、これらのケースについて知っておく必要があります。
(カジュアルなC++ コードでは誰が呼び出しますterminate
か?... GLUTexit
で遊んでいるときにその問題に対処しなければならなかったことを覚えています。スタックに割り当てられたデータについて、またはメインループから決して戻らないという「興味深い」決定を下すことについて...私はそれについてコメントしません)。
ブーストのスマート ポインターなどのスマート ポインターを確認する必要があります。
それ以外の
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をよく読んで、理解しておいてください。
ああ、あなたは幼い子供たちとあなたの新しいゴミ収集家...
「所有権」に関する非常に強力なルール-ソフトウェアのどのオブジェクトまたは一部がオブジェクトを削除する権利を持っているか。コメントと賢明な変数名を明確にして、ポインタが「所有している」か「見ているだけで触れない」かを明確にします。誰が何を所有しているかを判断するために、すべてのサブルーチンまたはメソッド内の「サンドイッチ」パターンに可能な限り従ってください。
create a thing
use that thing
destroy that thing
場合によっては、大きく異なる場所で作成および破棄する必要があります。私はそれを避けるのは難しいと思います。
複雑なデータ構造を必要とするプログラムでは、「所有者」ポインタを使用して、他のオブジェクトを含むオブジェクトの厳密な明確なツリーを作成します。このツリーは、アプリケーションドメインの概念の基本的な階層をモデル化しています。例3Dシーンは、オブジェクト、ライト、テクスチャを所有しています。プログラムが終了したときのレンダリングの最後に、すべてを破壊する明確な方法があります。
他の多くのポインタは、あるエンティティが別のエンティティにアクセスする必要があるとき、光線をスキャンする必要があるときなど、必要に応じて定義されます。これらは「見ているだけ」です。3Dシーンの例の場合-オブジェクトはテクスチャを使用しますが、所有していません。他のオブジェクトも同じテクスチャを使用する場合があります。オブジェクトを破棄しても、テクスチャは破棄されません。
はい、それは時間がかかりますが、それは私がしていることです。メモリリークやその他の問題はめったにありません。しかし、私は高性能の科学、データ取得、グラフィックソフトウェアの限られた分野で働いています。銀行やeコマース、イベント駆動型GUI、ネットワーク化された高度な非同期カオスなどのトランザクションを扱うことはあまりありません。たぶん、新しい方法には利点があります!
ほとんどのメモリ リークは、オブジェクトの所有権と有効期間が明確でないことが原因です。
最初に行うことは、可能な限りスタックに割り当てることです。これは、何らかの目的のために単一のオブジェクトを割り当てる必要があるほとんどのケースに対処します。
オブジェクトを「新しく」する必要がある場合は、ほとんどの場合、残りのライフタイムで明確な所有者が 1 人になります。このような状況では、ポインターによって格納されたオブジェクトを「所有」するように設計された一連のコレクション テンプレートを使用する傾向があります。これらは、STL ベクターおよびマップ コンテナーで実装されますが、いくつかの違いがあります。
- これらのコレクションをコピーしたり、割り当てたりすることはできません。(オブジェクトが含まれている場合)
- オブジェクトへのポインターが挿入されます。
- コレクションが削除されると、まずコレクション内のすべてのオブジェクトに対してデストラクタが呼び出されます。(破壊されて空でない場合にアサートする別のバージョンがあります。)
- これらはポインタを格納するため、継承されたオブジェクトをこれらのコンテナに格納することもできます。
STL に関する私の不満は、ほとんどのアプリケーション オブジェクトでは、これらのコンテナーで使用するために必要な意味のあるコピー セマンティクスを持たない一意のエンティティであるのに対し、STL は Value オブジェクトに重点を置いていることです。
素晴らしい質問です。
C++ を使用していて、リアルタイムの CPU とメモリを使用するアプリケーション (ゲームなど) を開発している場合は、独自のメモリ マネージャーを作成する必要があります。
さまざまな著者の興味深い作品をいくつかマージする方がよいと思います。いくつかのヒントを提供できます。
固定サイズのアロケーターは、ネットのいたるところでよく議論されています
スモール オブジェクト アロケーションは、2001 年に Alexandrescu によって彼の完璧な本「Modern c++ design」で紹介されました。
Dimitar Lazarov によって書かれた「High Performance Heap allocator」という名前の Game Programming Gem 7 (2008) の素晴らしい記事で、(ソース コードが配布された) 大きな進歩が見られます。
リソースの優れたリストは、この記事にあります。
noob の役に立たないアロケータを自分で書き始めないでください...最初に自分自身を文書化してください。
リークしない方法についてはすでにたくさんありますが、リークを追跡するのに役立つツールが必要な場合は、以下を参照してください。
- VSでのBoundsChecker
- FluidStudiohttp ://www.paulnettle.com/pub/FluidStudios/MemoryManagers/Fluid_Studios_Memory_Manager.zipのMMGRC/ C ++ lib (割り当てメソッドをオーバーライドし、割り当て、リークなどのレポートを作成します)
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()
}
それは明らかですが、スコープにコードを入力する前に必ず入力してください
これらのバグのよくある原因は、オブジェクトへの参照またはポインターを受け入れるが、所有権が不明確なままになっているメソッドがある場合です。スタイルとコメントの規則により、この可能性は低くなります。
関数がオブジェクトの所有権を取得する場合を特殊なケースとします。これが発生するすべての状況で、これを示すヘッダー ファイルの関数の横にコメントを必ず記述してください。ほとんどの場合、オブジェクトを割り当てるモジュールまたはクラスが、その割り当て解除にも責任があることを確認するように努める必要があります。
const を使用すると、場合によっては非常に役立ちます。関数がオブジェクトを変更せず、返された後も保持されるオブジェクトへの参照を格納しない場合は、const 参照を受け入れます。呼び出し元のコードを読むと、関数がオブジェクトの所有権を受け入れていないことが明らかです。同じ関数に非 const ポインターを受け入れるようにすることもできます。呼び出し元は、呼び出し先が所有権を受け入れたと想定している場合と想定していない場合がありますが、const 参照では問題ありません。
引数リストで非 const 参照を使用しないでください。呼び出し元のコードを読むと、呼び出し先がパラメーターへの参照を保持している可能性があることは非常に不明確です。
参照カウント ポインターを推奨するコメントには同意しません。これは通常は問題なく機能しますが、バグがあり機能しない場合、特にマルチスレッド プログラムなどでデストラクタが重要な処理を行う場合はそうです。難しすぎない場合は、参照カウントが不要になるように設計を調整してください。
重要度順のヒント:
-Tip#1 デストラクタを「仮想」として宣言することを常に忘れないでください。
-Tip#2 RAII を使う
-Tip#3 ブーストのスマートポインターを使用する
-Tip#4 独自のバグのある Smartpointers を作成しないでください。boost を使用してください。同じルートを繰り返しますが、今は依存関係にブーストを追加できません)
-Tip#5 カジュアル/非パフォーマンス クリティカル (数千のオブジェクトを使用するゲームなど) が機能する場合は、Thorsten Ottosen のブースト ポインター コンテナーを参照してください。
-Tip#6 Visual Leak Detection の「vld」ヘッダーなど、選択したプラットフォームのリーク検出ヘッダーを見つけます
他の人は、そもそもメモリ リークを回避する方法について言及しています (スマート ポインターなど)。しかし、プロファイリングおよびメモリ分析ツールは、多くの場合、メモリの問題が発生した後に追跡する唯一の方法です。
Valgrind memcheckは優れた無料のツールです。
可能であれば、boost shared_ptr と標準 C++ auto_ptr を使用してください。それらは所有権のセマンティクスを伝えます。
auto_ptr を返すと、呼び出し元にメモリの所有権を与えることを伝えます。
shared_ptr を返すときは、呼び出し元にそれへの参照があり、所有権の一部であることを伝えていますが、それは呼び出し元だけの責任ではありません。
これらのセマンティクスはパラメーターにも適用されます。呼び出し元が auto_ptr を渡した場合、所有権が与えられます。
MSVC の場合のみ、各 .cpp ファイルの先頭に次を追加します。
#ifdef _DEBUG
#define new DEBUG_NEW
#endif
次に、VS2003 以降でデバッグすると、プログラムが終了するときにリークが通知されます (新規/削除を追跡します)。これは基本的なことですが、過去に私を助けてくれました。
- オブジェクトを動的に割り当てることは避けてください。クラスに適切なコンストラクターとデストラクターがある限り、クラスへのポインターではなく、クラス型の変数を使用します。動的な割り当てと解放は、コンパイラーが行うため回避できます。
実際、これは「スマート ポインター」によって使用されるメカニズムでもあり、他のライターによって RAII と呼ばれています ;-) . - オブジェクトを他の関数に渡すときは、ポインターよりも参照パラメーターを優先してください。これにより、発生する可能性のあるエラーを回避できます。
- 可能であれば、特にオブジェクトへのポインタなど、パラメータ const を宣言します。そうすれば、オブジェクトを「誤って」解放することはできません (const をキャストした場合を除く ;-)))。
- プログラム内でメモリの割り当てと割り当て解除を行う場所の数を最小限に抑えます。例)同じ型を数回割り当てたり解放したりする場合は、そのための関数 (またはファクトリ メソッド ;-)) を記述します。
このようにして、必要に応じて、デバッグ出力 (どのアドレスが割り当てられ、割り当て解除されるかなど) を簡単に作成できます。 - ファクトリ関数を使用して、単一の関数から複数の関連クラスのオブジェクトを割り当てます。
- クラスに仮想デストラクタを持つ共通の基本クラスがある場合は、同じ関数 (または静的メソッド) を使用してそれらすべてを解放できます。
- Purify などのツールを使用してプログラムをチェックします (残念ながら $/€/... がたくさんあります)。
メモリを手動で管理する場合は、次の 2 つのケースがあります。
- オブジェクトを作成し (おそらく、新しいオブジェクトを割り当てる関数を呼び出すことによって間接的に)、それを使用し (または呼び出した関数がそれを使用し)、解放します。
- 誰かが私に参照をくれたので、解放すべきではありません。
これらの規則のいずれかを破る必要がある場合は、それを文書化してください。
ポインタの所有権がすべてです。
valgrind (*nix プラットフォームでのみ利用可能) は非常に優れたメモリ チェッカーです。
メモリ割り当て機能をインターセプトして、プログラムの終了時に解放されないメモリゾーンがあるかどうかを確認できます(ただし、すべてのアプリケーションに適しているわけではありません)。
また、演算子newとdelete、およびその他のメモリ割り当て関数を置き換えることにより、コンパイル時に実行することもできます。
たとえば、このサイトをチェックしてください[C ++でのメモリ割り当てのデバッグ]注:削除演算子には、次のようなトリックもあります。
#define DEBUG_DELETE PrepareDelete(__LINE__,__FILE__); delete
#define delete DEBUG_DELETE
いくつかの変数にファイルの名前を格納でき、オーバーロードされた削除演算子がいつ呼び出された場所であるかを知ることができます。このようにして、プログラムからのすべての削除とmallocのトレースを取得できます。メモリチェックシーケンスの最後に、割り当てられたメモリブロックが「削除」されなかったことを報告して、ファイル名と行番号で識別できるようにする必要があります。
Visual StudioでBoundsCheckerのようなものを試すこともできます。これは、非常に興味深く、使いやすいものです。
すべての割り当て関数を、先頭に短い文字列を追加し、最後に番兵フラグを追加するレイヤーでラップします。たとえば、 "myalloc(pszSomeString、iSize、iAlignment);またはnew(" description "、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();
メイン関数/メソッドで呼び出されますか? 何もない!auto_ptr に割り当て解除を無視するように「指示」するためです。C++ のメモリ管理は簡単ですね。乾杯、
エマ!
他のリソース (ハンドル、ファイル、データベース接続、ソケットなど) を管理するのと同じ方法でメモリを管理します。GCもそれらについては役に立ちません。
任意の関数からの戻り値は 1 つだけです。そうすれば、そこで割り当てを解除でき、見逃すことはありません。
そうしないと、間違いを犯しやすいです。
new a()
if (Bad()) {delete a; return;}
new b()
if (Bad()) {delete a; delete b; return;}
... // etc.