第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;