この C++ コードはメモリ リークを引き起こしますか (配列の新規キャスト)
-
09-06-2019 - |
質問
私は、可変長構造 (TAPI) を使用するレガシー C++ コードに取り組んできました。構造サイズは可変長文字列に依存します。構造体は配列のキャストによって割り当てられます new
したがって:
STRUCT* pStruct = (STRUCT*)new BYTE [sizeof(STRUCT) + nPaddingSize];
ただし、後でメモリは次のコマンドを使用して解放されます。 delete
電話:
delete pStruct;
この配列の組み合わせは new []
および非配列 delete
メモリリークの原因になりますか、それともコンパイラに依存しますか?このコードを変更して使用したほうがよいでしょうか malloc
そして free
その代わり?
解決
技術的には、アロケータの不一致による問題が発生する可能性があると考えていますが、実際には、この例で正しい動作をしないコンパイラを私は知りません。
さらに重要なのは、 STRUCT
デストラクターがある場所 (または与えられた場所) では、対応するコンストラクターを呼び出さずにデストラクターが呼び出されます。
もちろん、pStruct がどこから来たのかがわかっている場合は、割り当てに一致するように削除時にそれをキャストしてみてはいかがでしょうか。
delete [] (BYTE*) pStruct;
他のヒント
個人的には使った方が良いと思います std::vector
メモリを管理するため、 delete
.
std::vector<BYTE> backing(sizeof(STRUCT) + nPaddingSize);
STRUCT* pStruct = (STRUCT*)(&backing[0]);
支持が範囲を離れると、 pStruct
はもう無効です。
または、以下を使用することもできます。
boost::scoped_array<BYTE> backing(new BYTE[sizeof(STRUCT) + nPaddingSize]);
STRUCT* pStruct = (STRUCT*)backing.get();
または boost::shared_array
所有権を移動する必要がある場合。
はい、メモリリークが発生します。
C++ Gotchas 以外のこれを参照してください。 http://www.informit.com/articles/article.aspx?p=30642 なぜだろう。
Raymond Chen がベクトルをベクトルする方法について説明しています。 new
そして delete
Microsoft コンパイラの内部にあるスカラー バージョンとは異なります...ここ:http://blogs.msdn.com/oldnewthing/archive/2004/02/03/66660.aspx
私見では、削除を次のように修正する必要があります。
delete [] pStruct;
に切り替えるのではなく malloc
/free
, 間違いを犯さずに簡単に変更できるという理由だけであれば ;)
そしてもちろん、上で示した変更を加えるのがより簡単な方法は、元の割り当てでのキャストが原因で間違っています。
delete [] reinterpret_cast<BYTE *>(pStruct);
なので、おそらく切り替えるのは簡単だと思います malloc
/free
結局 ;)
コードの動作は未定義です。運が良ければ(あるいはそうでなくても)コンパイラで動作するかもしれませんが、実際にはそれは正しいコードではありません。これには 2 つの問題があります。
- の
delete
配列である必要がありますdelete []
. - の
delete
割り当てられた型と同じ型へのポインタで呼び出す必要があります。
したがって、完全に正しくするには、次のようなことをしたいとします。
delete [] (BYTE*)(pStruct);
C++ 標準には次のように明確に記載されています。
delete-expression:
::opt delete cast-expression
::opt delete [ ] cast-expression
最初の選択肢は非配列オブジェクト用で、2 つ目は配列用です。オペランドはポインタ型、またはポインタ型への単一の変換関数(12.3.2)を持つクラス型を持つ必要があります。結果の型は void になります。
最初の選択肢 (オブジェクトの削除) では、delete のオペランドの値は非配列オブジェクトへのポインタでなければなりません [...] そうでない場合、動作は未定義です。
のオペランドの値 delete pStruct
の配列へのポインタです char
, 、その静的型とは独立しています (STRUCT*
)。したがって、コードの形式が正しくなく、この場合は適切な実行可能ファイルを生成するために C++ コンパイラが必要ないため、メモリ リークに関する議論は全く無意味です。
メモリリークを起こす可能性もあれば、リークできない可能性もあれば、システムをクラッシュさせる可能性もあります。実際、コードをテストした C++ 実装では、削除式の時点でプログラムの実行が中止されます。
他の投稿で強調されているように:
1) new/delete の呼び出しはメモリを割り当て、コンストラクター/デストラクターを呼び出す場合があります (C++ '03 5.3.4/5.3.5)
2) 配列バージョンと非配列バージョンの混合 new
そして delete
未定義の動作です。(C++ '03 5.3.5/4)
ソースを見ると、誰かが検索して置換したようです malloc
そして free
そして上記がその結果です。C++ にはこれらの関数を直接置き換えるものがあり、それは割り当て関数を呼び出すことです。 new
そして delete
直接:
STRUCT* pStruct = (STRUCT*)::operator new (sizeof(STRUCT) + nPaddingSize);
// ...
pStruct->~STRUCT (); // Call STRUCT destructor
::operator delete (pStruct);
STRUCT のコンストラクターを呼び出す必要がある場合は、メモリを割り当ててから配置を使用することを検討できます。 new
:
BYTE * pByteData = new BYTE[sizeof(STRUCT) + nPaddingSize];
STRUCT * pStruct = new (pByteData) STRUCT ();
// ...
pStruct->~STRUCT ();
delete[] pByteData;
@eric - コメントありがとうございます。でも、あなたは何か言い続けて、私をイライラさせます。
これらのランタイムライブラリは、OSの独立した一貫した構文でOSへのメモリ管理コールを処理し、それらのランタイムライブラリは、Linux、Windows、Solaris、AIXなどのOS間でMallocと新しい作業を一貫して作る責任があります... 。
本当じゃない。たとえば、コンパイラ作成者は std ライブラリの実装を提供し、それらを OS に完全に自由に実装できます。 依存 方法。たとえば、malloc への巨大な呼び出しを 1 回行うだけで、ブロック内のメモリを自由に管理できます。
stdなどのAPIなので互換性があります。は同じです。ランタイム ライブラリがすべて向きを変えて、まったく同じ OS 呼び出しを呼び出すからではありません。
new および delete というキーワードのさまざまな使用法が考えられ、かなりの混乱が生じているようです。C++ で動的オブジェクトを構築するには、常に 2 つの段階があります。raw メモリの割り当てと、割り当てられたメモリ領域内での新しいオブジェクトの構築。オブジェクトの存続期間の反対側には、オブジェクトの破棄と、オブジェクトが存在していたメモリ位置の割り当て解除があります。
多くの場合、これら 2 つのステップは 1 つの C++ ステートメントによって実行されます。
MyObject* ObjPtr = new MyObject;
//...
delete MyObject;
上記の代わりに、C++ の生のメモリ割り当て関数を使用できます。 operator new
そして operator delete
および明示的な構築(配置による) new
) と破棄して、同等の手順を実行します。
void* MemoryPtr = ::operator new( sizeof(MyObject) );
MyObject* ObjPtr = new (MemoryPtr) MyObject;
// ...
ObjPtr->~MyObject();
::operator delete( MemoryPtr );
キャストが関与していないこと、および割り当てられたメモリ領域に 1 種類のオブジェクトのみが構築されることに注目してください。次のようなものを使用して new char[N]
raw メモリを割り当てる方法は、論理的には技術的に間違っています。 char
オブジェクトは新しく割り当てられたメモリに作成されます。これが「正常に機能しない」状況を私は知りませんが、生のメモリ割り当てとオブジェクト作成の区別があいまいになるため、使用しないことをお勧めします。
この特定のケースでは、次の 2 つのステップを分離しても何のメリットもありません。 delete
ただし、初期割り当てを手動で制御する必要があります。上記のコードは「すべてが機能する」シナリオで動作しますが、コンストラクターが次の場合に生のメモリをリークします。 MyObject
例外をスローします。これは、割り当ての時点で例外ハンドラーを使用して捕捉して解決することもできますが、完全な構築を配置 new 式で処理できるように、カスタム演算子 new を提供する方がおそらくより適切です。
class MyObject
{
void* operator new( std::size_t rqsize, std::size_t padding )
{
return ::operator new( rqsize + padding );
}
// Usual (non-placement) delete
// We need to define this as our placement operator delete
// function happens to have one of the allowed signatures for
// a non-placement operator delete
void operator delete( void* p )
{
::operator delete( p );
}
// Placement operator delete
void operator delete( void* p, std::size_t )
{
::operator delete( p );
}
};
ここには微妙な点がいくつかあります。クラス インスタンスに十分なメモリとユーザー指定可能なパディングを割り当てることができるように、クラスの配置を新しく定義します。これを行うため、メモリ割り当ては成功しても構築が失敗した場合に、割り当てられたメモリが自動的に割り当て解除されるように、一致する配置の削除を行う必要があります。残念ながら、プレースメントの削除の署名は、プレースメント以外の削除で許可されている 2 つの署名の 1 つと一致するため、実際のプレースメントの削除がプレースメントの削除として扱われるように、別の形式の非プレースメントの削除を提供する必要があります。(プレースメントの new とプレースメントの削除の両方に追加のダミー パラメーターを追加することでこの問題を回避できたかもしれませんが、これではすべての呼び出しサイトで追加の作業が必要になります。)
// Called in one step like so:
MyObject* ObjectPtr = new (padding) MyObject;
単一の新しい式を使用することで、新しい式の一部がスローされた場合でもメモリ リークが発生しないことが保証されるようになりました。
オブジェクトの有効期間のもう一方の端では、演算子 delete を定義したため (定義していなかったとしても、オブジェクトのメモリは元々グローバル演算子 new から取得されていました)、動的に作成されたオブジェクトを破棄する正しい方法は次のとおりです。 。
delete ObjectPtr;
まとめ!
見てください、キャストはいません!
operator new
そしてoperator delete
raw メモリを処理し、new を配置すると raw メモリ内にオブジェクトを構築できます。からの明示的なキャストvoid*
オブジェクト ポインタへの変更は、たとえそれが「正常に機能する」場合でも、通常は論理的に何かが間違っていることを示しています。new[] と delete[] は完全に無視されました。これらの可変サイズのオブジェクトは、いかなる場合でも配列では機能しません。
new を配置すると、新しい式がリークしないようにできます。新しい式は、引き続き、破棄が必要なオブジェクトへのポインタと割り当て解除が必要なメモリとして評価されます。ある種のスマート ポインターを使用すると、他の種類の漏洩を防ぐことができる場合があります。プラス面としては、プレーンを実現しました
delete
これを行う正しい方法であるため、ほとんどの標準的なスマート ポインターが機能します。
もし、あんたが 本当に このようなことをしなければならないので、おそらくオペレーターに電話する必要があります new
直接:
STRUCT* pStruct = operator new(sizeof(STRUCT) + nPaddingSize);
この方法で呼び出すと、コンストラクター/デストラクターの呼び出しが回避されると思います。
現在は投票できませんが、 スライスライムの答え より好ましい ロブ・ウォーカーの答え, なぜなら、この問題はアロケータや STRUCT にデストラクタがあるかどうかとは何の関係もないからです。
また、サンプル コードが必ずしもメモリ リークを引き起こすわけではないことにも注意してください。これは未定義の動作です。ほとんどすべてのことが起こる可能性があります(何も悪いことが起こらないことから、はるか遠くでの衝突まで)。
サンプル コードでは、単純明快な未定義の動作が発生します。スライスライムの答えは直接的かつ的を得ています(ベクトルは STL のものであるため、「ベクトル」という単語は「配列」に変更する必要があることに注意してください)。
この種のことは、C++ FAQ (セクション 16.12、16.13、および 16.14) でかなり詳しく説明されています。
http://www.parashift.com/c++-faq-lite/freestore-mgmt.html#faq-16.12
あなたが言及しているのは配列削除 ([]) であり、ベクトル削除ではありません。ベクターは std::vector であり、その要素の削除を処理します。
BYTE * にキャストバックして削除することもできます。
delete[] (BYTE*)pStruct;
はい、そうかもしれません。 new[] で割り当てますが、delelte で割り当てを解除するので、はい、ここでは malloc/free の方が安全ですが、C++ では (デ) コンストラクターを処理しないため、これらを使用しないでください。
また、コードはデコンストラクターを呼び出しますが、コンストラクターは呼び出しません。一部の構造体では、これによりメモリ リークが発生する可能性があります (コンストラクターが文字列などにさらにメモリを割り当てた場合)
これにより、コンストラクターとデコンストラクターも正しく呼び出されるので、正しく実行することをお勧めします。
STRUCT* pStruct = new STRUCT;
...
delete pStruct;
リソースの取得と解放のバランスを可能な限り保つことが常に最善です。この場合、漏れているかどうかは判断が難しいですが。これは、コンパイラーのベクトル (解放) 割り当ての実装に依存します。
BYTE * pBytes = new BYTE [sizeof(STRUCT) + nPaddingSize];
STRUCT* pStruct = reinterpret_cast< STRUCT* > ( pBytes ) ;
// do stuff with pStruct
delete [] pBytes ;
レン:問題は、pStruct が STRUCT* であるにもかかわらず、割り当てられたメモリが実際には未知のサイズの BYTE[] であることです。したがって、delete[] pStruct は、割り当てられたすべてのメモリの割り当てを解除しません。
C と C++ のやり方を混ぜ合わせたようなものですね。なぜ STRUCT のサイズを超えるサイズを割り当てるのでしょうか?なぜ単に「新しい STRUCT」ではないのでしょうか?これを行う必要がある場合は、malloc と free を使用する方が明確です。そうすることで、あなたや他のプログラマが、割り当てられたオブジェクトの型とサイズについて想定する可能性が少し低くなるからです。
@matt cruikshank delete []を呼び出さないことを提案したことは一度もないので、OSをクリーンアップさせることを提案したことはないので、私が書いたものをもう一度読んでください。そして、C++ ランタイム ライブラリがヒープを管理しているという点では間違っています。もしそうなら、C++ は現在のように移植性がなく、クラッシュしたアプリケーションが OS によってクリーンアップされることはありません。(C/C++ が移植性がないと思われる OS 固有のランタイムがあることを認めます)。kernel.org の Linux ソースから stdlib.h を見つけてください。C++ の新しいキーワードは、実際には malloc と同じメモリ管理ルーチンと通信しています。
C++ ランタイム ライブラリは OS システム コールを実行し、ヒープを管理するのは OS です。ランタイムライブラリがいつメモリを解放するかを示すという点では部分的に正しいですが、実際にはヒープテーブルを直接実行しません。つまり、リンク先のランタイムは、ヒープを移動して割り当てまたは割り当て解除するためのコードをアプリケーションに追加しません。これは、Windows、Linux、Solaris、AIX などの場合に当てはまります。これは、Linux のカーネル ソースで malloc が見つからない理由でもあり、Linux ソースで stdlib.h が見つからない理由でもあります。これらの最新のオペレーティング システムには仮想メモリ マネージャーがあり、これが事態をさらに複雑にすることを理解してください。
1G ボックス上の 2G RAM に対して malloc を呼び出しても、なぜ有効なメモリ ポインタが返されるのか不思議に思ったことはありませんか?
x86 プロセッサ上のメモリ管理は、3 つのテーブルを使用してカーネル空間内で管理されます。PAM (ページ割り当てテーブル)、PD (ページ ディレクトリ)、および PT (ページ テーブル)。これは私が話しているハードウェアレベルの話です。C++ アプリケーションではなく、OS メモリ マネージャーが行うことの 1 つは、起動中に BIOS 呼び出しを使用して、ボックスに搭載されている物理メモリの量を確認することです。OS は、アプリケーションが権限を持たないメモリにアクセスしようとした場合などの例外も処理します。(GPF 一般保護違反)。
マット、私たちも同じことを言っているのかもしれませんが、ボンネット内の機能について少し混乱しているのではないかと思います。私は生計のために C/C++ コンパイラを保守しています...
@ericmayo - クレープ。さて、VS2005 を実験してみたところ、vector new によって行われたメモリ上のスカラー削除からの正直なリークを取得できませんでした。ここでのコンパイラの動作は「未定義」であると思います。これが私が集められる最善の防御策だと思います。
ただし、元の投稿者の言うことを実行するのは本当にひどい行為であることを認めなければなりません。
その場合、C ++は今日のようにポータブルではなく、クラッシュするアプリケーションはOSによってクリーンアップされることはありません。
ただし、この論理は実際には当てはまりません。私の主張は、コンパイラのランタイムは、OS が返すメモリ ブロック内のメモリを管理できるということです。これがほとんどの仮想マシンの動作方法であるため、この場合の移植性に対するあなたの議論はあまり意味がありません。
@マット・クルックシャンク
「そうですね、VS2005 を実験してみたところ、vector new によって行われたメモリ上のスカラー削除からの正直なリークは得られませんでした。ここでのコンパイラの動作は「未定義」であると思いますが、これが私が集められる最善の防御策だと思います。」
私は、それがコンパイラの動作、あるいはコンパイラの問題であるという意見には同意しません。ご指摘のように、「new」キーワードはコンパイルされてランタイム ライブラリにリンクされます。これらのランタイム ライブラリは、OS に依存しない一貫した構文で OS へのメモリ管理呼び出しを処理し、Linux、Windows、Solaris、AIX などの OS 間での malloc と new の動作を一貫して行う役割を果たします。 。これが、私が移植性の議論に言及した理由です。ランタイムが実際にはメモリを管理していないことを証明する試みです。
OSはメモリを管理します。
ランタイム ライブラリは OS へのインターフェイスです。Windows では、これは仮想メモリ マネージャー DLL です。これが、stdlib.h が Linux カーネル ソースではなく GLIB-C ライブラリ内に実装される理由です。GLIB-C が他の OS で使用されている場合、正しい OS 呼び出しを行うために malloc の実装が変更されます。VS、ボーランドなどで。実際にメモリを管理するコンパイラに同梱されているライブラリも見つかりません。ただし、malloc の OS 固有の定義が見つかります。
Linux のソースがあるので、そこに malloc がどのように実装されているかを見てみることができます。malloc が実際には GCC コンパイラに実装されていることがわかります。このコンパイラは、基本的に、カーネルに 2 つの Linux システム コールを実行してメモリを割り当てます。絶対に、malloc 自体が実際にメモリを管理することはありません。
そして私からそれを奪わないでください。Linux OS のソース コードを読むか、K&R がそれについて何と言っているかを見ることができます...ここに、C の K&R への PDF リンクがあります。
http://www.oberon2005.ru/paper/kr_c.pdf
149 ページの終わり近くを参照してください。「malloc と free の呼び出しは任意の順序で実行できます。Mallocは、必要に応じてより多くのメモリを取得するようオペレーティングシステムに呼びかけます。これらのルーチンは、比較的マシンに依存しない方法でマシンに依存するコードを記述する際の考慮事項の一部を示し、また、構造体、共用体、typedef の現実の応用例も示します。
「しかし、最初の投稿者の言うことを実行するのは本当にひどい行為であることを認めなければなりません。」
ああ、そこには同意しません。私が言いたいのは、元の投稿者のコードはメモリ リークを引き起こさないということでした。私が言っていたのはそれだけです。私は物事のベストプラクティスの側面には同意しませんでした。コードが delete を呼び出しているため、メモリが解放されています。
あなたの弁護として、元の投稿者のコードが一度も終了しなかった場合、または削除呼び出しに到達しなかった場合、コードにメモリ リークが発生する可能性があることに同意しますが、彼は後で削除が呼び出されるのが見えると述べているためです。「ただし、後で、削除呼び出しを使用してメモリが解放されます。」
さらに、私がこのように応答した理由は、OPのコメント「可変長構造(TAPI)、構造のサイズは可変長の文字列に依存する」ためでした。
そのコメントは、行われているキャストに対する割り当ての動的な性質に疑問を抱いており、その結果メモリ リークが発生するのではないかと疑問に思っているように聞こえました。言ってみれば、私は行間を読んでいたのです ;)。
上記の優れた回答に加えて、次のことも追加したいと思います。
コードが Linux で実行される場合、または Linux でコンパイルできる場合は、次のように実行することをお勧めします。 ヴァルグリンド. 。これは優れたツールであり、生成される無数の有用な警告の中で、メモリを配列として割り当てた後、非配列として解放したとき (またはその逆) を通知します。
演算子 new と delete を使用します。
struct STRUCT
{
void *operator new (size_t)
{
return new char [sizeof(STRUCT) + nPaddingSize];
}
void operator delete (void *memory)
{
delete [] reinterpret_cast <char *> (memory);
}
};
void main()
{
STRUCT *s = new STRUCT;
delete s;
}
メモリリークは無いと思います。
STRUCT* pStruct = (STRUCT*)new BYTE [sizeof(STRUCT) + nPaddingSize];
これはオペレーティング システム内でメモリ割り当て呼び出しに変換され、そのメモリへのポインタが返されます。メモリが割り当てられる時点でのサイズは、 sizeof(STRUCT)
そしてそのサイズ nPaddingSize
これは、基礎となるオペレーティング システムに対するメモリ割り当て要求を満たすために知られています。
したがって、割り当てられたメモリは、オペレーティング システムのグローバル メモリ割り当てテーブルに「記録」されます。メモリ テーブルにはポインタによってインデックスが付けられます。したがって、対応する削除呼び出しでは、最初に割り当てられていたすべてのメモリが解放されます。(メモリの断片化は、この分野でも人気のあるテーマです)。
C/C++ コンパイラがメモリを管理しているのではなく、基盤となるオペレーティング システムがメモリを管理していることがわかります。
もっときれいな方法があることに同意しますが、OPはこれがレガシーコードであると言いました。
つまり、受け入れられた回答ではメモリリークがあると考えられているため、メモリリークは見当たりません。
ロブ・ウォーカー 返事 いいね。
ちょっとした追加ですが、コンストラクターやディストラクターがなく、基本的に生のメモリのチャンクを割り当てて解放する必要がある場合は、free/malloc ペアの使用を検討してください。
ericmayo.myopenid.com は非常に間違っているので、十分な評判のある誰かが彼に反対票を投じるべきです。
C または C++ ランタイム ライブラリは、Eric さんの指摘と同じように、オペレーティング システムによってブロック単位で与えられるヒープを管理しています。しかし、それは は 開発者の責任は、メモリを解放するためにどのランタイム呼び出しを行うべきかをコンパイラに指示し、場合によってはそこにあるオブジェクトを破壊することです。この場合、C++ ランタイムがヒープを有効な状態のままにするために、Vector delete (別名 delete[]) が必要です。PROCESS が終了すると、OS が基礎となるメモリ ブロックの割り当てを解除するほど賢明であるという事実は、開発者が依存すべきものではありません。これは、delete をまったく呼び出さないようなものです。