今回は話をちょっと変えて、new と delete についての話をします。これらが演算子だということを覚えている人はどれだけいるでしょうか?
では、今回の要点です。
では、いってみましょう。
第1部第71章で、次のようなことをいいました。
そうです。new も delete も演算子です。演算子だというのなら new も delete もオーバーロードできるはずです。それはその通りで、new も delete もオーバーロードできます。
では、実際にやってみましょう。
プログラム1 |
---|
// New3.cpp #include <iostream.h> #include <malloc.h> void* operator new(size_t nSize) { // new では確保できないので malloc で確保 void* ptr = malloc(nSize); if(ptr == NULL) throw 1; // 失敗したら例外を返す return ptr; } void operator delete(void* ptr) { free(ptr); // malloc に対しては free } int main() { try { struct HUGE_TYPE{ int dummy[0xFFFFFFF]; }; HUGE_TYPE* p = new HUGE_TYPE; delete p; } catch(int nErrorCode) { cout << "エラー発生 : " << nErrorCode << endl; return 1; } return 0; } |
実行結果例 |
エラー発生 : 1 |
ほとんど何にもしてませんが、大体こんな感じでオーバーロードできます。もうちょっとよく見てみましょう。
先ずは new です。
void* operator new(size_t nSize);
new はコンストラクタを呼んでくれますが、オーバーロード関数内でコンストラクタを呼ぶ必要はありません。コンストラクタは、このオーバーロード関数が終わった後に自動的に実行されます。つまり、ここではメモリを確保することだけに専念すればいいわけです。
そして、メモリを確保するのに必要な情報、確保するサイズが引数から渡されます。引数の型は size_t で、size_t は stddef.h で定義されています。普通は iostream.h などのヘッダファイルをインクルードすれば、自動的にインクルードされます。
そして、確保したらそのアドレスを返します。型は void* になっています。この段階では何の型ということもないので、void* にしてあるわけです。
次は delete です。
void operator delete(void* ptr);
delete では、解放するメモリのアドレスが渡されます。delete でもデストラクタを呼ぶ必要はなく、オーバーロード関数が実行される前に自動的にデストラクタを呼んでくれます。
[ ] をつけた new / delete も同様にオーバーロードできます。引数の型も戻り値の型も同じです。
void* operator new[](size_t nSize); void operator delete[](void* ptr);
このようにして、new と delete のオーバーロードができるわけです。
この時注意することは、C++の関数は内部で new を使っている可能性があるので、無限に new が呼び出されてスタックオーバーフローになる可能性があるということです。内部ではC++の関数を使わないことを奨めます。例えば文字の表示では cout ではなく printf を使う等です。
new と delete をオーバーロードすれば、上のように new が失敗したときに例外を返すことができます(注:C++の本来の仕様では new は例外を返しますが、VC++はデフォルトで例外を返しません。詳しくは次回話します)。また、new の回数と delete の回数をチェックしてメモリリークを示唆することもできます。
メモリを前後余分に確保し、その前後をある値で初期化し、delete 時にその値が変わっていないかチェックすることで、不正な書き込みがないかチェックすることもできます。VC++のデバッグモードではこのことがデフォルトでやられているので、VC++を使う限りはあまり気にする必要はないかも知れません(他のツールでもそうしてあるかもしれません)。
また、new をオーバーロードするときには余分に引数を持たせることもできます。これは、__FILE__ や __LINE__ を渡して、エラーが発生したときにファイル名と行番号を示してやるのに便利です。
例えば、こんな感じです。
プログラム2 |
---|
// New4.cpp #include <iostream.h> #include <malloc.h> // 例外型 class CErrorInfo { private: // __FILE__ はリテラル文字列になるので、 // アドレスを保存するだけで構いません const char* m_pszFile; int m_nLine; int m_nSize; public: CErrorInfo(int nSize, const char* pszFile, int nLine) : m_pszFile(pszFile), m_nLine(nLine), m_nSize(nSize) { } const char* GetFileName() { return m_pszFile; } int GetLineNo() { return m_nLine; } int GetTrialSize(){ return m_nSize; } }; void* operator new[](size_t nSize, const char* pszFile, int nLine) { void* ptr = malloc(nSize); if(ptr == NULL) // 情報を乗せて例外を投げます throw CErrorInfo(nSize, pszFile, nLine); return ptr; } #define new new(__FILE__, __LINE__) void operator delete[](void* ptr) { free(ptr); } int main() { try { int* p = new int[0xFFFFFFF]; delete [] p; } catch(CErrorInfo e) { cout << "エラー発生" << endl << "ファイル : " << e.GetFileName() << endl << "行番号 : " << e.GetLineNo() << endl << "サイズ : " << e.GetTrialSize() << endl; return 1; } return 0; } |
実行結果例 |
エラー発生 ファイル : C:\My Documents\MyProjects\New4\New4.cpp 行番号 : 40 サイズ : 1073741820 |
途中の #define 文を見て下さい。
#define new new(__FILE__, __LINE__)
これが実際に使われているところでは
int* p = new(<ファイル名>, <行番号>) int[0xFFFFFFF];
と展開されるわけです。このように、引数付きの new を使うときは new の後のカッコの中に引数を書きます。
このようにしてデバッグ用の new が作れるというわけです。もちろん、それ以外の利用法も考えられます。
演算子のオーバーロードといえば、クラスのメンバとして使うのが有名です。もちろん new や delete もクラスメンバとしてオーバーロードできます。
これを駆使すれば、配列で確保していながらそれぞれ異なる初期化を行えたりするのですが、配列で確保したあとはやはりデフォルトコンストラクタが呼ばれます。この時、初期化を潰さないようにデフォルトコンストラクタの動作を空にする必要があり、あまり奨められません。
また、デフォルトコンストラクタが例外を投げるとメモリリークを起こしたり、なにかと気を遣うので使うのは極力避けた方がいいかもしれません。
では、今回の要点です。
new と delete のオーバーロードは優れたデバッグ環境があればあまり自分ですることはないかも知れませんが、気合いの入ったデバッグ環境を作ってみるのも面白いかもしれません。
また、これを利用した面白いことができるのですが、それは次回に話しましょう。では。
Last update was done on 2001.5.6
この講座の著作権はロベールが保有しています