2012年7月17日火曜日

newによるメモリの確保

----要約-----

//自由領域にオブジェクトを作りたい時
int* i = new int; //単体
int* is = new int[10]; //配列
MyClass* mc = new MyClass(9999);//クラス

//自由領域のオブジェクトを解体したい時
delete i; //単体
delete [] is; //配列
delete mc; //クラス

 -----解説----

変数を作る時には普通のスコープに従う作り方と自由領域に確保する作り方の2種類があります。

int i = 3;
と書いた時は、通常の作り方です。多分、勉強して始めに覚える作り方です。
一方
 int* i = new int;
と書いた時は自由領域に確保する作り方です。
これはnewという演算子で確保したint型のメモリのアドレスをポインタ型の変数で受け止めています。
ではnewで作ったオブジェクトと通常のオブジェクトは何が違うのでしょうか?
-----------------------------
その前にポインタの基本を確認しておきましょう。

newを使って作ったオブジェクトはポインタで受け止めるので、そのオブジェクトを使いたい時はポインタを使う事になります。
ただし、配列の場合は、普通の配列のように使えます。


int* i = new int;
int *is = new int[10];
MyClass* mc = new MyClass;

*i = 3;//iの値は3になる

for(int i = 0; i <10; ++i)
   is[i] = i; //配列の各要素に0~9を入れる

mc->GetNantoka();

->はアロー演算子といいます。これを使うとポインタが指すクラスや構造体オブジェクトのpublicメンバにアクセスできます。
(*mc). GetNantoka();
と書いても同じ事です。
----------------------------------
さて、newの解説に戻ります。
通常のローカル変数はスタックという一時的なメモリの出し入れをする機構に確保されます。しかしnewで確保したオブジェクトはそこがグローバルスコープであろうとローカルスコープであろうとヒープというプログラムの持続中に永続的に使うための場所にメモリが作られます。


この写真の例は、newで確保したオブジェクトのアドレスだけは他と大分違うという事を示しています。

まぁここで大切なのは具体的なアドレスではなく
newしたオブジェクトはスコープを持たない
という事です。永続的に存在するのだからスコープは関係ないのです。この点はグローバル変数と似たイメージです。

int a = 0;
{
int b = 0;
int* c = new int;
 }
と書いた時、ここ(中括弧の外)ではaにはアクセスできてもbとcにはアクセスできません。アクセス出来ないどころかstaticを付けていない局所変数(ローカル変数)はスコープを抜けた時点で解体されるので変数bとcはデストラクタが呼ばれて破壊されています。

しかしそれにもかかわらずcのメモリ領域は残り続けます。
なぜでしょうか?
それはcというポインタ型の変数は確かにスコープを抜けた時点で破壊されますが、cが指していたnewで確保したメモリ領域はcとは無関係に存在し続けるからです。
いま破壊されたのはcというポインタ変数であって、ポインタ変数が指していたオブジェクトではないのです。

これがもしcがグローバル変数を指すポインタだったらどうでしょうか?

int a = 0;
{
int* c = a;
 }

この場合、newはしていませんがカッコを抜けた後にやはりcは破壊され、それにも関わらずcが指していたメモリaは生存し続けます。つまりここだけ見るとnewによる自由領域のメモリ確保はグローバル変数と似ています。

グローバル変数と違うのはオブジェクトの解体方法です。
 aは newしていないので、aが存在するスコープを抜けた時点で解体されます。例えばaがグローバル変数だとすれば、アプリケーションの終了時に解体されます。
 しかしにnewで作ったメモリ領域のオブジェクトの寿命はスコープとは無関係に残り続けます。

自由領域のメモリを解体するには、そのアドレスを指すポインタに対してdeleteをします。

例)
int* c = new int; //自由領域にintオブジェクトを作成
delete c; //自由領域のintオブジェクトを削除

ただし上の方で見た例の様に

{
int b = 0;
int* c = new int;
 }
ここ、つまりスコープを抜けた後にはポインタのcが存在しないのでdelete c とも書けない分けです。
・・・という分けでnewしたスコープやクラスがきちんとdeleteの面倒を見るように注意を払う必要があります。

----メモリリークについて----
確保したメモリをプログラム的にはもう使わないにも関わらずに解放する事を忘れていると、知らない間に(管理意識の及ばない部分、つまりバグとして)使えるメモリが減っていきます。これをメモリリークと言います。ただしアプリケーション終了時にOSが開放してくれる場合がほとんどだと思います。
いずれにせよメモリの管理が出来ていないのはメモリが減っていくという事実以前にプログラムの整理、管理が出来ていない事と繋がる気がします。プログラミングはメモリを加工したり管理する作業だと言えるので、前向きに捉えるとメモリリークはメモリの存在を意識するための良い教材かも知れません。
-----------------------------

ところで
int* c = new int;
int* d = c;
delete d;

と書いた場合はどうなるのでしょうか?
まず2行目を見ましょう。dはポインタであり、それに対してcのアドレスをコピーしたのでdとcは同じメモリ領域を指すようになりました。
こうなるともうどちらが正当なnewしたポインタだったのか区別が付きません。文法上の意味はどちらも同じであり、どちらか一方が偽物だという訳でもありません。
だからdelete dした時点でdが指していたメモリ領域は消えます。誰か一人がdeleteしてくれれば良いのです。そもそも物理的に同じものを指していたのですから。
つまり
newの数とdeleteの数が合っていれば、ポインタがいくつあろうと問題ありません

さらに、この場合はdがクラス等の他のスコープに所属するポインタ変数でも良いことになります。誰か一人、newした領域を指すポインタさんがどこかのスコープに生きていれば、そしてdeleteする事を忘れなければいつかはその領域を壊すことが出来るでしょう。(ただし一つのスコープで作ったポインタを別のスコープで消すと管理がややこしくなるかも知れません。)

これは勇者と魔王の関係と言えるかも知れません。
つまり
int *p = new int;
と書いた時、pが勇者であり、newが魔王です。
new「がっはっははははは!メモリは頂いた!この領域は永遠に俺のものだ!スコープによって寿命が尽きる普通の変数とは生命力が違うわ!!」
p「メモリを制御するためにお前を生んだだけだ。しかしお前の存在が見えるのは私か私の同胞だけ・・・!例え末代になろうともお前を消す事は忘れぬぞ!」

・・・ちょっとポインタの存在を強調し過ぎたかも知れませんが、まずはnewしたメモリ領域はdeleteされるまで存在し続けるというイメージが大切です。そしてそれに対するアクセス手段がポインタであると考えましょう。
・・・元来、ポインタ変数はどこでどの様に作ってどう消そうと無害です。ポインタ変数の中身はアドレスという数字に過ぎないからです。
しょせんは人間のレベルだと言えます。
それに対し、newしたメモリとはポインタがどうのこうのとは全く別の次元の場所に存在する何かなのです。

例えば、ポインタ変数とそれが指していたメモリ領域は別物なのでdelete した後でもポインタ変数は使い回せます。
例)
int *p = new int(100);
delete p;
p = new int;
delete p;
p = new int[99];
delete [] p;
 

連続でdeleteしてはならない

既にdeleteで解放したメモリに対してnewで新たなメモリを割り当てる事なく再びdeleteする事は認められていません。やると、システムが監視してくれていれば警告ダイアログが出てアプリケーションを終了させるでしょう。いくら魔王といえど2度も殺してはいけないのです。
2回毒殺しようとしたら・・・
・・・・毒が裏返った!
とか・・・まぁ例えはどうでも良いですが。

2度のdeleteというミスを防ぐ簡単な方法としてはdeleteしたらすぐにポインタに0を入れておく事です。アドレス0のポインタをヌルポインタと呼び、これにはdeleteが働きません。

 ヌルポインタは普通にポインタを使う局面においても、 アドレスを指していない初期化前のポインタだという事を表すために使えます。アドレス0番はプログラマーが使うアドレスとしては実在しない事が保証されている様です。

例えばコンストラクタ内やdelete時にポインタに0を入れておき、
if(p != 0)
 等でチェックすれば、アドレスが入っている状態かどうかを確認できます。
また、数値型として0を使う変数ではなく、ヌルポインタだという事を示すために
#define NULL 0
というマクロが良く使われます。

例)
// プログラム開始時
int *p = NULL;

// 使用時
if(p != NULL)
{
   p->Use();
}
else
{
//ここでnewするか、あるいは警告メッセージをだしたり
}

//削除
delete p;
p = NULL;

また、ifで分岐させないでアサートを使って仕様外の状況に対処するのもありでしょう。
例)
_ASSERTE(p);

_ASSERTE()マクロを使うには
#include <crtdbg.h>

というヘッダが必要です。
詳しくは_ASSERTで警告ダイアログを出すをどうぞ

1 件のコメント:

匿名 さんのコメント...

大変参考になる記事ありがとう御座います。
分からなくなる度にここを訪れています。
数年も前の記事ですが感謝をここで。