C++におけるメモリ管理:new/delete演算子の仕組みと実践

1. C言語の動的メモリ管理

void Test()
{
    int* p1 = (int*)malloc(sizeof(int));
    free(p1);

    // malloc/calloc/realloc の違い
    int* p2 = (int*)calloc(4, sizeof(int));
    int* p3 = (int*)realloc(p2, sizeof(int) * 10);

    // p2 を free する必要はあるか?
    free(p3);
}

realloc は元のメモリブロックを拡張(インプレース)するか、新しい領域にコピー(アウトオブプレース)します。インプレースの場合は p3 と p2 は同じアドレスを指し、アウトオブプレースの場合は元の領域が自動解放されるため、明示的に free(p2) する必要はありません。

2. C++ のメモリ管理方式

C++ では C 言語のメモリ管理関数も使用可能ですが、特にカスタム型の扱いが面倒な点があるため、newdelete 演算子が導入されました。

2.1 組み込み型に対する new/delete

void Test()
{
    // 1つの int 領域を動的確保
    int* ptr4 = new int;

    // 1つの int 領域を確保し、値 10 で初期化
    int* ptr5 = new int(10);

    // 10個の int 領域を連続確保
    int* ptr6 = new int[10];

    delete ptr4;
    delete ptr5;
    delete[] ptr6;
}

単一要素の確保・解放には new/delete を、連続領域には new[]/delete[] を使用し、これらは必ず対にして使います。

2.2 カスタム型に対する new/delete

C 言語スタイルのノード生成

struct ListNode* CreateListNode(int val)
{
    struct ListNode* newnode = (struct ListNode*)malloc(sizeof(struct ListNode));
    if (newnode == NULL)
    {
        perror("malloc fail");
        return NULL;
    }
    newnode->_next = NULL;
    newnode->_val = val;
    return newnode;
}

C++ スタイルのノード生成

struct ListNode
{
    ListNode* _next;
    int _val;

    ListNode(int val)
        : _next(nullptr), _val(val)
    {}
};

int main()
{
    // カスタム型:領域確保+コンストラクタ呼び出し
    // new 失敗時は例外がスローされるため、戻り値チェックは不要
    ListNode* node1 = new ListNode(1);
    ListNode* node2 = new ListNode(2);
    ListNode* node3 = new ListNode(3);
    // 後処理は省略
    return 0;
}

C++ による連結リスト生成

// 番兵なしリストの生成
ListNode* CreateList(int n)
{
    ListNode head(-1);  // 番兵ノード
    ListNode* tail = &head;
    int val;
    
    std::cout << "Please enter " << n << " node values: ";
    for (size_t i = 0; i < n; ++i)
    {
        std::cin >> val;
        tail->_next = new ListNode(val);
        tail = tail->_next;
    }
    return head._next;
}

int main()
{
    ListNode* list1 = CreateList(5);
    // 実際のアプリケーションでは適切な解放処理が必要
    return 0;
}

2.3 メモリ確保失敗時の挙動:例外 vs NULL

void func()
{
    int n = 1;
    while (true)
    {
        int* p = new int[1024 * 1024 * 100];
        std::cout << n << " -> " << p << std::endl;
        ++n;
    }
}

int main()
{
    func();
    return 0;
}

C 言語の malloc は失敗時に NULL を返すため手動チェックが必要ですが、C++ の new は失敗時に std::bad_alloc 例外をスローします。

2.4 カスタム型と new/delete の動作

class A
{
public:
    A(int a = 0) : _a(a)
    { std::cout << "A():" << this << std::endl; }
    ~A()
    { std::cout << "~A():" << this << std::endl; }
private:
    int _a;
};

class Stack
{
public:
    Stack()
    {
        _a = (int*)malloc(sizeof(int) * 4);
        _top = 0;
        _capacity = 4;
    }
    ~Stack()
    {
        free(_a);
        _top = _capacity = 0;
    }
private:
    int* _a;
    int _top;
    int _capacity;
};

int main()
{
    A* ptr1 = new A;          // operator new + 1回のコンストラクタ呼び出し
    A* ptr2 = new A[10];      // operator new[] + 10回のコンストラクタ呼び出し

    delete ptr1;              // 1回のデストラクタ + operator delete
    delete[] ptr2;            // 10回のデストラクタ + operator delete[]

    Stack* pst = new Stack;
    delete pst;

    int* p1 = new int[10];
    delete[] p1;

    return 0;
}

カスタム型に対して new は領域確保後にコンストラクタを呼び出し、delete はデストラクタを呼び出してから領域を解放します。malloc/free ではこの動作は行われません。

3. new/delete の内部実装

3.1 組み込み型の場合

組み込み型では newmallocdeletefree の動作はほぼ同様ですが、new/new[] は単一要素・連続領域の区別があり、失敗時には例外を発生させる点が異なります。

3.2 カスタム型の場合

new の内部処理:

  1. operator new 関数を呼び出してメモリ領域を確保
  2. 確保した領域上でコンストラクタを実行し、オブジェクトを初期化

delete の内部処理:

  1. 領域上でデストラクタを実行し、リソースを解放
  2. operator delete 関数を呼び出して領域を解放

new T[N] の内部処理:

  1. operator new[] を呼び出し、その内部で operator new を使って N 個分の領域を確保
  2. 確保した領域上で N 回のコンストラクタを実行

delete[] の内部処理:

  1. 解放対象の領域上で N 回のデストラクタを実行
  2. operator delete[] を呼び出し、その内部で operator delete を使って領域を解放

タグ: C++ new delete Memory-Management operator-new

5月31日 03:10 投稿