昨今はJavaやC#などメモリの扱いを気にしなくて良い環境が増えており
メモリ管理の存在そのものを知らない人も少なくない
今回はC,C++にて自前でメモリを管理する処理の基礎を説明していく
#メモリ管理の概要
大抵の環境ではOSやら根幹のシステムがメモリのすべてを保持しており
ユーザは必要に応じてシステムを問い合わせをしてメモリを確保し不要になったらシステムに返す
###ざっくりとした流れ
・メモリ確保
ユーザ「OSさん、メモリを100byte下さい」
↓
OS「OK、アドレス0x00010000から100byte使っていいよー」
・メモリ解放
ユーザ「OSさん、アドレス0x00010000のメモリをお返しします」
↓
OS「OK、メモリ返してもらったよー」
###メモリリーク
メモリリークとは、確保したメモリを不要になっても解放せずいること
結果として、メモリが不足して確保できなくなる問題が発生する
C,C++で自前でメモリを管理する場合に非常に多く起きる問題である
なお、JavaやC#はこれらを言語レベルで解消しているため、メモリリークが起きることはない
##Cのプログラム
###メモリ確保関数
####malloc
指定されたサイズのメモリを確保する関数
確保できない場合はNULLを返す
####calloc
指定されたサイズのメモリブロックを確保し、確保した領域を0クリアする関数
確保できない場合はNULLを返す
####realloc
確保済みのメモリを拡張する関数
確保できない場合はNULLを返す
###メモリ解放関数
####free
malloc,calloc,reallocで確保した領域を解放する関数
###コード
#include <stdlib.h>
int main()
{
// 100byteのメモリ確保
char* p = malloc(100);
if (p == NULL) {
// メモリが確保できなかった時の処理
}
// 100 * sizeof(int)分のbyteのメモリを確保し、中身を0でクリアする
// p = malloc(100 * sizeof(int)); memset(p, 0, 100 * sizeof(int)); と同じ処理
int* p2 = calloc(100, sizeof(int));
if (p2 == NULL) {
// メモリが確保できなかった時の処理
}
// 100byte確保しているメモリを200byteに拡張する
char* p3 = realloc(p, 200);
if (p3 == NULL) {
// メモリが確保できなかった時の処理
} else {
// メモリが確保できたので、pを拡張した領域として扱う
p = p3;
}
// 確保したメモリを解放する
free(p2);
free(p);
}
##reallocの注意点
mallocとcallocは深く考えずに使うことができるが、reallocを使う場合は注意が必要である。
reallocはメモリを拡張する機能を持つ関数であるがその動作は複雑である。
新しいアドレス = realloc(既存のアドレス, 確保したいメモリサイズ);
1:既存のアドレスのNULL判定
NULLである:mallocと同じ動作
NULLでない:2へ
2:確保したいメモリ量が既存のメモリより同じか小さいか大きいか
同じ:何もしないで返す
小さい:システムが管理しているアドレスのサイズ情報を書き換えて既存のポインタを返す
大きい:3へ
3:既存の領域の後ろに拡張できるか確認
拡張可能:システムが管理しているアドレスのサイズ情報を書き換えて既存のポインタを返す
拡張不可能かつメモリ不足:NULLを返す、既存のアドレスには何もしない
拡張不可能:別領域にメモリを確保し、既存のアドレスの内容を新しいアドレスにコピーした後、既存のアドレスを解放して新しいアドレスを返す
といった動作を行う。ここでの注意点はエラーの判定である。
サイズ拡張時にメモリが確保できない場合、既存のアドレスに対して何もしていないので
以下の書き方をした場合、メモリリークを発生させてしまうことになる。
char* p = malloc(100);
p = realloc(p, 200);
if (p == NULL) {
// pにNULLが入ってしまった場合、mallocで確保した領域を解放することが出来ない!
}
##C++のプログラム
Cの場合はメモリ確保が関数であったが、C++の場合は命令でメモリ確保を行うことができる
###メモリ確保命令
####new
メモリを確保する命令
確保できない場合は例外でstd::bad_allocを投げる
std::nothrowを指定すると、例外ではなくnullptrを返す
###メモリ解放命令
####delete
newで確保した領域を解放する命令
####delete[]
newで確保した配列を解放する命令
###コード
#include <memory>
int main()
{
// 通常の確保
char* p = new char[100];
// 例外判定
char* p2 = nullptr;
try {
p2 = new char[100];
} catch (const std::bad_alloc& e) {
// メモリ不足
}
// nullptr判定
char* p3 = new (std::nothrow) char[100];
if (p3 == nullptr) {
// メモリ不足
}
// 指定した領域からメモリを確保する
// この領域はdelete[]してはいけない
char buf[100];
char* p4 = new (buf) char[100];
// nの指す先は未初期化
int* n = new int;
// n2の指す先は0で初期化
int* n2 = new int();
// n3の指す先は10で初期化
int* n3 = new int(10);
delete n3;
delete n2;
delete n;
delete[] p3;
delete[] p2;
delete[] p;
}
##確保命令のうんちく
多すぎてだるい、コメントで何か言われたら書く
##解放命令のうんちく
###メモリ解放時のNULL判定について
解放時に以下のコードを見ることがよくある。
if (p != nullptr) {
free(p);
p = nullptr;
}
if (p2 != nullptr) {
delete p2;
p2 = nullptr;
}
しかしfreeやdeleteにNULLが指定された場合、何もしないということが規格で決まっているため
NULL判定をする必要がない
free(p);
p = nullptr;
delete p2;
p2 = nullptr;
これで十分である
###解放命令に不明なアドレスを指定したときの挙動
NULLや確保済み以外のアドレスに対して解放命令を呼んだ場合、その動作は未定義となります。
一度確保した領域を二回解放するのもダメです。
newで配列を確保した領域をdeleteで解放するのもダメです。
newで配列でない領域を確保したときにdelete[]で解放するのもダメです。
newで確保した領域をfreeで解放するのとかもダメです。
mallocで確保した領域をdeleteで(ry
未定義動作は何が起こるかわからないので、解放命令を呼んだ後、そのポインタにはNULLを入れておきましょう。