多くの無邪気に見えるものはC++では未定義の動作であることがわかります。たとえば、null以外のポインタを出力すると、そのポインタ値は未定義の動作になりdelete
ます。
現在、メモリリークは間違いなく悪いです。しかし、それらはどのようなクラスの状況ですか?定義済み、未定義、または他のどのクラスの動作ですか?
多くの無邪気に見えるものはC++では未定義の動作であることがわかります。たとえば、null以外のポインタを出力すると、そのポインタ値は未定義の動作になりdelete
ます。
現在、メモリリークは間違いなく悪いです。しかし、それらはどのようなクラスの状況ですか?定義済み、未定義、または他のどのクラスの動作ですか?
未定義の動作はありません。メモリをリークすることは完全に合法です。
未定義の動作:標準が特に定義したくないアクションであり、実装に任せているため、標準を破ることなく特定のタイプの最適化を柔軟に実行できます。
メモリ管理は明確に定義されています。
メモリを動的に割り当て、解放しない場合。その後、メモリは、適切と思われる場合に管理するアプリケーションのプロパティのままになります。あなたが記憶のその部分へのすべての参照を失ったという事実はここにもそこにもありません。
もちろん、リークが続くと、最終的には使用可能なメモリが不足し、アプリケーションはbad_alloc例外をスローし始めます。しかし、それは別の問題です。
メモリリークはC/C++で明確に定義されています。
私が行った場合:
int *a = new int[10];
に続く
a = new int[10];
最初に割り当てられたアレイにアクセスする方法がなく、GCがサポートされていないため、このメモリが自動的に解放されないため、メモリリークが発生しています。
ただし、このリークの結果は予測不可能であり、アプリケーションごとに、また同じ特定のアプリケーションのマシンごとに異なります。あるマシンでリークが原因でクラッシュしたアプリケーションが、RAMが多い別のマシンでも問題なく動作する可能性があるとします。また、特定のマシン上の特定のアプリケーションでは、リークによるクラッシュが実行中のさまざまな時点で発生する可能性があります。
メモリをリークすると、何も起こらないかのように実行が続行されます。これは定義された動作です。
将来的には、使用可能なメモリが不足しているためにへの呼び出しが失敗する場合があります。malloc
しかし、これはの定義された動作でmalloc
あり、結果も明確に定義されてい ます。malloc
呼び出しはを返しますNULL
。
これにより、結果をチェックしないプログラムがmalloc
セグメンテーション違反で失敗する可能性があります。しかし、その未定義の動作は(言語仕様のPOVから)プログラムが無効なポインターを逆参照しているためであり、以前のメモリリークや失敗したmalloc
呼び出しによるものではありません。
この声明の私の解釈:
自明でないデストラクタを持つクラスタイプのオブジェクトの場合、オブジェクトが占有するストレージが再利用または解放される前に、プログラムがデストラクタを明示的に呼び出す必要はありません。ただし、デストラクタへの明示的な呼び出しがない場合、または削除式(5.3.5)を使用してストレージを解放しない場合、デストラクタは暗黙的に呼び出されないものとし、デストラクタによって生成される副作用に依存するプログラムはありません。未定義の動作があります。
以下のとおりであります:
メモリを占有しているオブジェクトのデストラクタを呼び出さずに、オブジェクトが占有しているストレージを なんとかして解放した場合、デストラクタが重要で副作用がある場合は、UBが結果になります。
new
で割り当てた場合malloc
、rawストレージはで解放されfree()
、デストラクタは実行されず、UBが発生します。または、ポインタが無関係のタイプにキャストされて削除された場合、メモリは解放されますが、間違ったデストラクタ、UBが実行されます。
delete
これは、基になるメモリが解放されない省略されたものと同じではありません。省略delete
はUBではありません。
(「ヘッズアップ:この回答はここに移動されました。メモリリークは未定義の動作を引き起こしますか?」-この回答の適切な背景を取得するには、おそらくその質問を読む必要がありますO_o)。
規格のこの部分では、次のことが明示的に許可されているように思われます。
オブジェクトを配置するカスタムメモリプールを用意し、オブジェクトデストラクタの副作用に依存しない限りnew
、デストラクタの呼び出しに時間を費やすことなく、すべてを解放/再利用します。
少しのメモリを割り当て、決して解放しないライブラリそれらのアクセスが発生するたびに「フェニックス」のような再生。
副作用に依存しているときに標準が動作を未定義のままにする理由を理解できません-単にそれらの副作用が発生しないと言って、プログラムに通常の予想どおりの動作を定義または未定義にするのではありませんその前提。
規格が言っていることは未定義の振る舞いであるとまだ考えることができます。重要な部分は次のとおりです。
「デストラクタによって生成される副作用に応じて、未定義の動作があります。」
規格§1.9/12では、副作用を次のように明示的に定義しています(以下のイタリック体は規格であり、正式な定義の導入を示しています)。
glvalue(3.10)で指定されたオブジェクトへのアクセス、オブジェクトの
volatile
変更、ライブラリI / O関数の呼び出し、またはこれらの操作のいずれかを実行する関数の呼び出しはすべて副作用であり、実行環境の状態の変化です。
プログラムには依存関係がないため、未定義の動作はありません。
未定義動作の必要性または原因が明らかでない、§3.8p4のシナリオとほぼ間違いなく一致する依存関係の1つの例は次のとおりです。
struct X
{
~X() { std::cout << "bye!\n"; }
};
int main()
{
new X();
}
人々が議論している問題は、X
上記のオブジェクトが3.8 p4の目的で考慮released
されるかどうかです。これは、おそらくプログラムの終了後にOSにリリースされるだけであるためです。標準を読んでも、プロセスの「存続期間」のその段階が標準の動作要件の範囲(標準をすばやく検索しても、これは明確になりませんでした)。3.8p4がここに適用されることは個人的に危険です。これは、プログラマーの作成者がこのシナリオで未定義の動作を許可する資格があると主張するのに十分曖昧である限り、上記のコードがシナリオのリリースを簡単に構成しない場合でも、修正されたアラ...
int main()
{
X* p = new X();
*(char*)p = 'x'; // token memory reuse...
}
とにかく、メインが実装した上記のデストラクタには、「ライブラリI/O関数の呼び出し」ごとに副作用があります。さらに、プログラムの観察可能な動作は、デストラクタが実行された場合に影響を受けるバッファが終了時にフラッシュされるという意味で、ほぼ間違いなくプログラムに「依存」します。しかし、「副作用に依存する」とは、デストラクタが実行されなかった場合にプログラムが明らかに未定義の動作をする状況をほのめかすことだけを意味するのでしょうか。特に後者の場合は、動作が未定義であることを文書化するために標準の専用の段落を必要としないので、私は前者の側で誤りを犯します。明らかに未定義の動作の例を次に示します。
int* p_;
struct X
{
~X() { if (b_) p_ = 0; else delete p_; }
bool b_;
};
X x{true};
int main()
{
p_ = new int();
delete p_; // p_ now holds freed pointer
new (&x){false}; // reuse x without calling destructor
}
x
のデストラクタが終了中に呼び出されるとb_
、false
は、すでに解放されたポインタに対して呼び出され、~X()
したがって、未定義の動作を作成します。再利用する前に呼び出されていたdelete p_
場合は、0に設定され、削除は安全でした。その意味で、プログラムの正しい振る舞いはデストラクタに依存していると言え、振る舞いは明らかに未定義ですが、動作を結果にするのではなく、3.8p4の記述された振る舞い自体に一致するプログラムを作成しただけです。 3.8p4の...?x.~X();
p_
問題のあるより洗練されたシナリオ(コードを提供するには長すぎる)には、たとえば、I/Oのフラッシュやバックグラウンドスレッドの結合などの処理をトリガーするために0をヒットする必要があるファイルストリームオブジェクト内に参照カウンターがある奇妙なC++ライブラリが含まれる場合があります。これらのことを行わないと、デストラクタによって明示的に要求された出力を実行できないだけでなく、ストリームから他のバッファリングされた出力を出力できない、またはトランザクションファイルシステムを備えた一部のOSで、以前のI/Oのロールバックが発生する可能性があります-このような問題は、観察可能なプログラムの動作を変更したり、プログラムをハングさせたままにする可能性があります。
注:既存のコンパイラ/システムで奇妙に動作する実際のコードがあることを証明する必要はありません。Standardは、コンパイラが未定義の動作をする権利を明確に留保しています...それだけが重要です。これはあなたが推論して標準を無視することを選択できるものではありません-C++14または他のリビジョンがこの規定を変更する可能性がありますが、それがそこにある限り、おそらく副作用への「依存」さえあれば未定義の動作の可能性があります(もちろん、それ自体が特定のコンパイラ/実装によって定義されることが許可されているため、すべてのコンパイラが奇妙なことをする義務があることを自動的に意味するわけではありません)。
言語仕様には、「メモリリーク」については何も記載されていません。言語の観点から、ダイナミックメモリにオブジェクトを作成するときは、まさにそれを実行します。つまり、ライフタイム/ストレージ期間が無制限の匿名オブジェクトを作成します。この場合の「無制限」とは、オブジェクトの割り当てを明示的に解除した場合にのみ、オブジェクトの存続期間/保存期間を終了できることを意味しますが、それ以外の場合は、(プログラムが実行されている限り)永久に存続します。
現在、動的に割り当てられたオブジェクトは、プログラム実行の時点で、そのオブジェクトへのすべての参照(ポインタなどの一般的な「参照」)が失われて回復不能になると、「メモリリーク」になると通常考えられます。人間にとってさえ、「すべての参照が失われる」という概念はあまり正確に定義されていないことに注意してください。オブジェクトの一部への参照があり、理論的にはオブジェクト全体への参照に「再計算」できる場合はどうなりますか?メモリリークですか?オブジェクトへの参照がまったくないが、プログラムで利用可能な他の情報(割り当ての正確なシーケンスなど)を使用して、どういうわけかそのような参照を計算できる場合はどうなりますか?
言語仕様は、そのような問題には関係ありません。プログラムでの「メモリリーク」の出現をどのように考えても、言語の観点からは、それはまったくイベントではありません。言語の観点からは、「リークされた」動的に割り当てられたオブジェクトは、プログラムが終了するまで幸せに生き続けます。これが唯一残っている懸念事項です。プログラムが終了し、動的メモリがまだ割り当てられている場合はどうなりますか?
私の記憶が正しければ、言語は、プログラムの終了時にまだ割り当てられている動的メモリに何が起こるかを指定していません。ダイナミックメモリに作成したオブジェクトを自動的に破棄/割り当て解除する試みは行われません。しかし、そのような場合、正式な未定義の動作はありません。
立証責任は、メモリリークがC++UBである可能性があると考える人にあります。
当然、証拠は提示されていません。
要するに、疑念を抱く人は誰でも、この質問を明確に解決することはできません。ただし、ジャスティンビーバーの大音量の音楽などで委員会を非常に確実に脅迫し、UBではないことを明確にするC++14ステートメントを追加する場合を除きます。
問題はC++11§3.8/4です:
自明でないデストラクタを持つクラスタイプのオブジェクトの場合、プログラムは、オブジェクトが占有するストレージが再利用または解放される前に、デストラクタを明示的に呼び出す必要はありません。ただし、デストラクタへの明示的な呼び出しがない場合、または削除式(5.3.5)を使用してストレージを解放しない場合、デストラクタは暗黙的に呼び出されないものとし、デストラクタによって生成される副作用に依存するプログラムはありません。未定義の動作があります。
この一節は、C++98とC++03でまったく同じ表現でした。どういう意味ですか?
プログラムは、オブジェクトが占有するストレージが再利用または解放される前に、デストラクタを明示的に呼び出す必要はありません
。つまり、既存のオブジェクトを最初に破棄することなく、変数のメモリを取得してそのメモリを再利用できます。
デストラクタへの明示的な呼び出しがない場合、または削除式(5.3.5)を使用してストレージを解放しない場合、デストラクタは暗黙的に呼び出され
ません。つまり、メモリを再利用する前に既存のオブジェクトを破棄しない場合、次に、オブジェクトがそのデストラクタが自動的に呼び出されるようなものである場合(たとえば、ローカル自動変数)、そのデストラクタは存在しないオブジェクトを操作するため、プログラムは未定義の動作をします。
また、デストラクタによって生成される副作用に依存するプログラムの動作は未定義
です。プログラムは常に副作用の定義によって副作用に依存するため、文字通りの動作を意味することはできません。言い換えれば、プログラムが副作用に依存しない方法はありません。なぜなら、それらは副作用ではないからです。
おそらく意図されていたのは、最終的にC ++ 98に移行したものではなかったため、手元にあるのは欠陥です。
T
コンテキストから、プログラムが静的に既知のタイプのオブジェクトの自動破棄に依存している場合、メモリが再利用されてオブジェクトではないオブジェクトを作成したT
場合、それは未定義動作であると推測できます。
解説を読んだ人は、上記の「しなければならない」という言葉の説明は、私が以前に想定した意味ではないことに気付くかもしれません。私が今見ているように、「しなければならない」ことは実装の要件ではなく、それが何をすることが許されているかです。これはプログラムの要件であり、コードで実行できることです。
したがって、これは正式にはUBです。
auto main() -> int
{
string s( 666, '#' );
new( &s ) string( 42, '-' ); // <- Storage reuse.
cout << s << endl;
// <- Formal UB, because original destructor implicitly invoked.
}
しかし、これは文字通りの解釈で問題ありません。
auto main() -> int
{
string s( 666, '#' );
s.~string();
new( &s ) string( 42, '-' ); // <- Storage reuse.
cout << s << endl;
// OK, because of the explicit destruction of the original object.
}
主な問題は、上記の標準の段落の文字通りの解釈では、元のオブジェクトが明示的に破壊されたという理由だけで、新しい配置が異なるタイプのオブジェクトをそこに作成した場合でも、正式にOKであるということです。しかし、その場合、実際にはあまり問題ありません。たぶん、これは標準の他の段落でカバーされているので、正式にはUBでもあります。
そして、これもOKです。new
からの配置を使用します<new>
:
auto main() -> int
{
char* storage = new char[sizeof( string )];
new( storage ) string( 666, '#' );
string const& s = *(
new( storage ) string( 42, '-' ) // <- Storage reuse.
);
cout << s << endl;
// OK, because no implicit call of original object's destructor.
}
私が見ているように–今。
その明確に定義された動作。
サーバーが稼働していて、ヒープメモリを割り当て続け、使用しなくてもメモリが解放されない場合を考えてみます。したがって、最終的にはサーバーのメモリが不足し、間違いなくクラッシュが発生します。
他のすべての答えに加えて、いくつかのまったく異なるアプローチ。§5.3.4-18のメモリ割り当てを見ると、次のことがわかります。
上記のオブジェクト初期化のいずれかの部分が例外をスローして終了し、適切な割り当て解除関数が見つかった場合、割り当て解除関数が呼び出されて、オブジェクトが構築されていたメモリが解放されます。その後、例外はnew-expressionのコンテキスト。明確に一致する割り当て解除関数が見つからない場合、例外を伝播してもオブジェクトのメモリは解放されません。[注:これは、呼び出された割り当て関数がメモリを割り当てない場合に適しています。そうしないと、メモリリークが発生する可能性があります。—エンドノート]
ここでUBが発生するのでしょうか、言及されるので、「単なるメモリリーク」です。
§20.6.4-10のような場所では、ガベージコレクターとリークディテクターの可能性について言及されています。安全に導出されたポインタなどの概念には多くの考慮が払われています。ガベージコレクターでC++を使用できるようにする(C.2.10「ガベージコレクションされた領域の最小限のサポート」)。
したがって、あるオブジェクトへの最後のポインタを失うだけでUBになる場合、すべての努力は意味がありません。
「デストラクタがUBを実行しないという副作用がある場合」については、これは間違っていると思います。そうでなければ、そのような施設std::quick_exit()
も本質的にUBになります。
スペースシャトルが2分で離陸する必要があり、メモリリークのあるコードと未定義の動作のコードのどちらかを選択できる場合は、メモリリークのあるコードを入力します。
しかし、私たちのほとんどは通常そのような状況にありません、そして私たちがそうであるならば、それはおそらくさらに上の失敗によるものです。おそらく私は間違っていますが、私はこの質問を「どの罪が私を地獄に早く入れるのか」と読んでいます。
おそらく未定義の動作ですが、実際には両方です。
定義されているように、メモリリークは、自分の後でクリーンアップするのを忘れているためです。
もちろん、メモリリークは、後で未定義の動作を引き起こす可能性があります。
率直な答え:この標準は、メモリをリークしたときに何が起こるかを定義していないため、「未定義」です。ただし、これは暗黙的に未定義です。これは、標準で明示的に未定義のものよりも興味深いものではありません。
これは明らかに未定義の動作であってはなりません。UBはある時点で発生する必要があり、メモリの解放やデストラクタの呼び出しを忘れることは、どの時点でも発生しません。何が起こるかというと、プログラムはメモリを解放したり、デストラクタを呼び出したりすることなく終了します。これにより、プログラムの動作やその終了が未定義になることはありません。
そうは言っても、私の意見では、この箇所では基準が矛盾しています。一方では、このシナリオでデストラクタが呼び出されないことを保証し、他方では、プログラムがデストラクタによって生成された副作用に依存している場合、それは未定義の動作をしていると言います。デストラクタがを呼び出したとすると、デストラクタを呼び出すexit
ことの副作用により、デストラクタが他の方法で実行することを実行できなくなるため、何かを実行するプログラムがそれから独立しているふりをすることはできません。しかし、テキストは、プログラムが邪魔されずに作業を続行できるように、デストラクタが呼び出されないことも保証します。この一節の終わりを読む唯一の合理的な方法は、プログラムの適切な動作が必要な場合、呼び出されるデストラクタの場合、動作は実際には定義されていません。デストラクタが呼び出されないことが規定されたばかりであることを考えると、これは不必要な発言です。
未定義の動作とは、何が起こるかが定義されていないか、不明であることを意味します。メモリリークの動作は、C / C ++で、使用可能なメモリを使い果たしてしまうことが明確に知られています。ただし、結果として生じる問題は、ゲームオーバーで説明されているように、常に定義および変更できるとは限りません。