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::stringstd::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;
}
論理コード 59 行(40 行制限超過、参照専用)

▶ サンプル 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 クラスにポインタメンバー(動的メモリ)がある場合、必須。

クラスが基本型(intdouble)や STL 型(std::stringstd::vector)だけの場合、定義不要

Q コピーコンストラクタを書かないとどうなる?
A コンパイラがコピーコンストラクタを自動生成(メンバーごとのコピー)。

でもクラスにポインタメンバーがある場合、シャローコピーになる —— ポインタの値だけコピー


📖 まとめ

  • 三五則:デストラクタ、コピーコンストラクタ、コピー代入演算子のいずれかを定義する場合、残りも定義
  • 五則:C++11では、ムーブコンストラクタ、ムーブ代入演算子も追加
  • = default でコンパイラに実装を任せる
  • = delete で関数を禁止
  • ムーブセマンティクスで無駄なコピーを回避

📝 練習問題

  1. 初級(難易度 ⭐): Point クラスを定義し、x、y メンバーを持たせてください。

    • コピーコンストラクタ、コピー代入演算子、デストラクタを定義
    • main でテスト
  2. 中級(難易度 ⭐⭐): DynamicArray クラスを定義(動的配列)、以下を持つ:

    • int* data(ヒープ上の配列)
    • int size(配列サイズ)、必要なメンバー関数を実装。
  3. 上級(難易度 ⭐⭐⭐): DynamicArray クラスを拡張:

    • void push_back(int x):配列の末尾に追加(容量不足なら2倍に拡張)
    • void pop_back():末尾を削除


6. 🚀 次のステップ

コピー制御とムーブセマンティクスを理解したら、次は 継承(レッスン31) —— クラス間で「親子」関係を作り、コードを再利用しよう!

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%