ProgrammingC++ 中級開発者

C++におけるシャローコピーとディープコピーの違いを、動的メモリを使用したコンテナの例を挙げて説明してください。手動でディープコピーを実装するにはどうすればよいですか?

Hintsage AIアシスタントで面接を突破

回答。

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がもう一方にアクセスした際にクラッシュします。

利点:

  • 動作が速い(コピーなし)。

欠点:

  • 非常に追跡が難しいランタイムエラーの発生;ダブルフリー/セグメンテーションフォルトの発生。

ポジティブケース

開発者がディープコピーを実装します:配列の内容がコピーされ、自分のデストラクタと自己代入からの保護を持つ代入演算子があります。

利点:

  • 安全なコピーとメモリの解放。
  • コードの保守性と拡張性。

欠点:

  • コードとメモリの消費が少し増える。
  • 複数の動的リソースを持つクラスでは複雑になる。