I thought what I'd do was. I'd pretend I was one of those deaf-mutes or should I?

new ⁄ deleteマニアックス

C++のメモリ管理機構(?)、newとdeleteについての実験。newとdeleteの使い方くらいマスターしてるよって人向け。実行&コンパイル環境は、FedoraCore3、gcc4.0.0 20041019 (Red Hat 4.0.0-0.8)です。

実験1

さっそくいってみましょう。

1 : int main()
2 : {
3 :   int* p = new int[4];
4 :   delete p;
5 :   return 0;
6 : }
7 : 

言っときますけど、わざとですよ。わざとnew[]で確保した領域をdelete[]で解放せずにdeleteで解放しています。明らかにメモリリークしますね。けど、これやってみて驚いたんですけど、コンパイラは警告すら出さないし、普通に何事も無かったかのように実行しちゃいます。あちゃー、気をつけよう。次は、もうちょっと詳しく動作を確認してみます。

実験2

 1 : #include <iostream>
 2 : using namespace std;
 3 : 
 4 : class C
 5 : {
 6 : public:
 7 :   C()
 8 :   {
 9 :     cout << "C constructer called" << endl;
10 :   };
11 :   ~C()
12 :   {
13 :     cout << "C destructer called" << endl;
14 :   }
15 : };
16 : 
17 : int main()
18 : {
19 :   C* p = new C[4];
20 :   delete p;
21 :   return 0;
22 : }
23 : 

今度はプリミティブな型ではなく、自前クラスで同じことをやってみました。これもコンパイラの警告は一切ありません。ですが、実行時にセグメンテーションフォールトが発生しプログラムは止められます(OS側に「変な領域にアクセスしようとしてんじゃねーよっ」てな感じで)。一応、止められるところまでの結果を載せると

C constructer called
C constructer called
C constructer called
C constructer called
C destructer called

なんでこんな所で止まったのか。聞いた話によると、クラスなんかはコンストラクタなどの各種管理情報を確保した領域(&hogeで指されるアドレス)の手前に持っているらしく、今回の例だとクラスCの配列で、その管理情報はクラスCを確保した領域の手前にあるわけです。で、20行目で「delete p;」とすると、その0番目要素であるp[0]の領域だけを解放しようとするわけです。つまり「delete &p[0];」と同じ。そして同時にp配列の管理情報まで消そうとしたのはいいけれど、実際には、おそらく管理情報の形式が配列とただのインスタンスでは違うことから実行時エラーとなってしまうのだと思われます。

デストラクタを呼んでから、領域を解放しようするでしょうから、実行結果とも一致します。ま、私の勘違いっていうのはあるかもね(なれなれしい)? では、次こういうのはどうでしょう?

実験3

 1 : #include <iostream>
 2 : using namespace std;
 3 : 
 4 : class C
 5 : {
 6 : public:
 7 :   C()
 8 :   {
 9 :     cout << "C constructer called" << endl;
10 :   };
11 :   ~C()
12 :   {
13 :     cout << "C destructer called" << endl;
14 :   }
15 :   void foo()
16 :   {
17 :     cout << "foo called" << endl;
18 :   }
19 : };
20 : 
21 : int main()
22 : {
23 :   C* p = new C[4];
24 :   delete &p[2];
25 : 
26 :   return 0;
27 : }
28 : 

これは、「*** glibc detected *** free(): invalid pointer: 0x09ae400e *** アボートしました」と実行時にエラーが出力されました。おそらく、さっきの話と同じで、メモリ管理情報を見に行くときに、「んなもん、ねぇよ!」って怒られているわけです。実験2では形式違いで、今度は形式も糞もハナっから管理情報のかけらも見当たらなかったわけです。

実験4

次、少々マニアックになってきました。実験の目的は、「先の先までdeleteしてくれるのか?」ということです。

 1 : #include <iostream>
 2 : using namespace std;
 3 : 
 4 : class C
 5 : {
 6 : public:
 7 :   C()
 8 :   {
 9 :     cout << "C constructor\n";
10 :   };
11 :   ~C()
12 :   {
13 :     cout << "C destructor\n";
14 :   };
15 :   void foo()
16 :   {
17 :     cout << "foo called\n";
18 :   };
19 : };
20 : 
21 : int main()
22 : {
23 :   C** p = new C*[3];
24 :   C* q;
25 :   for (int i=0; i < 3; i++) {
26 :     p[i] = new C[2];
27 :   }
28 :   q = p[1];
29 :   delete[] p;
30 : 
31 :   q->foo();
32 : }
33 : 

ややこしいので、まず何をやっているのかから。実験3のコードから、Cクラスにfooという関数メンバが増えました。そのCクラスのポインタへのポインタ配列pを23行目でnewしています。さらに、pの各要素はCクラスへのポインタなので、25〜27行目のfor文でCクラスをnewしています。

この時点で、Cクラスのコンストラクタが呼ばれていきます。そして次に、Cクラスへのポインタ変数qにpの1番目の要素(Cクラスへのポインタ型)を代入しています。そして29行目でpをdelete。これでqの指す先にオブジェクトが残っているかどうかを31行目で調べているわけです。残っていなければ、何らかのエラーが起こるでしょう。実行結果は次の通り。

C constructor
C constructor
C constructor
C constructor
C constructor
C constructor
foo called

オブジェクトは残っていますね。つまり、こんなところでもメモリリークの心配をする必要があるわけです。ポインタのポインタには気をつけねば。

オワリニ。まだまだやろうと思えば実験できますけど、時間の都合上(?)これにて終わり。最期にひとこと。やっぱりC/C++ではメモリ管理を意識しないとダメですねー。

Comments are welcome! We can be reached at whoinside_reshia@hotmail.co.jp
2005/05/06 02:47:59 UTC