ホームに戻る
出典 :
特殊なコンストラクタ(C++) - 超初心者向けプログラミング入門 C++ コピーコンストラクタ【オブジェクトを使った初期化方法】
関連 :
参照型 演算子オーバーロード ムーブと右辺値参照
目次 :

コピーコンストラクタとは

クラス(または構造体、共用体)インスタンスを初期化する際に、既存の同型インスタンスを引数に取るコンストラクタのこと。
インスタンスへの代入(コピー)時に実行される。

デフォルトコピーコンストラクタ

コピーコンストラクタを定義せずにインスタンスの代入を行うと、メンバ変数がすべてコピーされる。
(Cにおける構造体代入と同様。)
これは、クラス定義時にデフォルトのコピーコンストラクタが自動生成されることによる。
// クラス定義 class SimpleClass { private: int number; std::string name; public: // コンストラクタ SimpleClass() { number = 0; } // コンストラクタ SimpleClass(int n, char *s) { number = n; name = s; } // (コピーコンストラクタは定義されていない) int getNumber() { return number; } void setNumber(int n) { number = n; } std::string getName() { return name; } void setName(char* s) { name = s; } }; void main() { // SimpleClass 型のインスタンス sc1 生成 // (通常のコンストラクタ実行) SimpleClass sc1(1, "John"); std::cout << "(sc1) number : " << sc1.getNumber() << std::endl; std::cout << "(sc1) name : " << sc1.getName() << std::endl; // SimpleClass 型のインスタンス sc2 生成 // (コピーコンストラクタ実行) SimpleClass sc2(sc1); std::cout << "(sc2) number : " << sc1.getNumber() << std::endl; std::cout << "(sc2) name : " << sc1.getName() << std::endl; // SimpleClass 型のインスタンス sc3 生成 // (コピーコンストラクタ実行) SimpleClass sc3 = sc1; std::cout << "(sc3) number : " << sc1.getNumber() << std::endl; std::cout << "(sc3) name : " << sc1.getName() << std::endl; }
実行結果 :
(sc1) number : 1 (sc1) name : John (sc2) number : 1 (sc2) name : John (sc3) number : 1 (sc3) name : John
上記の例では、SimpleClass 型のインスタンス sc1 を用いて、同じ型のインスタンス sc2 、sc3 を生成している。
( sc2 と sc3 で構文は異なるが、機能は等価である。)
このとき、SimpleClass 型はコピーコンストラクタが定義されていないためデフォルトのコピーコンストラクタが呼ばれ、
結果、sc2 および sc3 のメンバ変数の値は sc1 と等しくなる。

何故コピーコンストラクタが必要か

特にポインタ型変数を含む場合、デフォルトのコピーコンストラクタを用いることで問題が生じることがある。
// ポインタ変数を有する PointerClass 定義 class PointerClass { int* pointer; //< ポインタ変数 public: // コンストラクタ PointerClass(int* p = 0) { pointer = p; } // (コピーコンストラクタは定義されていない) int *get() { return pointer; } }; void main() { int num = 10; // PointerClass 型インスタンス pc1 生成 PointerClass pc1(&num); // PointerClass 型インスタンス pc2 生成 // (デフォルトコピーコンストラクタによるコピー) PointerClass pc2(pc1); std::cout << "pc1値 : " << *(pc1.get()) << std::endl; std::cout << "pc1ポインタ : " << pc1.get() << std::endl; std::cout << "pc2値 : " << *(pc2.get()) << std::endl; std::cout << "pc2ポインタ : " << pc2.get() << std::endl; // pc2から値を書き換え *(pc2.get()) = 20; std::cout << "pc1値 : " << *(pc1.get()) << std::endl; std::cout << "pc1ポインタ : " << pc1.get() << std::endl; std::cout << "pc2値 : " << *(pc2.get()) << std::endl; std::cout << "pc2ポインタ : " << pc2.get() << std::endl; }
実行結果 :
pc1値 : 10 pc1ポインタ : 00CFFA38 pc2値 : 10 pc2ポインタ : 00CFFA38 pc1値 : 20 //< pc2 に加えた変更が波及している pc1ポインタ : 00CFFA38 pc2値 : 20 pc2ポインタ : 00CFFA38
上記は、ポインタ型変数を有するクラスの例である。
デフォルトコピーコンストラクタはメンバ変数を全てコピーするため、pc1 の pointer (アドレス値)と pc2 の pointer は等しくなる。
同一のアドレスを両者で共有しているため、一方で pointer の指し示す値を変更すると他方にも波及してしまう。
// ポインタ変数を有する PointerClass 定義 class PointerClass { int* pointer; //< ポインタ変数 public: // コンストラクタ PointerClass(int num) { pointer = new int(num); } // (コピーコンストラクタは定義されていない) int *get() { return pointer; } private: // デストラクタ ~PointerClass() { delete pointer; } }; void main() { // PointerClass 型インスタンス pc1 生成 PointerClass pc1(10); // PointerClass 型インスタンス pc2 生成 // (デフォルトコピーコンストラクタによるコピー) PointerClass pc2(pc1); : // ここで実行時エラーとなる }
上記は、ポインタ型変数に動的メモリを割り当てている例である。
main() 関数を抜ける際に、ローカル変数である pc1 、pc2 は破棄されるが、その際にデストラクタによって動的メモリの破棄が行われる。
しかし先の例と同様 pc1 と pc2 の pointer (アドレス値)は等しいため、一方で解放したメモリを他方でも解放しようとし、エラーとなる。

コピーコンストラクタを定義し、ポインタ変数の値を共用しないようにすることで、これらの事態を防ぐことができる

コピーコンストラクタの定義

// ポインタ変数を有する PointerClass 定義 class PointerClass { int* pointer; //< ポインタ変数 public: // コンストラクタ PointerClass(int num) { pointer = new int(num); } // コピーコンストラクタ PointerClass(const PointerClass& pc) { pointer = new int( *(pc.get()) ); } int *get() { return pointer; } private: // デストラクタ ~PointerClass() { delete pointer; } }; void main() { // PointerClass 型インスタンス pc1 生成 PointerClass pc1(10); // PointerClass 型インスタンス pc2 生成 // (コピーコンストラクタによるコピー) PointerClass pc2(pc1); : }
コピーコンストラクタは、自身と同型のインスタンスの参照を引数として受け取る。
(引数を誤って更新しないよう、const 修飾をするのが望ましい。参照型についてはここを参照。)
コピーコンストラクタ内でメモリの確保を新たに行っているため、pc1 と pc2 の pointer (アドレス値)は異なる値となる。
(ただし、 pointer の指し示す値は等しい。)

関数の引数とコピーコンストラクタ

関数の引数にオブジェクトを指定した場合もオブジェクトのコピーが作成されるため、コピーコンストラクタが呼ばれる。
ただ、関数にオブジェクトをそのまま渡すことはオーバーヘッドの増大を招くため、そもそも避けられるべきである。

はしがき

コピーコンストラクタの管理は煩雑となりがちだが、参照渡しを用いるなど、コピーコンストラクタに依存しない設計とすることも可能である。
コピーコンストラクタが必要となるのはポインタ変数を含む場合が多い。ポインタ自体、扱いが煩雑で危険を伴うものであるため、
ポインタの使用を避けることでコピーコンストラクタを削減するとともに、コードの安全性を高めることができる。