CPP04 1.0
|
オブジェクト指向プログラミングでは、オブジェクトに動的に割り当てられたメモリへのポインタ Brain*
( Dog および Cat 内のポインタなど) が含まれている場合、コピーを処理する主な方法が 2 つあります。
* 1 つのオブジェクトのポインタを通じてデータを変更すると、他のオブジェクトにも影響します。
- 一方のオブジェクトが破棄されると、共有メモリが解放されます。その後、もう一方のオブジェクトが破棄されると、既に解放されているメモリを解放しようとします。その結果、二重解放エラーが発生し、クラッシュや未定義の動作が発生します。これは、メモリリーク(またはメモリ破損)の一般的なシナリオです。
* 1 つのオブジェクトの動的に割り当てられたメモリ内のデータを変更しても、他のオブジェクトには影響しません。
- 各オブジェクトは、破棄されるときに独自のメモリを安全に解放できるため、二重解放エラーを防止できます。
EX01の場合:
private Brain* brain;
がいます。* デストラクタでは、このBrainオブジェクトの割り当てを解除します。
this->brain = new Brain();
DogおよびCat がデフォルトの (コンパイラ生成の) コピー コンストラクタと代入演算子を使用した場合、またはそれらを浅いコピーで実装した場合:
delete this->brain;
// Hypothetical shallow copy constructorthis->brain = other.brain; // THIS IS A SHALLOW COPY!}Dog()Default constructor for Dog. Calls the Animal base class constructor and sets the type to "Dog"....Definition Dog.cpp:28
- 警告
- これは二重解放問題につながります。
originalDog
とcopiedDog
の両方がdelete this->brain;
同じメモリを指して解放しようとすると、失敗します。
Dog および Cat のディープ コピーを実現するには、コピー コンストラクターとコピー代入演算子で Brain* メンバーを明示的に処理する必要があります。
* 基本クラス (
Animal
) コピー コンストラクターを呼び出します。
方法1:
if (this != &other) {Animal::operator=(other); // Assign base part// Deep copy: assign the CONTENTS of 'other.brain' to 'this->brain'*(this->brain) = *(other.brain); // This calls Brain's copy assignment operator}return *this;}Animal & operator=(const Animal &other)Copy assignment operator for Animal. Assigns the type from the other Animal object....Definition Animal.cpp:47Dog & operator=(const Dog &other)Copy assignment operator for Dog. (Copy And Swap Idiom) Displays a specific copy assignment message.Definition Dog.cpp:53
- 自己割り当てチェック(
if (this != &other)
)が含まれます。- 基本クラス (
Animal
) コピー代入演算子を呼び出します。- 次に、
Brain
クラスのコピー代入演算子を使用して、other.brain
の内容をBrain
がすでに所有しているオブジェクト*(this->brain)
にコピーします。- この特定のEX01実装では、
this->brain
は既に存在しているものと想定されています(デフォルトコンストラクタで割り当てられています)。
- 警告
- ダングリングポインタになる危険性がある。
もし*(this->brain) = *(other.brain); // Deep copy of Brainthis->brain == NULL
の場合、ここで未定義動作になります。 さらに、もしthis->brain
が有効なポインタであったとして、この行の前にdelete this->brain;
を入れた場合:ダングリングポインタ( dangling pointer )とは? -> WikiPedia 参照
コンピューター プログラミングにおけるダングリング ポインターとワイルド ポインターは、適切な型の有効なオブジェクトを指さないポインターです。
方法2:Copy and Swap Idiom (コピー・アンド・スワップ・イエィオム) ※推奨
- 安全なコピー (
Dog temp(other);
):2. 例外安全な交換 (まず、コピーコンストラクタを使って、
other
オブジェクトの完全なディープコピーである一時オブジェクトtemp
を作成します。もしこのコピーコンストラクタ(この中での
new Brain()
など)でメモリ確保が失敗して std::bad_alloc がスローされても、この時点では*this
(代入の左辺のオブジェクト) は一切変更されていません。これは元の有効な状態を完全に保っており、強い例外保証を提供します。this->swap(temp);
):3. 自動的なクリーンアップ:一時オブジェクト
temp
が完全に、かつ例外なく構築されたら、*this
のリソース(this->brain
)とtemp
のリソース(temp.brain
)を交換(スワップ)します。ポインタの交換は、std::swap を使えば通常は例外をスローしません。
この操作は非常に高速です。
swap
が終わると、temp
は以前*this
が持っていたリソース(古い Brain オブジェクト)の所有権を持つことになります。
operator=
関数が終了し、temp
オブジェクトがスコープを抜ける際に、temp
のデストラクタが自動的に呼び出され、以前*this
が持っていた古い Brain のメモリが解放されます。
- 覚え書き
- メリット:
* 強い例外保証:
operator=
の途中でnew
が失敗しても、代入の左辺のオブジェクト (*this
) は変更されず、元の有効な状態を保ちます。
- 正しいリソース解放: 古いリソースの解放が、安全に、かつ自動的に行われます。
- 自己代入の自動処理:
dog = dog;
のような自己代入も自動的に正しく処理されます。 この「コピー・アンド・スワップ」イディオムは、リソースを所有するクラスのコピー代入演算子を実装する際の、C++98 およびそれ以降でも非常に推奨されるパターンです。
* この特定のインスタンスBrainが所有するオブジェクトの割り当てを解除する役割を担います。
Dog::~Dog() {delete this->brain; // Deallocate *this object's* unique Brain// ...}
EX01 では、main関数 (および理想的には Google Test を使用した単体テスト) にディープ コピーの特定のチェックが含まれます。
// In main.cpp or test fileDog originalDog;Dog copiedDog = originalDog; // Copy constructor// Prove they are deep copies:// 1. Check if their Brain pointers are different (meaning they point to different Brain objects)// ASSERT_NE(originalDog.getBrain(), copiedDog.getBrain()); // This is a good unit test assertion// 2. Modify the copied Brain and observe the original remains unchanged// Now, originalDog.getBrain()->getIdea(0) should STILL be "Original idea"// This is the definitive proof of a deep copy.void setIdea(int index, const std::string &idea)Sets an idea at a specific index in the brain. Performs bounds checking to ensure the index is valid ...Definition Brain.cpp:80EX01 は、ディープ コピーを保証することで重大なメモリ エラーを防ぎ、 Dog, Cat オブジェクトを安全にコピーして個別に管理できるようにします。これは、堅牢な C++ アプリケーションの基本要件です。
クラス Brain 自体も正統派標準クラス形式に準拠し、
std::string ideas[100];
配列を処理します。 std::string クラス自体は独自の内部ヒープメモリを管理するため(例えば、長い文字列の場合)、 std::string のデフォルトのコピーコンストラクタと代入演算子は、文字列データのディープコピーを実行します。したがって、 Brain のコピーコンストラクタまたは代入演算子がthis->ideas[i] = other.ideas[i];
を反復処理する場合、配列内の各オブジェクト std::string のディープコピーが暗黙的に実行されます。つまり、「深さ」は下まで広がります。