C++のメモリ確保と解放について調べてみた。
入手先
スタック領域とヒープ領域
項目 | スタック | ヒープ |
---|---|---|
管理者 | OS・コンパイラ | プログラマ |
生存期間 | 関数終了・デストラクタまで | delete・freeされるまで |
変数サイズ | コンパイル時点で固定 | 実行時に動的 |
メモリ総量 | 少ない | 多い |
確保 | int v; | int v = new int(); |
解放 | (宣言した関数終了時に自動解放) | delete v; |
- 変数を宣言する方法が違う
- スタック領域は少ないため、多く確保してしまうとスタックオーバーフローを起こしてしまう。
メモリ確保
メモリ確保にはおおよそ2種類の方法がある。
- 自動変数によるスタック領域のメモリ確保
- newやmallocによるヒープ領域のメモリ確保
int value;
上記のコードは自動変数と呼ぶ。 OSやコンパイラがメモリの確保と解放を行ってくれる。 スタック領域に確保される。(実行環境により異なるらしいが)
main () {
int value;
}
上記のコードでメモリ確保される。 OSが32bitなら32bit(4byte)、64bitなら64bit(8byte)、確保される。
自動変数は、メモリの解放はOSやコンパイラが自動で行う。 関数終了時に解放される。
上記コードの場合、int value;
と宣言したときにメモリ確保され、main関数が終了したら自動的にメモリ解放される。
対して、以下のコードは自分でメモリの確保と解放を明示している。
int* p1 = new int;
delete p1;
char* str = malloc(100 * sizeof(char));
free(str);
自前で確保と解放を行う。この場合、スタック領域でなくヒープ領域に確保される。
new/deleteはC++の構文。classに対するメモリ確保を指示する malloc/freeはC言語の関数。メモリの確保と解放を指示する。
classのメンバ変数
classのメンバ変数はいつメモリ確保され、いつ解放されるのか。 自動変数として宣言するか、自前で確保と解放をするかで違う。
どうやって確保と解放をすべきなのか確かめるためコードを書いてみた。
完成コード
includeなどは省略している。
Item.h
class Item
{
public:
Item(void);
~Item(void);
std::basic_string<TCHAR> Name;
};
Item.cpp
Item::Item(void) {}
Item::~Item(void) {}
Human.h
class Human
{
public:
Human(void);
~Human(void);
Item item;
Item* pItem;
};
Human.cpp
Human::Human(void)
{
this->item.Name = _T("アイテム");
this->pItem = new Item();
this->pItem->Name.assign(_T("アイテムポインタ"));
}
Human::~Human(void)
{
if (NULL != this->pItem) {
delete this->pItem;
this->pItem = NULL;
}
}
Program.cpp
void main () {
// スタック領域
Human human;
human.~Human(); // 記述しようがしまいがmain関数が終了されたときにデストラクタが呼び出される
// ヒープ領域
Human* pHuman = new Human();
delete pHuman; // newしたら必ずdeleteでデストラクタを呼び出すべき
}
解放タイミング
変数の解放タイミングは、確保の仕方によって異なる。 確保の仕方は2種類ある。自動変数とnewである。
先述の完成コードを例にする。
自動変数
Human.Itemは自動変数。 自動変数は宣言されたときにメモリ確保されるはず。 そして、宣言した関数が終了するときに解放されるはず。
確保と解放はOSやコンパイラが自動で行うらしい。 そのためプログラマは確保と解放を明記する必要がない。 解放忘れが起こりえない代わりに、タイミングを指示できない。
また、自動変数はスタック領域にメモリを確保するらしい。 しかしスタック領域はヒープ領域に比べて少ないらしい。 そのため大量にメモリ確保するとスタックオーバーフローを引き起こすことがあるとか。 その場合、自動変数でなくnewなどでヒープ領域にて確保することで回避できるらしい。
スタック領域かヒープ領域、どちらにメモリ確保するかはOSやコンパイラ次第でもあるらしい。
組込系など限られたメモリでやりくりせねばならない場合などは自前で管理する必要があるのかもしれない。
human
Humanインスタンスが解放されるタイミングは、Humanインスタンスを確保した方法によって異なる。 例のコードでは2種類の方法がある。
- 自動変数として確保しているhuman
- newで確保しているpHuman
自動変数humanは~Human()でデストラクタを呼び出している。 human.pItemはデストラクタを呼び出したら解放される。 デストラクタ内で解放処理を実装しているためである。
しかし、human.Itemはまだ削除されないはず。 main()の終了時に自動的に削除されると思われる。 humanは自動変数なので、humanを宣言したmain関数が終了したときに自動的に解放されると思われる。 そのタイミングでhuman.Itemも解放されると思われる。
new変数
対してnewしたほうは、deleteでデストラクタを呼び出している。 自動変数とおなじくpHuman.pItemはデストラクタを呼び出したら解放される。
おそらくpHuman.Itemもデストラクタを呼び出したら解放される。 ここが自動変数との違い。
pHuman
ちなみにポインタ変数であるpHuman自体は自動変数。 pHumanを宣言したmain関数が終了すると自動で解放される。 deleteで解放するのは、pHumanポインタが指し示すアドレスに存在するメモリのほう。 これはHuman.pItemポインタ変数も同じ。
間違いコード
間違いと知りつつ、わざとやってみることで先述のコードの正しさを確かめてみた。
試行1
自動変数に対してdeleteしてみた。
Human.cpp
Human::~Human(void)
{
delete this->item; // error C2440: 'delete' : 'Item' から 'void *' に変換できません。
// 自動変数として確保した場合、deleteしてはダメ。
// deleteはnewで確保した場合のみ。
// 他、malloc()したときのみfree()する。
// このように確保と解放の対応が決まっている。
// 自動変数の場合、宣言元の関数が終了したときに自動解放される。よって解放を明記しない(できない)。
delete this->pItem; // 0xC0000005: 場所 0xfeeefee2 を読み込み中にアクセス違反が発生しました。
// main()で宣言した「Human human;」は自動変数なので、二重解放になってアクセス違反になる。
// 1回目はデストラクタを呼び出したとき。(human.~Human();)
// 2回目はmain関数が終了したときに自動解放されるとき。
// deleteは1回だけ呼び出すようにし、以降はNULLを代入してdeleteしないようにすることで回避できる
}
Program.cpp
main () {
// スタック領域
Human human;
human.~Human(); // 0xC0000005: 場所 0xfeeefee2 を読み込み中にアクセス違反が発生しました。
// 上記の呼出方法はスタック領域での確保だから呼出元の関数が終了時に自動的に破棄される。
// つまり、関数終了時に二重解放となりアクセス違反になる。
// ここでデストラクタを呼び出しているため、すでに解放済みだから。
// デストラクタ側で、nullでないときだけ削除するようにすればエラーが出なくなる。
// その場合、デストラクタ呼出のタイミングで解放できるし、遅くとも呼出元関数が終了する時に自動解放される。
// 詳しくは完成コードのほうを参照。
}
試行2
自動変数のアドレスを渡してdeleteしてみた。
Program.cpp
main () {
// スタック領域
Human human;
delete human; // error C2440: 'delete' : 'Human' から 'void *' に変換できません。
// 自動変数の場合、宣言元の関数が終了したときに自動解放される。よって解放を明記しない(できない)。
}