• ベストアンサー

結局 deleteしないとダメ? ファイナライザー万能主義は理解不足?

安直に後始末はファイナライザ部に書いておけば、OKと思っていましたが、実行させると実行時エラー?になり正しくファイルに書き込めていません。リソースを解放するタイミングが理解できていないです。 書き込んでcloseすると確実ですが、何度もopenしないといけないので、オブジェクトの有効期間開きっぱなしで処理させて、オブジェクト消滅と共にクローズさせたいです。 以下、無理やり動作させたソースです。 この考え方は正しいでしょうか? /* ファイナライザに後始末処理を入れておけば すべてうまくいくと理解していたのですが そんな単純なものではないのでしょうか? */ #using <system.dll> using namespace System; using namespace System::IO; ref class Test_A { StreamWriter^ sw; // ファイルのオープン void open(void){ try { sw = gcnew StreamWriter( "test.log", true); } catch ( Exception^ ex) { Console::WriteLine("オープンエラー"); } } // ファイルのクローズ void close(){ if (sw){ sw->Close(); } } public: // コンストラク Test_A(){ sw = nullptr; open(); } // デストラクタ ~Test_A(){ this->!Test_A(); // ← これを書き、delete a;とすると、正しく書き込まれて変な例外ダイアログメッセージが表示されなくなる。 } // ファイナライザー !Test_A(){ close(); // ファイナライザで後処理すれば、万事OKではない?? } // ファイルの書き込み void Write(String^ log){ try { sw->Write(log); } catch ( Exception^ ex ) { Console::WriteLine("書き込みでエラー"); } } }; //------------------------------------- // メイン //------------------------------------- int main(void){ Test_A^ a = gcnew Test_A(); a->Write("delete aと、~Test_Aに処理を施さないと、正しく書き込めない。\n"); delete a; // ←これが無いと、変なダイアログメッセージが表示される、しかも正しく書き込めない。 return 0; }

質問者が選んだベストアンサー

  • ベストアンサー
  • chie65536
  • ベストアンサー率41% (2512/6032)
回答No.1

C++のnew(gcnew)文は ・そのクラス変数に必要なメモリをmallocする ・mallocが出来たら、クラス内の変数を初期化する ・クラス変数内の関数アドレスを初期化する ・クラス変数内のコンストラクタ関数を呼ぶ。引数の記述があればそれを渡す ・クラス変数そのもののアドレス、つまりmallocが返したアドレスをnew文の値とする と言う動作をします。 なので Test_A^ a = gcnew Test_A(); と書くと、クラス変数aに「mallocで返されたアドレス」が代入される事になります。 変数aにアドレスが入ってるからこそ、構造体のポインタ変数でのメンバ参照に「->」を使うのと同じく a->Write(ほげほげ) のように「アロー演算子」を使うのです。 C++のdelete文は ・指定されたクラスのデストラクタ関数を呼ぶ ・指定された変数を「ポインタとして」freeを呼び、実体(mallocしたメモリ)を開放する と言う事をします。 ここまでを理解できたら、問題がどこにあるか判りますか? 「mallocしたメモリは必ずfreeしなければならない」のは知ってますね? つまり「new(gcnew)した物は、必ずdeleteしなければならない」のです。 ソースを見ると、ストリームの実体「sw」は sw = gcnew StreamWriter( "test.log", true); でnew(gcnew)されてますが、deleteしている所が何処にもありません。 ここでメモリリーク(確保したまま永久に開放されないメモリ)が起きます。 クローズルーチンは void close(){  if (sw) {   sw->Close();   delete sw;   sw = nullptr;  } } としましょう。 あと、2重オープンされると sw = gcnew StreamWriter( "test.log", true); の文が「前のswを上書きする」ので、やはりメモリリークの原因になります。ここも void open(void){  if (!sw) {   try {    sw = gcnew StreamWriter( "test.log", true);   } catch ( Exception^ ex) {    Console::WriteLine("オープンエラー");   }  } } と言う感じで「2重に呼んだら何もしない」ようにしましょう。 あと、ストリームは「書きかけのデータはバッファに溜められ、バッファに溜まったまま吐き出されてないデータは、プログラムがアボートすると書きこまれずに失われる」ので注意しましょう。 正しく書きこめないのは「バッファがフラッシュされてファイルに書き出される前に、メモリリークが原因でmainからランタイムライブラリに制御が移った後に例外が起きて、プログラムがアボートしたから」です。 「newったら必ずdeleteる」「newった変数をnewで上書きしない」を合い言葉にしましょう。

binma
質問者

補足

非常にわかりやすい説明で、ありがとうございます。 上書きの件は理解できましたが、 まだ理解できないのは、ハンドル自体がポインタのように解放するということでnewでなく、gcnew (C++/CLI) は、GC(ガーベージコレクト)なので、勝手に解放してくると思いました。 例外的に直ぐに解放する必要があるファイルやソケットなどリソースは明示的にdeleteで解放する必要があるということでしょうか?

その他の回答 (2)

  • mizmiz
  • ベストアンサー率45% (46/101)
回答No.3

gcnewで確保したオブジェクトはどの時点で処分されるかわかりません。おそらくはアプリケーションで必要がなくなった時点ですが、リアルタイムに行われるわけではないと思います。 メモリーに余裕があればアプリケーションの終了までほっておかれる可能性がありますのでcloseを呼び出さない可能性があり、終了時には処理されると思いますが他の処理との前後関係も保証されません。 私も.Netに移行して間がないので細かいことを説明するだけの経験と知識に欠けるので十分な説明とは言えませんが。それにC++で長いことプログラムを書いてきてC++/CLIの違和感に耐え切れえずにC#に移行した口ですので(笑)。 おそらく変な例外ダイアログメッセージと書かれているものの内容がわかればもう少しいい説明ができるかもしれません。 たとえばメッセージの内容をそのままググってみれば何かわかると思いますよ(わたしもよく使う手です)。 C++/CLIではdeleteが必要でないというのは理解として間違いないと思いますが、このようなクラス設計に問題がありそうです。 close処理は明示的に確実に実行されるように設計するべきだと思います。ファイナライザーは後始末はしなければならないデータがあるけど適当なタイミングがないような処理に使うべきでしょう。ファイルのI/Oなどアプリケーションの外部に影響があるような処理は明示的に行った方がいいと思います。

  • atushi256
  • ベストアンサー率62% (10/16)
回答No.2

#c++/cliは使ったことすらないので、間違えてるかもしれません。 http://geekswithblogs.net/akraus1/articles/81629.aspx 上記URLにC#の場合の解説が書いてあるのですが、たぶん同じようなことが起こっているのではないかと推測しました。 StreamWriterは内部にFileStreamを持っていて、勝手にファイルを開いてくれます。それはいいのですが、問題はFinalizerが呼ばれる順番は保証できないということにあります。GCがStreamWriterを消そうとして、Finalize処理をするわけですが、FileStreamのFinalizerが呼び出されてから、StreamWriterのFinalizerが呼ばれる可能性があります。そうなると「ファイルはもう閉じちゃったのにStreamWriterがファイルを閉じようとする(もしくはまだ書き込んでいない内容をフラッシュしようとする)」という変な状況になります。 そこで、StreamWriterにはFinalizerを用意しないことにしたそうです。で、明示的にCloseを呼び出してもらうことで、Finalizerの順番が保証できない問題を回避することにしたようです。 さて、残る問題はデストラクタとFinalizerのどちらが先に呼ばれるのか?ということです。もしデストラクタが先に呼ばれるならば、StreamWriterのCloseがFileStreamのFinalizerより先に呼ばれることになり、問題は起きません。しかし、どうもデストラクタはFinalizerより後に呼ばれる仕組みのようです。ですので、先ほどのべた問題が発生し、実行時エラーとなってしまいます。 ところが、今度はdelete a;としましたので、この時点でTest_Aのデストラクタが呼び出されます。つまりdelete a;することで、明示的にデストラクタがFinalizerより先に呼ばれるようにしたわけです。結果としてStreamWriter::CloseがFileStream::Finalizerより先に呼び出され、問題なしとなります。 すべての原因は「Finalizerの呼び出し順序が保証できない」「デストラクタはFinalizerより後に呼び出される」ということにあるのかもしれません。

関連するQ&A