C ++プログラムでメモリをリークしないようにするための一般的なヒントは何ですか?動的に割り当てられたメモリを解放する必要があるのはどのようにすればよいですか?
C ++プログラムでメモリをリークしないようにするための一般的なヒントは何ですか?動的に割り当てられたメモリを解放する必要があるのはどのようにすればよいですか?
回答:
メモリを手動で管理する代わりに、適切な場所でスマートポインタを使用してみてください。
見てくださいブーストlibに、TR1、およびスマートポインタを。
また、スマートポインターはC ++ 11と呼ばれるC ++標準の一部になりました。
RAIIとスマートポインターに関するすべてのアドバイスを徹底的に推奨しますが、少しだけレベルの高いヒントも追加したいと思います。管理するのが最も簡単なメモリは、割り当てられていないメモリです。ほとんどすべてが参照であるC#やJavaなどの言語とは異なり、C ++では、できる限りオブジェクトをスタックに配置する必要があります。いくつかの人々(Dr Stroustrupを含む)が指摘しているように、ガベージコレクションがC ++で人気がなかった主な理由は、適切に作成されたC ++が最初から多くのガベージを生成しないためです。
書かないで
Object* x = new Object;
あるいは
shared_ptr<Object> x(new Object);
あなたが書くことができるとき
Object x;
この投稿は繰り返しのようですが、C ++では、最も基本的なパターンはRAIIです。
スマートポインターの使用方法を学習します。両方とも、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.
}
要約すると(Ogre Psalm33からのコメントの後)、RAIIは3つの概念に依存します。
つまり、正しいC ++コードでは、ほとんどのオブジェクトはで作成されずnew
、代わりにスタックで宣言されます。そして、を使用して構築されたものについてはnew
、すべてが何らかの形でスコープされます(たとえば、スマートポインターにアタッチされます)。
開発者として、これは実際に非常に強力です(Cで行われるように、またはtry
/ finally
を集中的に使用するJavaの一部のオブジェクトに対して)手動のリソース処理を気にする必要がないため...
「スコープ付きオブジェクト...破壊されます...出口に関係なく」それは完全に真実ではありません。RAIIをだます方法があります。terminate()のフレーバーはクリーンアップをバイパスします。exit(EXIT_SUCCESS)は、この点に関して矛盾している。
– ヴィルヘルムテル
wilhelmtellはそのことについてかなり右である:あり、例外 RAIIをごまかすための方法は、すべてのプロセス急停止につながります。
C ++コードが終了や終了などで散らかされないため、これらは例外的な方法です。または、例外が発生した場合、未処理の例外がプロセスをクラッシュさせ、メモリイメージをそのままクリーンアップしてコアをダンプしたいので、クリーニングした後ではありません。
ただし、これらのケースについては、ほとんど発生しませんが、発生する可能性があるため、引き続き知っておく必要があります。
(誰が呼び出したterminate
かexit
カジュアルなC ++コードに...私は一緒に遊んでたときにその問題に対処することを覚えて?GLUT:このライブラリは非常に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!
}
いつものように、あらゆるツールで思考キャップを使用してください...
ほとんどのメモリリークは、オブジェクトの所有権と有効期間が明確でないことが原因です。
最初にすることは、できる限りスタックに割り当てることです。これは、何らかの目的で単一のオブジェクトを割り当てる必要があるほとんどの場合を扱います。
オブジェクトを「新規」にする必要がある場合、ほとんどの場合、オブジェクトには残りの存続期間中、明らかな所有者が1人います。この状況では、ポインターによって格納されたオブジェクトを「所有」するように設計されたコレクションテンプレートの束を使用する傾向があります。これらはSTLベクトルとマップコンテナーで実装されていますが、いくつかの違いがあります。
私のSTLの欠点は、Valueオブジェクトに重点が置かれていることですが、ほとんどのアプリケーションでは、オブジェクトはこれらのコンテナーでの使用に必要な意味のあるコピーセマンティクスを持たない一意のエンティティです。
ああ、あなたはあなたの若い子供たちとあなたの新しくできたガベージコレクター...
「所有権」に関する非常に強力なルール-どのオブジェクトまたはソフトウェアの一部がオブジェクトを削除する権利を持っているか。コメントと賢明な変数名を明確にして、ポインターが「所有している」か「見ているだけで触らない」かを明確にします。誰が何を所有しているのかを判断するために、すべてのサブルーチンまたはメソッド内の「サンドイッチ」パターンをできる限り従います。
create a thing
use that thing
destroy that thing
場合によっては、さまざまな場所で作成および破棄する必要があります。それを避けるのは難しいと思います。
複雑なデータ構造を必要とするプログラムでは、「所有者」ポインタを使用して、他のオブジェクトを含むオブジェクトの厳密な明確なツリーを作成します。このツリーは、アプリケーションドメインの概念の基本的な階層をモデル化しています。3Dシーンがオブジェクト、ライト、テクスチャを所有している例。プログラムが終了するレンダリングの最後に、すべてを破壊する明確な方法があります。
他の多くのポインタは、あるエンティティが別のエンティティにアクセスする必要があるとき、アレイ上をスキャンするために、必要に応じて定義されます。これらは「見ているだけ」です。3Dシーンの例-オブジェクトはテクスチャを使用していますが、所有していません。他のオブジェクトが同じテクスチャを使用する場合があります。オブジェクトを破棄しても、テクスチャは破棄されません。
はい、それは時間がかかりますが、それは私がやっていることです。メモリリークやその他の問題が発生することはほとんどありません。しかし、私は高性能の科学、データ収集、およびグラフィックスソフトウェアの限られた分野で働いています。私は、銀行やeコマース、イベント駆動型GUI、高度なネットワーク非同期カオスなどのトランザクションを処理することはあまりありません。多分新しい手間のかかる方法はそこに利点があります!
すばらしい質問です。
c ++を使用していて、リアルタイムのCPUおよびメモリの大容量アプリケーション(ゲームなど)を開発している場合は、独自のメモリマネージャを作成する必要があります。
あなたができるより良いことは、様々な作者のいくつかの興味深い作品をマージすることだと思います、私はあなたにいくつかのヒントを与えることができます:
固定サイズのアロケーターについては、ネット上のあらゆる場所で頻繁に議論されています
小さなオブジェクトの割り当ては、2001年にAlexandrescuによって彼の完璧な本「Modern c ++ design」で紹介されました。
(ソースコードが配布された)大きな進歩は、Dimitar Lazarovによって書かれた「High Performance Heap allocator」という名前のGame Programming Gem 7(2008)の驚くべき記事にあります。
リソースのすばらしいリストはこの記事で見つけることができます
noobの役に立たないアロケータを自分で書き始めないでください...最初に自分自身を文書化してください。
C ++のメモリ管理で一般的になっている1つの手法はRAIIです。基本的には、コンストラクター/デストラクターを使用してリソース割り当てを処理します。もちろん、例外の安全性のためにC ++には他にも不愉快な詳細がありますが、基本的な考え方はかなり単純です。
問題は一般的に所有権の1つに帰着します。Scott MeyersによるEffective C ++シリーズとAndrei AlexandrescuによるModern C ++ Designを読むことを強くお勧めします。
リークを防ぐ方法はすでにたくさんありますが、リークの追跡に役立つツールが必要な場合は、以下をご覧ください。
これらのバグのよくある原因は、オブジェクトへの参照またはポインタを受け入れるが所有権が不明確なメソッドがある場合です。スタイルとコメントの規則により、これが起こりにくくなる場合があります。
関数がオブジェクトの所有権を取得する場合を特殊なケースとします。これが発生するすべての状況で、ヘッダーファイルの関数の横に、これを示すコメントを必ず記述してください。ほとんどの場合、オブジェクトを割り当てるモジュールまたはクラスが、オブジェクトの割り当てを解除する責任を負うように努める必要があります。
場合によっては、constを使用すると大きな効果があります。関数がオブジェクトを変更せず、戻り後も持続するオブジェクトへの参照を保存しない場合は、const参照を受け入れます。呼び出し元のコードを読むと、関数がオブジェクトの所有権を受け入れていないことが明らかになります。同じ関数で非constポインターを受け入れるようにすることもできます。呼び出し側は、呼び出し先が所有権を受け入れたと想定している場合と想定していない場合がありますが、const参照の場合は問題ありません。
引数リストでは非const参照を使用しないでください。呼び出し元のコードを読み取るときに、呼び出し先がパラメーターへの参照を保持していた可能性があることは非常に不明確です。
参照カウントポインターを推奨するコメントには同意しません。これは通常は正常に機能しますが、バグがあり機能しない場合、特にデストラクタがマルチスレッドプログラムなどで重要なことを行う場合は特にそうです。難しいことではない場合は、必ず参照カウントが不要になるように設計を調整してください。
重要度順のヒント:
-Tip#1デストラクタを「仮想」として宣言することを忘れないでください。
-Tip#2 RAIIを使用する
-Tip#3 boostのスマートポインターを使用する
-Tip#4独自のバグのあるスマートポインターを記述せずに、ブーストを使用します(現在進行中のプロジェクトでは、ブーストを使用できません。自分のスマートポインターをデバッグする必要があり、間違いなく取りません。再び同じルートですが、今度は依存関係にブーストを追加できません)
-Tip#5(数千のオブジェクトを持つゲームのように)カジュアル/非パフォーマンスが重要な場合は、Thorsten Ottosenのブーストポインターコンテナーを確認してください
-Tip#6 Visual Leak Detectionの「vld」ヘッダーなど、選択したプラットフォームのリーク検出ヘッダーを見つける
そもそもメモリリークを回避する方法(スマートポインタなど)について言及している人もいます。しかし、プロファイリングおよびメモリ分析ツールは、多くの場合、メモリの問題が発生した後、それを追跡する唯一の方法です。
Valgrind memcheckは優れた無料のソフトウェアです。
メモリ割り当て関数をインターセプトして、プログラムの終了時に解放されていないメモリゾーンがあるかどうかを確認できます(すべてのアプリケーションに適しているわけではありません)。
また、オペレーターの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の特定の説明を照合すると、「サウンド」が予想よりもはるかに多くのスペースを占めていることがわかります。
さまざまな場所での割り当てと破棄に関する唯一の例の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 ++のメモリ管理は簡単ですか?乾杯、
えま!
他のリソース(ハンドル、ファイル、db接続、ソケットなど)を管理するのと同じ方法でメモリを管理します。GCもそれらを支援しません。
任意の関数からの正確な1つの戻り。そうすることで、割り当てを解除して、見逃すことはありません。
それ以外の場合、間違いを犯すのは簡単です。
new a()
if (Bad()) {delete a; return;}
new b()
if (Bad()) {delete a; delete b; return;}
... // etc.