C++: コピー制御
最終更新:2026-08-31
レッスン29ではコンストラクタの応用を学びました。
でもオブジェクトのコピー、代入演算子、オブジェクトの移動、C++にはルールがあります。
これを理解すると、安全なクラスが書けます。
1. 三五則(Rule of Three/Five)
(1) 1.1 三五則とは?
安全なクラスを設計するには、以下の関数を定義するか、定義しないかを明確に:
| ルール | 関数 | 説明 |
|---|---|---|
| 三則(C++98) | 1. デストラクタ 2. コピーコンストラクタ 3. コピー代入演算子 |
動的メモリがある場合 |
| 五則(C++11) | + 4. ムーブコンストラクタ 5. ムーブ代入演算子 |
C++11のムーブセマンティクス |
(2) 1.2 なぜ三五則が必要?
三五則を守らないと、クラスにリソース(動的メモリ、ファイル)がある場合、重大なバグが起きます。
例:シャローコピーの問題(難易度 ⭐⭐)
▶ サンプル 2:シャローコピーの問題(難易度 ⭐)
CPP
#include <iostream>
#include <cstring>
class BadString {
private:
char* data;
public:
// コンストラクタ
BadString(const char* s) {
data = new char[strlen(s) + 1];
strcpy(data, s);
}
// ❌ コピーコンストラクタ、代入演算子、デストラクタを定義しない
// (シャローコピー)
};
int main() {
BadString s1("Hello");
BadString s2 = s1; // ❌ シャローコピー:s1.dataとs2.dataが同じアドレス
// main終了時、s2が先に破棄(dataを解放)
// s1が次に破棄(dataは既に解放済み)→ 二重解放エラー!
return 0;
}
結果: プログラムがクラッシュ(二重解放)。
解決策: 三五則に従い、必要な関数を定義。
2. コピー代入演算子
(1) 2.1 基本的な使い方
CPP
#include <iostream>
#include <cstring>
class MyString {
private:
char* data;
public:
// コンストラクタ
MyString(const char* s) {
data = new char[strlen(s) + 1];
strcpy(data, s);
}
// コピーコンストラクタ
MyString(const MyString& other) {
data = new char[strlen(other.data) + 1];
strcpy(data, other.data);
}
// コピー代入演算子
MyString& operator=(const MyString& other) {
if (this != &other) { // 自己代入チェック
delete data; // 古いメモリを解放
data = new char[strlen(other.data) + 1];
strcpy(data, other.data);
}
return *this;
}
// デストラクタ
~MyString() {
delete data;
}
};
int main() {
MyString s1("Hello");
MyString s2("World");
s2 = s1; // ✅ コピー代入演算子呼び出し
return 0;
}
💡 ヒント: コピー代入演算子は自己代入チェックを忘れない(
if (this != &other))。
3. ムーブセマンティクス(C++11)
(1) 3.1 なぜムーブセマンティクスが必要?
一時オブジェクト(右辺値)から新しいオブジェクトを作る場合、コピーは無駄 —— 「盗む」だけで十分。
例:ムーブセマンティクスで無駄なコピーを回避(難易度 ⭐⭐)
TEXT
📖 参照専用
#include <iostream>
#include <cstring>
class MyString {
private:
char* data;
public:
// ムーブコンストラクタ(C++11)
MyString(MyString&& other) noexcept {
data = other.data; // リソースを盗む
other.data = nullptr; // 元のオブジェクトを空にする
}
// ムーブ代入演算子(C++11)
MyString& operator=(MyString&& other) noexcept {
if (this != &other) {
delete data; // 古いメモリを解放
data = other.data; // リソースを盗む
other.data = nullptr; // 元のオブジェクトを空にする
}
return *this;
}
};
int main() {
MyString s1("Hello");
MyString s2 = std::move(s1); // ✅ ムーブコンストラクタ呼び出し
// s1.dataはnullptr、s2.dataがリソースを持つ
return 0;
}
💡 ヒント:
std::move() は左辺値を右辺値参照にキャスト、ムーブコンストラクタやムーブ代入演算子を呼び出す。
4. = default と = delete
(1) 4.1 = default でコンパイラ生成の実装
安全なクラスでデフォルト実装が必要な場合、= default でコンパイラに任せます。
CPP
#include <iostream>
#include <string>
class Student {
private:
std::string name;
int age;
public:
// コピーコンストラクタ、コピー代入演算子、デストラクタ
Student(const Student& other) = default;
Student& operator=(const Student& other) = default;
~Student() = default;
};
int main() {
Student s1;
Student s2 = s1; // ✅ デフォルトコピーコンストラクタ呼び出し
return 0;
}
💡 ヒント: クラスが STL 型(
std::string、std::vector など)だけの場合、= default でOK。
(2) 4.2 = delete でコピーを禁止
コピーを禁止したいクラス(シングルトン、リソース管理)の場合、= delete を使います。
CPP
#include <iostream>
class Singleton {
private:
Singleton() {} // プライベートコンストラクタ
public:
// コピーを禁止
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton& getInstance() {
static Singleton instance;
return instance;
}
};
int main() {
Singleton& s = Singleton::getInstance();
// Singleton s2 = s; // ❌ コンパイルエラー:コピーコンストラクタが削除
return 0;
}
5. 実践例:完全な MyString クラス
▶ サンプル 1:完全な MyString クラスの実装(難易度 ⭐⭐⭐)
CPP
📖 参照専用
#include <iostream>
#include <cstring>
class MyString {
private:
char* data;
int length;
public:
// コンストラクタ
MyString(const char* s = "") {
length = strlen(s);
data = new char[length + 1];
strcpy(data, s);
}
// コピーコンストラクタ
MyString(const MyString& other) {
length = other.length;
data = new char[length + 1];
strcpy(data, other.data);
}
// ムーブコンストラクタ
MyString(MyString&& other) noexcept {
data = other.data;
length = other.length;
other.data = nullptr;
other.length = 0;
}
// コピー代入演算子
MyString& operator=(const MyString& other) {
if (this != &other) {
delete data;
length = other.length;
data = new char[length + 1];
strcpy(data, other.data);
}
return *this;
}
// ムーブ代入演算子
MyString& operator=(MyString&& other) noexcept {
if (this != &other) {
delete data;
data = other.data;
length = other.length;
other.data = nullptr;
other.length = 0;
}
return *this;
}
// デストラクタ
~MyString() {
delete data;
}
// ユーティリティ関数
void print() const {
if (data) std::cout << data;
}
int size() const {
return length;
}
};
int main() {
MyString s1("Hello");
MyString s2 = s1; // コピーコンストラクタ呼び出し
s1.print(); // Hello
std::cout << std::endl;
s2.print(); // Hello
std::cout << std::endl;
return 0;
}
▶ サンプル 3:= delete でコピーを禁止(難易度 ⭐⭐)
CPP
#include <iostream>
#include <string>
class UniqueResource {
private:
std::string name;
int resourceId;
public:
UniqueResource(const std::string& n, int id)
: name(n), resourceId(id) {}
// コピーを禁止
UniqueResource(const UniqueResource&) = delete;
UniqueResource& operator=(const UniqueResource&) = delete;
// ムーブは許可
UniqueResource(UniqueResource&& other) noexcept
: name(std::move(other.name)), resourceId(other.resourceId) {
other.resourceId = -1;
}
void print() const {
std::cout << name << " (ID: " << resourceId << ")" << std::endl;
}
};
int main() {
UniqueResource r1("Database", 101);
r1.print();
UniqueResource r2 = std::move(r1); // ムーブは許可
r2.print();
// UniqueResource r3 = r2; // エラー:コピーは削除
return 0;
}
出力:
TEXT 📖 参照専用Database (ID: 101) Database (ID: 101)
❓ よくある質問
Q コピーコンストラクタを定義すべき?
A クラスにポインタメンバー(動的メモリ)がある場合、必須。
クラスが基本型(int、double)や STL 型(std::string、std::vector)だけの場合、定義不要
Q コピーコンストラクタを書かないとどうなる?
A コンパイラがコピーコンストラクタを自動生成(メンバーごとのコピー)。
でもクラスにポインタメンバーがある場合、シャローコピーになる —— ポインタの値だけコピー
📖 まとめ
- 三五則:デストラクタ、コピーコンストラクタ、コピー代入演算子のいずれかを定義する場合、残りも定義
- 五則:C++11では、ムーブコンストラクタ、ムーブ代入演算子も追加
= defaultでコンパイラに実装を任せる= deleteで関数を禁止- ムーブセマンティクスで無駄なコピーを回避
📝 練習問題
-
初級(難易度 ⭐):
Pointクラスを定義し、x、y メンバーを持たせてください。- コピーコンストラクタ、コピー代入演算子、デストラクタを定義
mainでテスト
-
中級(難易度 ⭐⭐):
DynamicArrayクラスを定義(動的配列)、以下を持つ:int* data(ヒープ上の配列)int size(配列サイズ)、必要なメンバー関数を実装。
-
上級(難易度 ⭐⭐⭐):
DynamicArrayクラスを拡張:void push_back(int x):配列の末尾に追加(容量不足なら2倍に拡張)void pop_back():末尾を削除
6. 🚀 次のステップ
コピー制御とムーブセマンティクスを理解したら、次は 継承(レッスン31) —— クラス間で「親子」関係を作り、コードを再利用しよう!