2007年10月25日

Visual C++の標準ライブラリとcrtdbg.h

先日のcrtdbg.hを用いたメモリリークの自動検出の記事に関連して、開発中のソフトウェアに組み込んだ際に気づいた点がある。それはcrtdbg.hを#includeしなくても、関連の関数が使えてしまうことだ。それでは非常に不気味なので色々試行錯誤をしてみると、どうやらC++の標準ライブラリであるstringの#includeに問題があり、削除してみたところ、関数が使用できなくなった。そこで原因を探るべく、Visual C++のincludeディレクトリのヘッダーファイルを開いてざっと眺めてみたところ、どうやらstringから様々なヘッダーファイルに依存した結果、最終的にcrtdbg.hを#includeしてしまっているようだ。以下に依存関係を列挙しておく。

#include <string> ← #include <istream> ← #include <ostream>
← #include <ios> ← #include <xlocnum> ← #include <streambuf>
← #include <xiosbase> ← #include <xlocale> ← #include <xdebug>

最後のヘッダーファイルであるxdebugの22行目より

 #if defined(_DEBUG)
  //省略

  #include <crtdbg.h>

マクロ名_DEBUG、つまりVisual StudioのIDEでReleaseではなくDebugでビルドした場合に、問答無用でcrtdbg.hがロードされるわけである。何が問題なのかというと、crtdbg.hではグローバルスコープのnew演算子をオーバーロードしているので、仮に別途new演算子をオーバーロードしている場合にエラーとなってしまうのである。さらに他にもistreamをロードしている標準ライブラリとして、iostreamやiomanip、ios、sstreamなど多数のライブラリがロードを行っているようである。Visual C++で標準ライブラリを用いてなお且つnew演算子をオーバーロードする場合の解決策として、直感的にはxdebugの問題箇所にメスを入れれば良いが、根本的な問題はVisual C++の標準ライブラリの実装の問題なのでスマートではない。


2007年10月24日

メモリリークの自動検出と特定の方法

C/C++では他の言語ではあまりお目にかからない、メモリアロケーションを扱うことが出来る。便利な反面、使い方のお作法を間違えると利点以上の代償を払うことになる。メモリリークが発生する大きな要因としては、確保したメモリの開放のし忘れである。Javaなどとは違いガーベージコレクタは標準ではサポートされておらず、明示的な開放の記述が必要である。
Visual C++では、crtdbg.hというヘッダーにメモリリークを検出し、デバッグログに出力を行ってくれる独自の関数が定義されている。以下に使い方や応用例を示しておく。


まず、ヘッダーをロードしている記述のある場所を以下のように書き換えを行う。

#define _CRTDBG_MAP_ALLOC

//ここに任意の#includeを記述する
//例えばwindows.hやstdio.hなど

#ifdef _DEBUG
  #include <crtdbg.h>
  #define new  ::new(_NORMAL_BLOCK, __FILE__, __LINE__)
#endif

このような順番で記述を行う理由としては、まず下の方の#defineで既存のnewをオリジナルの記述へと置き換えを行っているが、これはcrtdbg.hのバグ対策用であり、new演算子を用いた場合に適切なファイル名がデバッグログに出力されない問題を修正するためのものであるが、たとえばこの記述を標準ライブラリより前に行ってしまうと、プリプロセス時に関係の無いnew演算子まで置き換えを行ってしまい、エラーを起こしてしまうからである。なお#defineで_CRTDBG_MAP_ALLOCの定義を一番先頭で行っているのには深い理由があるのだが、その解説は後日行う。

次にmain関数もしくはWinMain関数の先頭で以下の関数を呼び出す。

_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
これは、プログラム終了と同時にVisual Studioのデバッグログにメモリリークの有無を出力することを指定する記述である。

以上の記述により、malloc(int);やnew int;などを行ったままでプログラムを終了すると、デバッグログにメモリリークの詳細が出力される。

ちなみに、もしGUIやDirectXを用いたソフトウェアを開発している場合に、AllocConsoleなどで確保したコンソールにメモリリークのログを出力したい場合は、WinMain関数の一番最後の行に以下の記述を行うことにより可能となる。

HANDLE hStdout = GetStdHandle(STD_OUTPUT_HANDLE);  //標準出力のハンドルを取得
_CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_FILE);  //ログの出力対象をファイルハンドルに指定
_CrtSetReportFile(_CRT_WARN,hStdout);  //ワーニングの出力対象を第二引数のストリームに指定
_CrtDumpMemoryLeaks();  //実行時点での開放されていないメモリの一覧を表示


参考:
メモリ リークの検出と特定
_CRTDBG_MAP_ALLOC マクロは、正常に機能しません。

2007年09月24日

const修飾子と定数及びポインタ

Effective C++を読んでのメモ

C言語では書き換え不可という意味であったconstであるが、C++では定数として利用できるconst。それによりC++では配列の添え字として変数の利用が可能である。ここで注意したいのはC++では#defineで指定したマクロ名を定数として利用するのは好ましくない。その理由として、まず大抵のコンパイラの実装はプリプロセッサ命令として#defineで指定されたマクロ名の置き換え処理を行うので、マクロ名は隠蔽される。もしその定数が原因でコンパイルエラーが起きた際に、原因の特定が厄介になってしまうのである。またスコープの概念も#defineには適用されないのでクラスの中で定数を宣言したい場合には#defineでは無く、constを使うべきである。

ところでconst修飾子とポインタの関係について。

int num = 100;

const int *cVar1 = #  //データに対しconst (1)
int const *cVar2 = #  //データに対しconst (2)
int *const cVar3 = #  //ポインタに対しconst (3)
const int *const cVar4 = #  //ポインタとデータに対しconst (4)
int const *const cVar5 = #  //ポインタとデータに対しconst (5)

所持している参考書では(1)と(3)と(4)のスタイルが紹介されていたが、(1)に関しては(2)の形でも表すことが可能である。(2)により(4)に関しては(5)のようなスタイルも可能である。コーディングスタイルを規定する時のために頭の隅に入れておきたい。
×

この広告は180日以上新しい記事の投稿がないブログに表示されております。