[gemini-3-flash-preview]
情報工学を専攻する学生の皆さんに向けて、C++
における重要な設計指針である「三原則 (rule of
three)」について解説します。
C言語でポインタやメモリ管理を学んだ際の知識を思い出しながら、なぜ C++ においてこの原則が不可欠なのかを理解していきましょう。
C++ において、クラスがリソース(動的メモリ、ファイルハンドル、ネットワークソケットなど)を管理する場合、以下の 3 つの特別なメンバ関数をすべて適切に定義しなければならない、という設計上のガイドラインです。
「どれか 1 つでも自分で定義する必要があるなら、残り 2 つも定義しなければならない」というのがこのルールの核心です。
C言語で次のような構造体を定義したとします。
struct Buffer {
int *data;
int size;
};この構造体を別の変数に代入すると、何が起きるでしょうか?
struct Buffer b1;
b1.data = (int *)malloc(sizeof(int) * 10);
b1.size = 10;
struct Buffer b2 = b1; // 構造体のコピーC言語の仕様では、b2 = b1
は「メモリの単純なコピー(ビット単位のコピー)」を行います。その結果、b1.data
と b2.data
は全く同じメモリ番地を指すことになります。
ここで free(b1.data) を行うと、b2.data
が指す先も無効になり、b2
を使おうとするとプログラムがクラッシュします。これが、いわゆる浅いコピー
(shallow copy) の問題です。
C++
では、オブジェクトの寿命が尽きたときに自動的に呼び出される「デストラクタ」という仕組みがあります。これにより、C言語で頻発した「free
のし忘れ(メモリリーク)」を防ぐことができます。
しかし、デストラクタを導入すると、先ほどの「浅いコピー」が致命的な問題を引き起こします。
オブジェクトが破棄されるときに delete を実行します。
~MyArray() {
delete[] data; // Cの free() に相当
}新しいオブジェクトを既存のオブジェクトから作る際に呼び出されます。
ここで「新しいメモリ領域を確保し、中身を複製する」という深いコピー
(deep copy) を行わないと、同じメモリを 2 回 delete
してしまう「二重解放 (double free)」エラーが発生します。
既に存在するオブジェクトに、別のオブジェクトを代入する際に呼び出されます。
単に中身をコピーするだけでなく、「自分自身が持っていた古いメモリを解放する」処理も必要になります。
以下のクラスは、動的に整数配列を確保する簡単な例です。三原則をどのように実装すべきか見てみましょう。
class MyArray {
private:
int* data;
int size;
public:
// 通常のコンストラクタ
MyArray(int s) : size(s) {
data = new int[size];
}
// 1. デストラクタ:メモリを解放する
~MyArray() {
delete[] data;
}
// 2. コピーコンストラクタ:深いコピーを行う
MyArray(const MyArray& other) : size(other.size) {
data = new int[size]; // 新しいメモリを確保
for (int i = 0; i < size; ++i) {
data[i] = other.data[i]; // 中身をコピー
}
}
// 3. コピー代入演算子:古いメモリを捨ててから深いコピー
MyArray& operator=(const MyArray& other) {
if (this == &other) return *this; // 自己代入チェック
delete[] data; // 自分の古いメモリを解放
size = other.size;
data = new int[size]; // 新しく確保
for (int i = 0; i < size; ++i) {
data[i] = other.data[i];
}
return *this;
}
};C++ のコンパイラは、これら 3
つの関数を定義しなかった場合、自動的に「デフォルト」のものを生成します。しかし、デフォルトの動作は「メンバ変数をそのままコピーする(浅いコピー)」です。
ポインタを持つクラスにおいて、このデフォルト動作はほぼ確実にバグ(二重解放や不正アクセス)の原因となります。
「コピーコンストラクタ」と「代入演算子」は似ていますが、呼び出されるタイミングが異なります。
- MyArray a = b;
はコンストラクタ(新しいオブジェクトの生成)
- a = b;
は代入演算子(既存のオブジェクトの書き換え)
片方だけ実装し、もう片方を忘れると、特定のコードパスでだけクラッシュするような、見つけにくいバグを生みます。
現代の C++ (C++11 以降) では、この「三原則」はさらに進化しています。
効率化のために「メモリをコピーする」のではなく「所有権を移動させる」というムーブセマンティクス
(move semantics) が導入されました。これにより、以下の 2
つを加えた「五原則」を考慮することが推奨される場合があります。
- ムーブコンストラクタ
- ムーブ代入演算子
「そもそも生ポインタ (int*)
を使わず、std::vector や std::unique_ptr
などの標準ライブラリを活用せよ」という考え方です。これらを使えば、メモリ管理はライブラリ側が行ってくれるため、プログラマが三原則を自分で実装する必要はなくなります(=自作する関数が
0 個で済む)。
プログラミングにおいて、リソースの「所有権」がどこにあるのかを意識することは、安全で堅牢なシステムを構築するための第一歩です。