クラスの応用_コピーコンストラクタと代入演算子のオーバーロード 〜静的オブジェクトと動的オブジェクトのコピー〜
静的オブジェクトと動的オブジェクトのコピー時の挙動が異なっていたので整理する。
その過程で、コピーコンストラクタと代入演算子の使い方をみてみる。
静的オブジェクト、動的オブジェクトのコピー
<静的メモリの場合>
先に動きを確認する。
【プログラム】
#include <iostream> using namespace std; class TEST { private: int num; public: TEST(int n = 0) : num(n) {} ~TEST(){} int getNum(){ return num; } }; int main(){ TEST t1(123); TEST t2(456); TEST t3; cout << "t1のnumの値 : " << t1.getNum() << endl; cout << "t1のアドレス : " << &t1 << endl; cout << "t2のnumの値 : " << t2.getNum() << endl; cout << "t2のアドレス : " << &t2 << endl; cout << "t3のnumの値 : " << t3.getNum() << endl; cout << "t3のアドレス : " << &t3 << endl; cout << endl; t2 = t1; // 代入 ①代入演算子が呼ばれている t3 = t1; TEST t4 = t1; // 初期化 ②コピーコンストラクタが呼ばれている cout << "t2,t3,t4にそれぞれt1を代入しました(t4のみ宣言ど同時にt1で初期化)" << endl; cout << endl; cout << "t1のnumの値 : " << t1.getNum() << endl; cout << "t1のアドレス : " << &t1 << endl; cout << "t2のnumの値 : " << t2.getNum() << endl; cout << "t2のアドレス : " << &t2 << endl; cout << "t3のnumの値 : " << t3.getNum() << endl; cout << "t3のアドレス : " << &t3 << endl; cout << "t4のnumの値 : " << t4.getNum() << endl; cout << "t4のアドレス : " << &t4 << endl; return 0; }
【実行結果】
$ t1のnumの値 : 123 t1のアドレス : 0x7fff5c883988 t2のnumの値 : 456 t2のアドレス : 0x7fff5c883980 t3のnumの値 : 0 t3のアドレス : 0x7fff5c883970 t2,t3,t4にそれぞれt1を代入しました(t4のみ宣言ど同時にt1で初期化)123 t1のnumの値 : 123 t1のアドレス : 0x7fff5c883988 t2のnumの値 : 123 t2のアドレス : 0x7fff5c883980 t3のnumの値 : 123 t3のアドレス : 0x7fff5c883970 t4のnumの値 : 123 t4のアドレス : 0x7fff5c883968 $
このように、静的メモリはどのように操作してもオブジェクト生成時に必ず新しいメモリ領域を確保する。
同時に、その後別のオブジェクトをコピー(代入や初期化)して上書きしても最初に確保したメモリは変わらない。(初期化時に既存の別のオブジェクトで初期化しても、メモリ領域だけは新たに確保する。)
ただし、データメンバ(メンバ変数)の値は変わる。
しかし、動的メモリを確保して生成したオブジェクトをコピーすると同じアドレスを指す。
<動的メモリの場合>
【プログラム】
#include <iostream> using namespace std; class TEST { private: int num; public: TEST(int n) : num(n) {} ~TEST(){} int getNum(){ return num; } }; int main(){ TEST *t1 = new TEST(123); TEST *t2 = new TEST(456); TEST *t3; cout << "t1のnumの値 : " << t1->getNum() << endl; cout << "t1のアドレス : " << t1 << endl; cout << "t2のnumの値 : " << t2->getNum() << endl; cout << "t2のアドレス : " << t2 << endl; // オブジェクトがないのでコンパイルエラー // cout << "t3のnumの値 : " << t3->getNum() << endl; // cout << "t3のアドレス : " << t3 << endl; cout << endl; t2 = t1; // 単なるアドレスのコピー ③ t3 = t1; // (同上) TEST *t4 = t1; // (同上) cout << "t2,t3,t4にそれぞれt1を代入しました(t4のみ宣言と同時にt1で初期化)" << endl; cout << "t1のnumの値 : " << t1->getNum() << endl; cout << "t1のアドレス : " << t1 << endl; cout << "t2のnumの値 : " << t2->getNum() << endl; cout << "t2のアドレス : " << t2 << endl; cout << "t3のnumの値 : " << t3->getNum() << endl; cout << "t3のアドレス : " << t3 << endl; cout << "t4のnumの値 : " << t4->getNum() << endl; cout << "t4のアドレス : " << t4 << endl; cout << endl; return 0; }
【実行結果】
$ t1のnumの値 : 123 t1のアドレス : 0x7fe67b404ae0 t2のnumの値 : 456 t2のアドレス : 0x7fe67b404af0 t2,t3,t4にそれぞれt1を代入しました(t4のみ宣言と同時にt1で初期化) t1のnumの値 : 123 t1のアドレス : 0x7fe67b404ae0 t2のnumの値 : 123 t2のアドレス : 0x7fe67b404ae0 t3のnumの値 : 123 t3のアドレス : 0x7fe67b404ae0 t4のnumの値 : 123 t4のアドレス : 0x7fe67b404ae0 $
静的メモリ
まず、静的メモリについて確認する。
オブジェクト生成時に必ず新しいメモリ領域を確保しつつ、データメンバはコピーされていた。
そもそも、オブジェクトから別のオブジェクトにコピーする際には代入演算子が、オブジェクトをコピーして新しいオブジェクトを生成する際にはコピーコンストラクタが呼ばれる。
今回は自分で代入演算子やコピーコンストラクタを定義していないので、コンパイラが自動的に作成してくれている。
コンパイラが準備した代入演算子やコピーコンストラクタは、基本的な使い方をする限り(基本的、というと語弊があるかもしれないが)、そのままオブジェクトをまるっとコピーしてくれる。
オブジェクトが代入演算子「=」(プログラム内①)やコピーコンストラクタ(プログラム内②)によって値渡しされているものと思われる。
結論だけいうと、自動で異なるメモリ領域を確保
動的メモリ
次に、動的メモリについて。
動的メモリのプログラムを見ると分かるが(プログラム内③)、単純にアドレスをコピーしているだけだ。
なので細かく言及する必要はないだろう。
メンバ変数がポインタのとき
問題があるのは、オブジェクトの内部にメンバ変数としてポインタを持っているとき。
下記を実行するとエラーになる。
【プログラム】
#include <iostream> #include <string> using namespace std; class TEST { private: char *c; public: TEST(char *str = "zzz"){ c = new char[strlen(str)+1]; strcpy(c, str); } ~TEST(){ cout << "デストラクタ実行" << endl; delete c; } char* getC(){ return c; } }; int main(){ TEST t1("abc"); TEST t2("def"); cout << "t1のcの値 : " << t1.getC() << endl; cout << "t1のcのアドレス : " << (void*)t1.getC() << endl; cout << "t1のアドレス : " << &t1 << endl; cout << "t2のcの値 : " << t2.getC() << endl; cout << "t2のcのアドレス : " << (void*)t2.getC() << endl; cout << "t2のアドレス : " << &t2 << endl; cout << endl; t2 = t1; // ④ 代入 TEST t3 = t1; // ⑤ 初期化 cout << "t2,t3にt1を代入しました" << endl; cout << "t1のcの値 : " << t1.getC() << endl; cout << "t1のcのアドレス : " << (void*)t1.getC() << endl; cout << "t1のアドレス : " << &t1 << endl; cout << "t2のcの値 : " << t2.getC() << endl; cout << "t2のcのアドレス : " << (void*)t2.getC() << endl; cout << "t2のアドレス : " << &t2 << endl; cout << "t3のcの値 : " << t3.getC() << endl; cout << "t3のcのアドレス : " << (void*)t3.getC() << endl; cout << "t3のアドレス : " << &t3 << endl; cout << endl; return 0; }
【実行結果】
$ t1のcの値 : abc t1のcのアドレス : 0x7fc413c04ae0 t1のアドレス : 0x7fff58676980 t2のcの値 : def t2のcのアドレス : 0x7fc413c04af0 t2のアドレス : 0x7fff58676978 t2,t3にt1を代入しました t1のcの値 : abc t1のcのアドレス : 0x7fc413c04ae0 // ⑥A t1のアドレス : 0x7fff58676980 t2のcの値 : abc t2のcのアドレス : 0x7fc413c04ae0 // ⑥B t2のアドレス : 0x7fff58676978 t3のcの値 : abc t3のcのアドレス : 0x7fc413c04ae0 // ⑥C t3のアドレス : 0x7fff58676960 デストラクタ実行 デストラクタ実行 errVer(4479,0x7fff74237300) malloc: *** error for object 0x7fc413c04ae0: pointer being freed was not allocated *** set a breakpoint in malloc_error_break to debug Abort trap: 6 $
上記のようにコピーするとクラスのメンバ変数の向き先が全て同じになってしまう。
結果として、デストラクタが3回実行される際にすでに1回目でdeleteされたメモリ領域をdeleteしようとするのでエラーとなる。
また、delete時だけでなく、値を書き変えた時に
そこを指しているポインタは全て同じ値になってしまうので、コピーして別のオブジェクトを作成して色々使いわけたいときに不都合が生じてしまう。
初期化時のコピー時に新たに領域確保する「コピーコンストラクタ」
まず最初に上記の⑤の初期化を見てみる。
初期化されるタイミングで新たに領域を確保すれば、別領域なので前述したようにdeleteしても問題ないはずだ。
そこでコピーコンストラクタを作成する。
要は、コピーによる初期化時にはこっちのコンストラクタを実行してくださいねということのようだ。
コピーコンストラクタで受け取ったオブジェクトと同じオブジェクトが作成されるように設計するだけの話。ここまでたどり着くのは大変だったが、わかってしまえば簡単。
クラス名(const クラス名& 引数){ // 引数でコピー元のオブジェクトを受け取る 〜ここに受け取ったオブジェクトと同じオブジェクトが作成される処理をつくる〜 }
【プログラム】
#include <iostream> #include <string> using namespace std; class TEST { private: char *c; public: TEST(char *str = "zzz"){ c = new char[strlen(str)+1]; strcpy(c, str); } // コピーコンストラクタ TEST(const TEST& t){ cout << "コピーコンストラクタ実行" << endl; c = new char[strlen(t.c)+1]; // ここでコピー元のメンバ変数と同じ大きさの動的メモリ領域確保 strcpy(c, t.c); } ~TEST(){ cout << "デストラクタ実行" << endl; delete c; } char* getC(){ return c; } }; int main(){ TEST t1("abc"); TEST t2("def"); cout << "t1のcの値 : " << t1.getC() << endl; cout << "t1のcのアドレス : " << (void*)t1.getC() << endl; cout << "t1のアドレス : " << &t1 << endl; cout << "t2のcの値 : " << t2.getC() << endl; cout << "t2のcのアドレス : " << (void*)t2.getC() << endl; cout << "t2のアドレス : " << &t2 << endl; cout << endl; t2 = t1; TEST t3 = t1; cout << "t2,t1を代入しました" << endl; cout << "t1のcの値 : " << t1.getC() << endl; cout << "t1のcのアドレス : " << (void*)t1.getC() << endl; cout << "t1のアドレス : " << &t1 << endl; cout << "t2のcの値 : " << t2.getC() << endl; cout << "t2のcのアドレス : " << (void*)t2.getC() << endl; cout << "t2のアドレス : " << &t2 << endl; cout << "t3のcの値 : " << t3.getC() << endl; cout << "t3のcのアドレス : " << (void*)t3.getC() << endl; cout << "t3のアドレス : " << &t3 << endl; cout << endl; return 0; }
$ t1のcの値 : abc t1のcのアドレス : 0x7ff42bd04a60 t1のアドレス : 0x7fff5d10b980 t2のcの値 : def t2のcのアドレス : 0x7ff42bd04a70 t2のアドレス : 0x7fff5d10b978 コピーコンストラクタ実行 t2,t1を代入しました t1のcの値 : abc t1のcのアドレス : 0x7ff42bd04a60 // ⑦A t1のアドレス : 0x7fff5d10b980 t2のcの値 : abc t2のcのアドレス : 0x7ff42bd04a60 // ⑦B t2のアドレス : 0x7fff5d10b978 t3のcの値 : abc t3のcのアドレス : 0x7ff42bd04a80 // ⑦C t3のアドレス : 0x7fff5d10b960 デストラクタ実行 デストラクタ実行 デストラクタ実行 // ⑦D okVer(4492,0x7fff74237300) malloc: *** error for object 0x7ff42bd04a60: pointer being freed was not allocated *** set a breakpoint in malloc_error_break to debug Abort trap: 6 $
さっき(⑥A〜C)との違いは、「初期化したときのt3のcのアドレス(⑥C)が同じではなくなっている」ということだ。
コピーコンストラクタを自分で設計したことで、初期化時は内部のポインタをコピーせずに、新たな領域確保するようになった。
また、もう一点大事なのは⑦Dをみると分かるが、デストラクタが前回2回しか実行していなかったが、今回は3回目が実行されている(厳密には、3回目のメッセージが出ているだけで、3回目のデストラクタは途中でエラーになっている)
コンストラクタが作成された順番と逆順にデストラクタが実行される(らしい)ので3回目で失敗したということはすなわち、
⑦Cに対してデストラクタを実行
↓
⑦Bに対してデストラクタを実行 ※⑦Cと異なるアドレスなのでdelete可
↓
⑦Aに対してデストラクタを実行時に失敗 ※⑦Aとは異なるが、⑦Cと同じアドレスなのでdelete不可
↓
エラー
ということ。
代入時のコピー時に新たに領域確保する「代入演算子」
代入演算子のオーバーロードについては以前記述した。(クラスの応用_演算子のオーバーロード - Awesome Hacks!)
しかし、今回は少し書き方が違うので注意。
#include <iostream> #include <string> using namespace std; class TEST { private: char *c; public: TEST(char *str = "zzz"){ // 既にデフォルトコンストラクタなので問題なし(この確認は必要) c = new char[strlen(str)+1]; strcpy(c, str); } // コピーコンストラクタ TEST(const TEST& t){ cout << "コピーコンストラクタ実行" << endl; c = new char[strlen(t.c)+1]; // ここでコピー元のメンバ変数と同じ大きさの動的メモリ領域確保 strcpy(c, t.c); } // 代入演算子のオーバーロード TEST& operator=(const TEST& rt){ if(this != &rt){ // コピー先(代入式左辺)自身による代入防止 delete c; // コピー先(代入式左辺)の確保済みメモリを解放 c = new char[strlen(rt.c)+1]; // ここでコピー元のメンバ変数と同じ大きさの動的メモリ領域確保 strcpy(c, rt.c); } return *this; } ~TEST(){ cout << "デストラクタ実行" << endl; delete c; } char* getC(){ return c; } }; int main(){ TEST t1("abc"); TEST t2("def"); cout << "t1のcの値 : " << t1.getC() << endl; cout << "t1のcのアドレス : " << (void*)t1.getC() << endl; cout << "t1のアドレス : " << &t1 << endl; cout << "t2のcの値 : " << t2.getC() << endl; cout << "t2のcのアドレス : " << (void*)t2.getC() << endl; cout << "t2のアドレス : " << &t2 << endl; cout << endl; t2 = t1; TEST t3 = t1; cout << "t2,t1を代入しました" << endl; cout << "t1のcの値 : " << t1.getC() << endl; cout << "t1のcのアドレス : " << (void*)t1.getC() << endl; cout << "t1のアドレス : " << &t1 << endl; cout << "t2のcの値 : " << t2.getC() << endl; cout << "t2のcのアドレス : " << (void*)t2.getC() << endl; cout << "t2のアドレス : " << &t2 << endl; cout << "t3のcの値 : " << t3.getC() << endl; cout << "t3のcのアドレス : " << (void*)t3.getC() << endl; cout << "t3のアドレス : " << &t3 << endl; cout << endl; return 0; }
【実行結果】
$ t1のcの値 : abc t1のcのアドレス : 0x7fe412c04ae0 t1のアドレス : 0x7fff5b774980 t2のcの値 : def t2のcのアドレス : 0x7fe412c04af0 t2のアドレス : 0x7fff5b774978 コピーコンストラクタ実行 t2,t1を代入しました t1のcの値 : abc t1のcのアドレス : 0x7fe412c04ae0 t1のアドレス : 0x7fff5b774980 t2のcの値 : abc t2のcのアドレス : 0x7fe412c04af0 t2のアドレス : 0x7fff5b774978 t3のcの値 : abc t3のcのアドレス : 0x7fe412c04b00 t3のアドレス : 0x7fff5b774960 デストラクタ実行 デストラクタ実行 デストラクタ実行 $