C++での三数法則

適切なリソース処理でメモリ管理をマスター

C++でクラスのメモリリソースを適切に管理するには、3つの重要な関数を実装する必要があります:デストラクタコピーコンストラクタコピー代入演算子。基本概念を復習する必要がありますか?数学的原理ガイドをご覧ください。他の実装を探していますか?Excelの公式またはPythonスクリプトのガイドをお試しください、または迅速な計算のために計算機を使用してください。

自分で試してみよう:三数法則の実装

C++での三数法則の動作を確認:


class StringWrapper {
private:
    char* data;

public:
    // コンストラクタ
    StringWrapper(const char* str) {
        data = new char[strlen(str) + 1];
        strcpy(data, str);
    }

    // デストラクタ
    ~StringWrapper() {
        delete[] data;
    }

    // コピーコンストラクタ
    StringWrapper(const StringWrapper& other) {
        data = new char[strlen(other.data) + 1];
        strcpy(data, other.data);
    }

    // コピー代入演算子
    StringWrapper& operator=(const StringWrapper& other) {
        if (this != &other) {
            delete[] data;
            data = new char[strlen(other.data) + 1];
            strcpy(data, other.data);
        }
        return *this;
    }

    // デモ用のゲッター
    const char* getData() const { return data; }
};

int main() {
    // 元のオブジェクトを作成
    StringWrapper str1("こんにちは");
    
    // コピーコンストラクタを使用
    StringWrapper str2 = str1;
    
    // 代入演算子を使用
    StringWrapper str3("世界");
    str3 = str1;
    
    return 0;
}

メモリ管理の詳細


// 適切なメモリ管理の例
class ResourceManager {
private:
    int* data;
    size_t size;

public:
    // コンストラクタ
    ResourceManager(size_t n) : size(n) {
        data = new int[size];
        std::fill_n(data, size, 0);
    }

    // デストラクタ
    ~ResourceManager() {
        delete[] data;
    }

    // コピーコンストラクタ
    ResourceManager(const ResourceManager& other) : size(other.size) {
        data = new int[size];
        std::copy(other.data, other.data + size, data);
    }

    // コピー代入演算子
    ResourceManager& operator=(const ResourceManager& other) {
        if (this != &other) {
            // 自己代入を処理するための一時変数を作成
            int* temp = new int[other.size];
            std::copy(other.data, other.data + other.size, temp);
            
            // 古いリソースをクリーンアップ
            delete[] data;
            
            // 新しいリソースの所有権を取得
            data = temp;
            size = other.size;
        }
        return *this;
    }
};

よくある落とし穴

1. 自己代入チェックの欠如


// 誤り:
MyClass& operator=(const MyClass& other) {
    delete[] data;  // おっと!データを削除してしまいました
    data = new int[other.size];  // otherが*thisの場合、削除されたデータを使用しています
    // ...
    return *this;
}

// 正しい:
MyClass& operator=(const MyClass& other) {
    if (this != &other) {  // 自己代入をチェック
        delete[] data;
        data = new int[other.size];
        // ...
    }
    return *this;
}

2. コンストラクタの失敗を処理しない


// 誤り:
MyClass(const MyClass& other) {
    data = new int[other.size];  // 例外をスローする可能性
    size = other.size;  // 上記がスローした場合、sizeが間違っています
}

// 正しい:
MyClass(const MyClass& other) : size(0), data(nullptr) {
    int* temp = new int[other.size];  // 例外をスローする可能性
    // ここに到達した場合、割り当ては成功しています
    data = temp;
    size = other.size;
}

3. 浅いコピー vs 深いコピー


// 誤り(浅いコピー):
MyClass(const MyClass& other) {
    data = other.data;  // 両方のオブジェクトが同じポインタを共有します!
}

// 正しい(深いコピー):
MyClass(const MyClass& other) {
    data = new int[other.size];
    std::copy(other.data, other.data + other.size, data);
}

モダンC++の代替案

五の法則(C++11以降)

C++11以降では、移動セマンティクスを含む五の法則も考慮する必要があります:


class ModernClass {
private:
    int* data;
    size_t size;

public:
    // コンストラクタ
    ModernClass(size_t n) : size(n), data(new int[n]) {}

    // デストラクタ
    ~ModernClass() { delete[] data; }

    // コピーコンストラクタ
    ModernClass(const ModernClass& other) : size(other.size) {
        data = new int[size];
        std::copy(other.data, other.data + size, data);
    }

    // コピー代入演算子
    ModernClass& operator=(const ModernClass& other) {
        if (this != &other) {
            ModernClass temp(other);
            std::swap(data, temp.data);
            std::swap(size, temp.size);
        }
        return *this;
    }

    // ムーブコンストラクタ
    ModernClass(ModernClass&& other) noexcept 
        : data(other.data), size(other.size) {
        other.data = nullptr;
        other.size = 0;
    }

    // ムーブ代入演算子
    ModernClass& operator=(ModernClass&& other) noexcept {
        if (this != &other) {
            delete[] data;
            data = other.data;
            size = other.size;
            other.data = nullptr;
            other.size = 0;
        }
        return *this;
    }
};

スマートポインタを使用(推奨)

モダンC++では、スマートポインタを使用して手動メモリ管理を回避することを強く推奨します:


#include 
#include 

class SmartClass {
private:
    std::unique_ptr data;
    size_t size;

public:
    // コンストラクタ
    SmartClass(size_t n) 
        : data(std::make_unique(n)), size(n) {}

    // デフォルトのデストラクタが機能します
    // コンパイラ生成のコピー/ムーブ操作が機能します
    // またはstd::vectorをさらに簡単に使用:
};

class EvenBetter {
private:
    std::vector data;

public:
    EvenBetter(size_t n) : data(n) {}
    // すべて自動的に処理されます!
};

重要なポイント

  • クラスが動的メモリを管理する場合、デストラクタ、コピーコンストラクタ、コピー代入演算子を実装します
  • 常に自己代入をチェックし、例外安全性を考慮します
  • モダンC++では、スマートポインタまたはSTLコンテナを使用して手動メモリ管理を回避します
  • C++11以降を使用する場合は、ムーブセマンティクスのための五の法則も考慮します
  • 可能な限り、生のポインタよりもRAII原則とスマートポインタを優先します