C++におけるオブジェクトのコピー機構は、シャローコピーとディープコピーに分かれます。この違いは、特に動的に割り当てられるメモリを扱うクラスにおいて重要です。
C++では、多くのデータ構造が動的メモリ(new/delete)とともに機能します。デフォルトでは、コンパイラはバイト単位でコピーを行うコピーコンストラクタと代入演算子を生成します(シャローコピー)。これは迅速ですが、オブジェクトが外部リソースを管理している場合には危険です。
シャローコピーは、動的に割り当てられたリソースのアドレスのみをコピーします。オブジェクトの1つが削除されると、そのメモリが解放され、別のインスタンスは「ダングリング」(使用後の無効な)ポインタを持つことになります。その結果、ダブル削除、メモリリーク、クラッシュが発生します。
ディープコピーは、すべての動的リソースのコピーを明示的に作成することを伴います。これを行うためには、クラス内でコピーコンストラクタと代入演算子を自分で実装し、各要素のコピーを確保する必要があります。
配列を持つクラスのコード例は以下の通りです:
class DynArray { int* data; size_t size; public: DynArray(size_t n) : size(n), data(new int[n]) {} ~DynArray() { delete[] data; } // ディープコピーコンストラクタ DynArray(const DynArray& other) : size(other.size), data(new int[other.size]) { for (size_t i = 0; i < size; ++i) data[i] = other.data[i]; } // ディープコピー代入演算子 DynArray& operator=(const DynArray& other) { if (this != &other) { delete[] data; size = other.size; data = new int[size]; for (size_t i = 0; i < size; ++i) data[i] = other.data[i]; } return *this; } };
主な特徴:
コンパイラは常にコピーコンストラクタと代入演算子を正しく生成しますか?
回答:
いいえ。動的リソースを持つクラスの場合、デフォルトのコピーは正しくありません:両方のオブジェクトが同じリソースを所有することになります。外部リソースを所有している場合、ディープコピーは明示的に実装する必要があります。
コピーコンストラクタ/代入演算子のみを実装した場合、デストラクタを実装する必要がありますか?
回答:
はい。そうしないとメモリリークが発生します:ユーザー定義のコピーコンストラクタ内でメモリを解放しても、デストラクタを実装しないと、オブジェクトの破壊時にメモリが解放されなくなります。
std::vectorはポインタを保持できますか?なぜそのコピーでメモリリークが発生する可能性があるのですか?
回答:
はい、std::vectorはポインタを問題なく保持できます。このようなstd::vectorをコピーすると、ポインタ自体がコピーされ、指しているオブジェクトはコピーされません。これはシャローコピーです:コンテンツ全体をディープコピーする必要がある場合は、各オブジェクトを手動でコピーし、メモリに独立して配置する必要があります。
例:
std::vector<int*> v1; v1.push_back(new int(42)); std::vector<int*> v2 = v1; // ポインタがコピーされ、*intはコピーされない
プログラマがコピーコンストラクタ/代入演算子をオーバーライドせずに配列のラッパークラスを実装します。その結果、両方のオブジェクトが同じメモリを所有し、一方のdestroyがもう一方にアクセスした際にクラッシュします。
利点:
欠点:
開発者がディープコピーを実装します:配列の内容がコピーされ、自分のデストラクタと自己代入からの保護を持つ代入演算子があります。
利点:
欠点: