コピーコンストラクタ
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; }
};カスタムコピーコンストラクタがない場合、両方のオブジェクトは同じメモリを指すことになります。一方が破棄されると、もう一方はダングリングポインタを持つことになります。ディープコピーは新しいメモリを割り当て、実際のデータをコピーすることで、各オブジェクトが独自のリソースを所有することを保証します。
チャレンジ
簡単クラスが動的メモリを管理する際のディープコピーの重要性を示すメッセージ履歴システムを構築しましょう。動的に割り当てられた配列にメッセージを保存する 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;
}
このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。
オブジェクト指向プログラミングのすべてのレッスン
3コンストラクタとデストラクタ
デフォルトコンストラクタ引数付きコンストラクタコピーコンストラクタムーブコンストラクタコンストラクタ初期化リスト委譲コンストラクタデストラクタ詳解Rule of Three / Five / Zeroまとめ - Stringクラス6演算子オーバーロード
演算子オーバーロード入門算術演算子のオーバーロード比較演算子のオーバーロードストリーム演算子代入演算子のオーバーロード[] と () 演算子のオーバーロード型変換演算子まとめ:Matrix クラス