Menu
Coddy logo textTech

コピーコンストラクタ

CoddyのC++ジャーニー「オブジェクト指向プログラミング」セクションの一部 — レッスン 20/104。

コピーコンストラクタは、既存のオブジェクトのコピーとして新しいオブジェクトを作成します。これは、同じ型の別のオブジェクトからオブジェクトを初期化する場合、オブジェクトを値渡しする場合、またはオブジェクトを値で返す場合に呼び出されます。

コピーコンストラクタは、同じクラスのオブジェクトへの const 参照を受け取ります:

class Player {
    std::string name;
    int health;
public:
    Player(std::string n, int h) : name(n), health(h) {}
    
    // コピーコンストラクタ
    Player(const Player& other) {
        name = other.name;
        health = other.health;
    }
};

Player original("Hero", 100);
Player copy = original;    // コピーコンストラクタが呼び出される
Player another(original);  // これもコピーコンストラクタを呼び出す

コピーコンストラクタを定義しない場合、コンパイラはシャローコピー(浅いコピー)を行うものを生成します。これは各メンバの値を直接コピーするものです。これは単純な型の場合は問題なく動作しますが、クラスが動的メモリを管理している場合には問題を引き起こします。

class Buffer {
    int* data;
    size_t size;
public:
    Buffer(size_t s) : size(s) {
        data = new int[size];
    }
    
    // ディープコピーコンストラクタ
    Buffer(const Buffer& other) : size(other.size) {
        data = new int[size];              // 新しいメモリを割り当てる
        for (size_t i = 0; i < size; i++)
            data[i] = other.data[i];       // 内容をコピーする
    }
    
    ~Buffer() { delete[] data; }
};

カスタムコピーコンストラクタがない場合、両方のオブジェクトは同じメモリを指すことになります。一方が破棄されると、もう一方はダングリングポインタを持つことになります。ディープコピーは新しいメモリを割り当て、実際のデータをコピーすることで、各オブジェクトが独自のリソースを所有することを保証します。

challenge icon

チャレンジ

簡単

クラスが動的メモリを管理する際のディープコピーの重要性を示すメッセージ履歴システムを構築しましょう。動的に割り当てられた配列にメッセージを保存する MessageLog クラスを作成し、各コピーが独自の独立したメモリを持つように適切なコピーコンストラクタを実装します。

コードを整理するために2つのファイルを作成します:

  • MessageLog.h: 動的メモリを使用してメッセージのコレクションを管理する MessageLog クラスを定義します。クラスには以下のものを含める必要があります:
    • プライベートメンバ:メッセージを格納するための文字列配列へのポインタ、最大メッセージ数のための capacity、および現在のメッセージ数のための count
    • 容量(capacity)を受け取り、ヒープ上に配列を割り当て、count を 0 に初期化する引数付きコンストラクタ
    • ディープコピーを実行するコピーコンストラクタ — 新しいメモリを割り当て、ソースオブジェクトからすべてのメッセージをコピーします
    • 割り当てられたメモリを解放し、"MessageLog destroyed" と出力するデストラクタ
    • 空きがある場合にメッセージを追加する addMessage(string msg) メソッド
    • 現在のメッセージ数を返す getCount() メソッド
    • 指定されたインデックスのメッセージを返す getMessage(int index) メソッド
  • main.cpp: コピーコンストラクタが独立したコピーを作成することを確認します。入力から容量の値と2つのメッセージを読み取り、以下の手順を実行します:
    • 入力された容量で original という名前の MessageLog を作成します
    • original に両方のメッセージを追加します
    • コピーコンストラクタを使用して backup という名前のコピーを作成します
    • original にのみ3番目のメッセージ "New message" を追加します
    • "Original count: <count>" と出力します
    • "Backup count: <count>" と出力します
    • backup からすべてのメッセージを1行に1つずつ出力します

入力形式は以下の通りです:

  • 1行目:容量(整数)
  • 2行目:1番目のメッセージ(文字列)
  • 3行目:2番目のメッセージ(文字列)

ディープコピーが正しく実装されていれば、original には3つのメッセージがあるのに対し、backup には2つのメッセージが保持されます。これは、それらが独立したメモリを持っていることを証明します。適切なコピーコンストラクタがない場合、両方が同じ配列を共有してしまい、一方への変更が他方に影響を与えてしまいます!

main.cpp で #include "MessageLog.h" を使用してヘッダーファイルをインクルードしてください。

チートシート

コピーコンストラクタは、既存のオブジェクトのコピーとして新しいオブジェクトを作成します。これは、同じクラスのオブジェクトへの const 参照を受け取ります:

class Player {
    std::string name;
    int health;
public:
    Player(std::string n, int h) : name(n), health(h) {}
    
    // コピーコンストラクタ
    Player(const Player& other) {
        name = other.name;
        health = other.health;
    }
};

Player original("Hero", 100);
Player copy = original;    // コピーコンストラクタが呼び出される
Player another(original);  // これもコピーコンストラクタを呼び出す

コピーコンストラクタを定義しない場合、コンパイラは各メンバの値を直接コピーする浅いコピー(shallow copy)を行うコンストラクタを生成します。これは、クラスが動的メモリを管理している場合に問題を引き起こします。

深いコピー(deep copy)コンストラクタは、新しいメモリを割り当てて実際のデータをコピーし、各オブジェクトが独自のリソースを所有することを保証します:

class Buffer {
    int* data;
    size_t size;
public:
    Buffer(size_t s) : size(s) {
        data = new int[size];
    }
    
    // 深いコピーコンストラクタ
    Buffer(const Buffer& other) : size(other.size) {
        data = new int[size];              // 新しいメモリを割り当てる
        for (size_t i = 0; i < size; i++)
            data[i] = other.data[i];       // 内容をコピーする
    }
    
    ~Buffer() { delete[] data; }
};

カスタムコピーコンストラクタがないと、両方のオブジェクトが同じメモリを指すことになり、一方が破棄されたときにダングリングポインタが発生します。

自分で試してみよう

#include <iostream>
#include <string>
#include "MessageLog.h"
using namespace std;

int main() {
    // 入力を読み込む
    int capacity;
    cin >> capacity;
    cin.ignore();
    
    string msg1, msg2;
    getline(cin, msg1);
    getline(cin, msg2);

    // TODO: 入力された容量(capacity)で 'original' という名前の MessageLog を作成する

    // TODO: 両方のメッセージを original に追加する

    // TODO: コピーコンストラクタを使用して 'backup' という名前のコピーを作成する

    // TODO: original にのみ "New message" を追加する

    // TODO: "Original count: <count>" を出力する

    // TODO: "Backup count: <count>" を出力する

    // TODO: backup からすべてのメッセージを1行に1つずつ出力する

    return 0;
}
quiz icon腕試し

このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。

オブジェクト指向プログラミングのすべてのレッスン