第4章:メモリ管理とRAII

はじめに

C++はCから低レベルのメモリ操作能力を継承していますが、より安全で表現力豊かなメモリ管理機構も提供しています。本章では、new/delete演算子とRAII(Resource Acquisition Is Initialization)パターンについて学びます。

RAIIは、Bjarne Stroustrupが命名したC++の中核的なイディオムであり、リソース管理を劇的に簡素化します。

---

1. new/delete演算子

1.1 malloc/freeとの違い

C言語のメモリ割り当て:

/* C言語 */
int* arr = (int*)malloc(10 * sizeof(int));
if (arr == NULL) {
    /* エラー処理 */
}
/* 使用 */
free(arr);

C++のメモリ割り当て:

// C++
int* arr = new int[10];  // キャストと sizeof 不要
// 使用
delete[] arr;

主な違い:

| 観点 | malloc/free | new/delete | |------|-------------|------------| | 型安全性 | なし(void*返却) | あり(正しい型を返す) | | サイズ計算 | 手動(sizeof必要) | 自動 | | コンストラクタ | 呼ばれない | 呼ばれる | | デストラクタ | 呼ばれない | 呼ばれる | | 失敗時 | NULL返却 | 例外を投げる |

1.2 単一オブジェクトの動的割り当て

class Point {
    int x, y;
public:
    Point(int x, int y) : x(x), y(y) {
        std::cout << "Point constructed" << std::endl;
    }
    ~Point() {
        std::cout << "Point destroyed" << std::endl;
    }
};

int main() {
    // newでコンストラクタが呼ばれる
    Point* p = new Point(10, 20);

    // deleteでデストラクタが呼ばれる
    delete p;

    return 0;
}

1.3 配列の動的割り当て

// 配列の割り当て
int* arr = new int[100];

// 配列の解放(delete[]を使う)
delete[] arr;

// 注意: delete と delete[] を混同しない!
// delete arr;   // 未定義動作!
// delete[] ptr; // 単一オブジェクトに使うと未定義動作!

1.4 newの失敗

newがメモリ確保に失敗すると、std::bad_alloc例外を投げます:

#include <new>

try {
    // 非常に大きなメモリを要求
    int* huge = new int[10000000000ULL];
} catch (const std::bad_alloc& e) {
    std::cerr << "Memory allocation failed: " << e.what() << std::endl;
}

例外を投げない版も存在します:

int* ptr = new (std::nothrow) int[1000000000];
if (ptr == nullptr) {
    std::cerr << "Memory allocation failed" << std::endl;
}

---

2. RAII(Resource Acquisition Is Initialization)

2.1 概念と歴史

RAIIは「リソースの取得は初期化である」という意味ですが、より重要なのは「リソースの解放は破棄である」という点です。

Bjarne Stroustrupがこの用語を作りました:

> "The basic idea is to represent a resource by a local object, so that the local object's destructor will release the resource." > > (基本的なアイデアは、リソースをローカルオブジェクトで表現し、ローカルオブジェクトのデストラクタがリソースを解放するようにすることである)

2.2 RAIIの原則

  • コンストラクタでリソースを取得
  • デストラクタでリソースを解放
  • コピーの意味を明確に定義(または禁止)

class RAIIFile {
private:
    FILE* file;

public:
    // リソースの取得(コンストラクタ)
    explicit RAIIFile(const char* filename, const char* mode) {
        file = fopen(filename, mode);
        if (!file) {
            throw std::runtime_error("Failed to open file");
        }
    }

    // リソースの解放(デストラクタ)
    ~RAIIFile() {
        if (file) {
            fclose(file);
        }
    }

    // コピー禁止
    RAIIFile(const RAIIFile&) = delete;
    RAIIFile& operator=(const RAIIFile&) = delete;

    // ファイル操作
    size_t read(void* buffer, size_t size, size_t count) {
        return fread(buffer, size, count, file);
    }
};

2.3 RAIIと例外安全性

RAIIの最大の利点は、例外発生時でもリソースが確実に解放されることです:

// 悪い例:リソースリークの可能性
void badExample() {
    FILE* file = fopen("data.txt", "r");
    if (!file) return;

    processData();  // ここで例外が発生したら...
    // fcloseは呼ばれない!

    fclose(file);
}

// 良い例:RAIIによる安全な管理
void goodExample() {
    RAIIFile file("data.txt", "r");  // ファイルを開く

    processData();  // 例外が発生しても...
    // デストラクタが必ず呼ばれ、ファイルは閉じられる
}

2.4 RAIIの適用例

ミューテックスロック

class MutexLock {
    std::mutex& mutex;
public:
    explicit MutexLock(std::mutex& m) : mutex(m) {
        mutex.lock();
    }
    ~MutexLock() {
        mutex.unlock();
    }
    MutexLock(const MutexLock&) = delete;
    MutexLock& operator=(const MutexLock&) = delete;
};

void criticalSection(std::mutex& m) {
    MutexLock lock(m);  // ロック取得
    // 処理...
}  // 自動的にアンロック

メモリ管理

class AutoArray {
    int* data;
    size_t size;
public:
    explicit AutoArray(size_t n) : data(new int[n]), size(n) {}
    ~AutoArray() { delete[] data; }

    AutoArray(const AutoArray&) = delete;
    AutoArray& operator=(const AutoArray&) = delete;

    int& operator[](size_t i) { return data[i]; }
};

---

3. スマートポインタ概要

3.1 生ポインタの問題

void problematicFunction() {
    int* ptr = new int(42);

    if (someCondition()) {
        return;  // メモリリーク!
    }

    if (otherCondition()) {
        throw std::runtime_error("Error");  // メモリリーク!
    }

    delete ptr;  // ここに到達しないかも
}

3.2 unique_ptr

所有権が唯一(unique)のスマートポインタ:

#include <memory>

void safeFunction() {
    std::unique_ptr<int> ptr(new int(42));
    // または C++14以降
    auto ptr2 = std::make_unique<int>(42);

    if (someCondition()) {
        return;  // OK: ptrのデストラクタでメモリ解放
    }

    // deleteは不要
}

特性

  • コピー不可(所有権は唯一)
  • ムーブ可能(所有権の移動)
  • オーバーヘッドなし(生ポインタと同等)

std::unique_ptr<int> ptr1(new int(42));
// std::unique_ptr<int> ptr2 = ptr1;  // コンパイルエラー

std::unique_ptr<int> ptr2 = std::move(ptr1);  // OK: 所有権移動
// ptr1 は nullptr になる

3.3 shared_ptr

参照カウント方式の共有所有ポインタ:

#include <memory>

void sharedOwnership() {
    auto ptr1 = std::make_shared<int>(42);
    std::cout << "Count: " << ptr1.use_count() << std::endl;  // 1

    {
        std::shared_ptr<int> ptr2 = ptr1;  // コピーOK
        std::cout << "Count: " << ptr1.use_count() << std::endl;  // 2
    }  // ptr2のデストラクタで参照カウント減少

    std::cout << "Count: " << ptr1.use_count() << std::endl;  // 1
}  // ptr1のデストラクタでメモリ解放(カウント0)

特性

  • コピー可能(参照カウント増加)
  • ムーブ可能(カウント変化なし)
  • オーバーヘッドあり(参照カウント管理)

3.4 weak_ptr

shared_ptrの循環参照を防ぐための弱参照:

class Node {
public:
    std::shared_ptr<Node> next;
    std::weak_ptr<Node> prev;  // 循環参照を防ぐ
};

3.5 42 CPP Moduleでの扱い

42のCPP Module(C++98ベース)では、スマートポインタは標準ライブラリにありません。しかし、概念を理解しておくことは重要です。

C++98では、以下の方法でメモリを管理します:

  • RAIIクラスを自作する
  • コピー禁止の実装
  • 明示的なdelete

---

4. メモリリークの検出

4.1 Valgrind

Linuxでのメモリリーク検出ツール:

# コンパイル(デバッグ情報付き)
g++ -g -o program program.cpp

# Valgrindで実行
valgrind --leak-check=full ./program

出力例:

==12345== HEAP SUMMARY:
==12345==     in use at exit: 40 bytes in 1 blocks
==12345==   total heap usage: 5 allocs, 4 frees
==12345==
==12345== 40 bytes in 1 blocks are definitely lost in loss record 1 of 1
==12345==    at 0x4C2E0EF: operator new[](unsigned long)
==12345==    by 0x400A3B: main (program.cpp:15)

4.2 AddressSanitizer

コンパイラ組み込みのメモリエラー検出:

# コンパイル(ASANを有効化)
g++ -fsanitize=address -g -o program program.cpp

# 実行(追加のツール不要)
./program

検出できるエラー:

  • メモリリーク
  • ヒープバッファオーバーフロー
  • スタックバッファオーバーフロー
  • Use-after-free
  • Double-free

4.3 よくあるメモリエラーパターン

1. メモリリーク

void leak() {
    int* ptr = new int(42);
    // deleteを忘れた
}

2. 二重解放

void doubleFree() {
    int* ptr = new int(42);
    delete ptr;
    delete ptr;  // 二重解放!
}

3. Use-after-free

void useAfterFree() {
    int* ptr = new int(42);
    delete ptr;
    *ptr = 100;  // 解放後のアクセス!
}

4. delete/delete[]の混同

void mismatch() {
    int* arr = new int[10];
    delete arr;    // delete[]であるべき!

    int* single = new int(42);
    delete[] single;  // deleteであるべき!
}

---

5. ダングリングポインタ

5.1 ダングリングポインタとは

解放済みまたは無効なメモリを指すポインタ:

int* getDangling() {
    int local = 42;
    return &local;  // ローカル変数のアドレスを返す!
}  // localはスコープ終了で無効

int main() {
    int* ptr = getDangling();
    std::cout << *ptr << std::endl;  // 未定義動作
    return 0;
}

5.2 回避パターン

1. 解放後にnullptrを代入

int* ptr = new int(42);
delete ptr;
ptr = nullptr;  // 再アクセスを防ぐ

if (ptr) {
    // ptrがnullptrなので実行されない
}

2. スマートポインタの使用

std::unique_ptr<int> ptr = std::make_unique<int>(42);
// ptrは自動的に管理される

3. 動的割り当てを避ける

// ヒープより
std::vector<int> vec(100);  // 自動的に管理

---

まとめ

本章で学んだこと:

  • new/delete: C++の動的メモリ管理、コンストラクタ/デストラクタの呼び出し
  • RAII: リソース管理の自動化、例外安全性
  • スマートポインタ: unique_ptr, shared_ptrの概念
  • デバッグツール: Valgrind, AddressSanitizer
  • メモリエラー: リーク、二重解放、ダングリングポインタ
  • 次章では、演算子オーバーロードとコピーセマンティクスについて学びます。

    ---

    練習問題

  • 以下のコードのメモリリークを修正してください:
void processData() {
    int* data = new int[1000];
    if (validateData(data) == false) {
        return;  // メモリリーク!
    }
    // 処理
    delete[] data;
}

  • RAIIクラスを作成して、以下の要件を満たしてください:
- コンストラクタでファイルを開く - デストラクタでファイルを閉じる - コピー禁止

  • unique_ptrとshared_ptrの違いを3つ挙げてください。
  • 以下のコードの問題を指摘してください:
int* ptr = new int[10];
delete ptr;