C++では、オブジェクトをコピーする際にメンバー毎のコピー機構が使用されます。オブジェクトの各メンバーに対して、独自のコピー操作が呼び出されます。組み込み型にとっては安全ですが、動的リソースに対しては問題が発生します。ポインターのみがコピーされ、データ自体はコピーされないからです。
もしオブジェクトがヒープメモリに割り当てられたポインターを含む場合、2つのオブジェクトをコピーした後、それらは同じメモリ領域を指すことになります。そのため、一方のオブジェクトが破棄されるとメモリが解放され、もう一方のポインターは無効(「野生」ポインター)になります。これにより、実行時エラーやメモリリークが発生する可能性があります。
コピーが独立するためには、深いコピー—バイト単位のコピーと専用バッファの割り当てが必要です。これはカスタムコピーコンストラクターと代入演算子を作成することで実現されます。
コード例:
class MyString { char* data; public: MyString(const char* s) { data = new char[strlen(s)+1]; strcpy(data, s); } // 深いコピーコンストラクター MyString(const MyString& src) { data = new char[strlen(src.data) + 1]; strcpy(data, src.data); } // 深いコピー代入演算子 MyString& operator=(const MyString& src) { if (this != &src) { delete[] data; data = new char[strlen(src.data) + 1]; strcpy(data, src.data); } return *this; } ~MyString() { delete[] data; } };
主な特徴:
クラスにポインターのみが含まれ、メモリが割り当てられていない場合、なぜカスタムデストラクターが必要ですか?
デストラクターは、クラス内で明示的にメモリ(または他のリソース)を割り当てた場合にのみ必要です。ポインターがメモリを割り当てない場合、デフォルトのデストラクターで十分です。
動的メモリを持つクラスでoperator=を実装しない場合、コピーコンストラクターが宣言されていても何が起こりますか?
コピーコンストラクターを手動で定義した場合、コンパイラは自動的にoperator=を実装しません。暗黙的に宣言されるか、コンパイラがエラー/警告を出すことになります(標準によります)。これは、代入の際に悪化した動作を引き起こします:メンバー毎のコピーが行われ、ダブルフリーまたはメモリリークが発生します。
コード例:
MyString a("hi"); MyString b = a; // OK: あなたのコピーコンストラクター MyString c("bye"); c = a; // 問題! operator=が手動で実装されていない場合、浅いコピーになります。
手動でoperator=を実装する際に自己代入を行うことの危険性は何ですか?
リソースを共有する場合、this!=&rhsをチェックせずに自己代入を行うと、delete[] dataが実行された後、すでに解放された配列をコピーしようとしてセグメンテーションフォルトを引き起こします。自己保護: 常に自己代入を確認してください。
if (this != &rhs) { ... }
開発者は、深いコピーを実装せずに組み込みポインタを持つクラスのオブジェクトをコピーします。コピー後、複数のオブジェクトが同じメモリ領域を共有します。2つのデストラクタが呼び出され、ダブル削除が発生し、プログラムがクラッシュします。
利点:
欠点:
開発者は、コピーコンストラクタ、代入演算子、デストラクタを適切に実装します。各オブジェクトは自分のメモリを所有します。
利点:
欠点: