- オブジェクトのコピーとはどういう意味ですか?
- コピーコンストラクタとコピー代入演算子とは何ですか?
- いつ自分で宣言する必要がありますか?
- オブジェクトがコピーされないようにするにはどうすればよいですか?
8 に答える
序章
C ++は、ユーザー定義型の変数を値セマンティクスで処理します。これは、オブジェクトがさまざまなコンテキストで暗黙的にコピーされることを意味し、「オブジェクトのコピー」が実際に何を意味するかを理解する必要があります。
簡単な例を考えてみましょう。
class person
{
std::string name;
int age;
public:
person(const std::string& name, int age) : name(name), age(age)
{
}
};
int main()
{
person a("Bjarne Stroustrup", 60);
person b(a); // What happens here?
b = a; // And here?
}
(その部分に戸惑う場合name(name), age(age)
、これはメンバー初期化子リストと呼ばれます。)
特別会員機能
person
オブジェクトをコピーするとはどういう意味ですか?このmain
関数は、2つの異なるコピーシナリオを示しています。初期化は、コピーコンストラクタperson b(a);
によって実行されます。その仕事は、既存のオブジェクトの状態に基づいて新しいオブジェクトを構築することです。代入は、コピー代入演算子によって実行されます。ターゲットオブジェクトはすでに処理が必要な有効な状態にあるため、その仕事は一般的にもう少し複雑です。b = a
コピーコンストラクタも代入演算子(またはデストラクタ)も自分で宣言していないので、これらは暗黙的に定義されています。標準からの引用:
[...]コピーコンストラクタとコピー代入演算子、[...]とデストラクタは特別なメンバー関数です。[注:プログラムが明示的に宣言していない場合、実装は一部のクラスタイプに対してこれらのメンバー関数を暗黙的に宣言します。 それらが使用される場合、実装はそれらを暗黙的に定義します。[...]エンドノート][n3126.pdfセクション12§1]
デフォルトでは、オブジェクトをコピーするということは、そのメンバーをコピーすることを意味します。
非ユニオンクラスXの暗黙的に定義されたコピーコンストラクターは、そのサブオブジェクトのメンバーごとのコピーを実行します。[n3126.pdfセクション12.8§16]
非ユニオンクラスXに対して暗黙的に定義されたコピー代入演算子は、そのサブオブジェクトのメンバーごとのコピー代入を実行します。[n3126.pdfセクション12.8§30]
暗黙の定義
暗黙的に定義された特殊メンバー関数は次のperson
ようになります。
// 1. copy constructor
person(const person& that) : name(that.name), age(that.age)
{
}
// 2. copy assignment operator
person& operator=(const person& that)
{
name = that.name;
age = that.age;
return *this;
}
// 3. destructor
~person()
{
}
メンバーごとのコピーは、まさにこの場合に必要なものです。
name
そしてage
コピーされるので、自己完結型の独立したperson
オブジェクトを取得します。暗黙的に定義されたデストラクタは常に空です。この場合も、コンストラクターでリソースを取得しなかったため、これで問題ありません。person
メンバーのデストラクタは、デストラクタが終了した後に暗黙的に呼び出されます。
デストラクタの本体を実行し、本体内に割り当てられた自動オブジェクトを破棄した後、クラスXのデストラクタは、Xの直接[...]メンバーのデストラクタを呼び出します[n3126.pdf12.4§6]
リソースの管理
では、これらの特別なメンバー関数を明示的に宣言する必要があるのはいつですか?クラスがリソースを管理するとき、つまり、クラスのオブジェクトがそのリソースを担当するとき。これは通常、リソースがコンストラクターで取得され(またはコンストラクターに渡され)、デストラクタで解放されることを意味します。
時間をさかのぼって、以前の標準のC++に戻りましょう。のようなものはありませんでしたstd::string
、そしてプログラマーはポインターに恋をしていました。person
クラスは次のようになっている可能性があります。
class person
{
char* name;
int age;
public:
// the constructor acquires a resource:
// in this case, dynamic memory obtained via new[]
person(const char* the_name, int the_age)
{
name = new char[strlen(the_name) + 1];
strcpy(name, the_name);
age = the_age;
}
// the destructor must release this resource via delete[]
~person()
{
delete[] name;
}
};
今日でも、人々はこのスタイルでクラスを作成し、問題を抱えています。「人をベクトルにプッシュすると、クレイジーなメモリエラーが発生します!」デフォルトでは、オブジェクトのコピーはそのメンバーのコピーを意味しますが、name
メンバーのコピーは単にポインタが指す文字配列ではなく、ポインタをコピーします!これにはいくつかの不快な影響があります。
- を介した変更は、を介して
a
観察できますb
。 - 一度
b
破壊さa.name
れると、ダングリングポインタです。 - が破棄された場合
a
、ダングリングポインタを削除すると、未定義の動作が発生します。 - 割り当てでは、割り当ての前に何を指していたかが考慮されていないため
name
、遅かれ早かれ、あちこちでメモリリークが発生します。
明示的な定義
メンバーごとのコピーには望ましい効果がないため、文字配列のディープコピーを作成するには、コピーコンストラクターとコピー代入演算子を明示的に定義する必要があります。
// 1. copy constructor
person(const person& that)
{
name = new char[strlen(that.name) + 1];
strcpy(name, that.name);
age = that.age;
}
// 2. copy assignment operator
person& operator=(const person& that)
{
if (this != &that)
{
delete[] name;
// This is a dangerous point in the flow of execution!
// We have temporarily invalidated the class invariants,
// and the next statement might throw an exception,
// leaving the object in an invalid state :(
name = new char[strlen(that.name) + 1];
strcpy(name, that.name);
age = that.age;
}
return *this;
}
初期化と割り当ての違いに注意してくださいname
。メモリリークを防ぐために、割り当てる前に古い状態を破棄する必要があります。また、フォームの自己割り当てから保護する必要がありx = x
ます。このチェックを行わないと、ソースdelete[] name
文字列を含む配列が削除されます。これは、を書き込むと、両方に同じポインタが含まれるためです。x = x
this->name
that.name
例外安全性
残念ながら、new char[...]
メモリの枯渇が原因で例外をスローすると、このソリューションは失敗します。考えられる解決策の1つは、ローカル変数を導入してステートメントを並べ替えることです。
// 2. copy assignment operator
person& operator=(const person& that)
{
char* local_name = new char[strlen(that.name) + 1];
// If the above statement throws,
// the object is still in the same state as before.
// None of the following statements will throw an exception :)
strcpy(local_name, that.name);
delete[] name;
name = local_name;
age = that.age;
return *this;
}
これにより、明示的なチェックなしで自己割り当ても処理されます。この問題に対するさらに強力な解決策は、コピーアンドスワップのイディオムですが、ここでは例外安全性の詳細については説明しません。私は次の点を指摘するために例外についてのみ言及しました:リソースを管理するクラスを書くことは難しいです。
コピー不可能なリソース
ファイルハンドルやミューテックスなど、一部のリソースはコピーできないか、コピーすべきではありません。private
その場合、定義を与えずに、コピーコンストラクタとコピー代入演算子を宣言するだけです。
private:
person(const person& that);
person& operator=(const person& that);
または、それらを継承するboost::noncopyable
か、削除済みとして宣言することもできます(C ++ 11以降)。
person(const person& that) = delete;
person& operator=(const person& that) = delete;
三つのルール
リソースを管理するクラスを実装する必要がある場合があります。(1つのクラスで複数のリソースを管理しないでください。これは苦痛につながるだけです。)その場合、3つのルールを覚えておいてください。
デストラクタ、コピーコンストラクタ、またはコピー代入演算子のいずれかを自分で明示的に宣言する必要がある場合は、おそらく3つすべてを明示的に宣言する必要があります。
(残念ながら、この「ルール」は、C ++標準または私が知っているコンパイラーによって強制されません。)
5のルール
C ++ 11以降、オブジェクトには2つの特別なメンバー関数があります。ムーブコンストラクターとムーブ代入です。これらの機能も実装するための5つの州のルール。
署名の例:
class person
{
std::string name;
int age;
public:
person(const std::string& name, int age); // Ctor
person(const person &) = default; // 1/5: Copy Ctor
person(person &&) noexcept = default; // 4/5: Move Ctor
person& operator=(const person &) = default; // 2/5: Copy Assignment
person& operator=(person &&) noexcept = default; // 5/5: Move Assignment
~person() noexcept = default; // 3/5: Dtor
};
ゼロのルール
3/5のルールは0/3/5のルールとも呼ばれます。ルールのゼロ部分は、クラスを作成するときに特別なメンバー関数を記述できないことを示しています。
助言
ほとんどの場合、リソースを自分で管理する必要はありません。これは、などの既存のクラスがstd::string
すでにリソースを管理しているためです。std::string
メンバーを使用する単純なコードを、を使用する複雑でエラーが発生しやすい代替コードと比較するだけchar*
で、納得できるはずです。生のポインターメンバーから離れている限り、3つのルールが自分のコードに関係する可能性はほとんどありません。
三つのルールはC++の経験則であり、基本的には次のように述べています。
あなたのクラスが
- コピーコンストラクタ、
- 代入演算子、
- またはデストラクタ、
明示的に定義すると、3つすべてが必要になる可能性があります。
この理由は、通常、3つすべてがリソースの管理に使用され、クラスがリソースを管理する場合は、通常、コピーと解放を管理する必要があるためです。
クラスが管理するリソースをコピーするための適切なセマンティクスがない場合は、コピーコンストラクターと代入演算子をとして宣言(定義private
しない)して、コピーを禁止することを検討してください。
(C ++標準の次の新しいバージョン(C ++ 11)は、C ++に移動セマンティクスを追加することに注意してください。これにより、三つのルールが変更される可能性があります。ただし、これについてはほとんど知らないため、C++11セクションを作成できません。三つのルールについて。)
ビッグスリーの法則は上記のとおりです。
平易な英語で、それが解決する種類の問題の簡単な例:
デフォルト以外のデストラクタ
コンストラクタにメモリを割り当てたので、それを削除するにはデストラクタを作成する必要があります。そうしないと、メモリリークが発生します。
あなたはこれが仕事であると思うかもしれません。
問題は、オブジェクトのコピーが作成された場合、そのコピーが元のオブジェクトと同じメモリを指すことです。
これらの1つがデストラクタのメモリを削除すると、もう1つは無効なメモリへのポインタ(これはダングリングポインタと呼ばれます)を使用しようとすると、問題が発生します。
したがって、コピーコンストラクターを作成して、新しいオブジェクトに独自のメモリを割り当てて破棄します。
代入演算子とコピーコンストラクタ
コンストラクターのメモリーをクラスのメンバーポインターに割り当てました。このクラスのオブジェクトをコピーすると、デフォルトの代入演算子とコピーコンストラクターがこのメンバーポインターの値を新しいオブジェクトにコピーします。
これは、新しいオブジェクトと古いオブジェクトが同じメモリを指しているため、一方のオブジェクトで変更すると、もう一方のオブジェクトでも変更されることを意味します。一方のオブジェクトがこのメモリを削除すると、もう一方のオブジェクトはそれを使用しようとし続けます--eek。
これを解決するには、独自のバージョンのコピーコンストラクターと代入演算子を作成します。お使いのバージョンでは、新しいオブジェクトに個別のメモリを割り当て、アドレスではなく最初のポインタが指している値をコピーします。
基本的に、デストラクタ(デフォルトのデストラクタではない)がある場合は、定義したクラスにメモリが割り当てられていることを意味します。クラスが外部でクライアントコードまたはユーザーによって使用されているとします。
MyClass x(a, b);
MyClass y(c, d);
x = y; // This is a shallow copy if assignment operator is not provided
MyClassにプリミティブ型のメンバーがいくつかある場合、デフォルトの代入演算子は機能しますが、代入演算子を持たないポインターメンバーとオブジェクトがある場合、結果は予測できません。したがって、クラスのデストラクタに削除するものがある場合は、ディープコピー演算子が必要になる可能性があります。つまり、コピーコンストラクタと代入演算子を提供する必要があります。
オブジェクトのコピーとはどういう意味ですか?オブジェクトをコピーする方法はいくつかあります。最も参照しやすい2種類について、ディープコピーとシャローコピーについて説明します。
私たちはオブジェクト指向言語を使用しているので(または少なくともそう想定している)、メモリの一部が割り当てられているとしましょう。これはオブジェクト指向言語であるため、割り当てたメモリのチャンクを簡単に参照できます。これらは通常、プリミティブ変数(int、char、bytes)または独自の型とプリミティブで作成された定義済みのクラスであるためです。したがって、次のような車のクラスがあるとしましょう。
class Car //A very simple class just to demonstrate what these definitions mean.
//It's pseudocode C++/Javaish, I assume strings do not need to be allocated.
{
private String sPrintColor;
private String sModel;
private String sMake;
public changePaint(String newColor)
{
this.sPrintColor = newColor;
}
public Car(String model, String make, String color) //Constructor
{
this.sPrintColor = color;
this.sModel = model;
this.sMake = make;
}
public ~Car() //Destructor
{
//Because we did not create any custom types, we aren't adding more code.
//Anytime your object goes out of scope / program collects garbage / etc. this guy gets called + all other related destructors.
//Since we did not use anything but strings, we have nothing additional to handle.
//The assumption is being made that the 3 strings will be handled by string's destructor and that it is being called automatically--if this were not the case you would need to do it here.
}
public Car(const Car &other) // Copy Constructor
{
this.sPrintColor = other.sPrintColor;
this.sModel = other.sModel;
this.sMake = other.sMake;
}
public Car &operator =(const Car &other) // Assignment Operator
{
if(this != &other)
{
this.sPrintColor = other.sPrintColor;
this.sModel = other.sModel;
this.sMake = other.sMake;
}
return *this;
}
}
ディープコピーとは、オブジェクトを宣言してから、オブジェクトの完全に別個のコピーを作成する場合です...2つの完全なメモリセットに2つのオブジェクトが含まれることになります。
Car car1 = new Car("mustang", "ford", "red");
Car car2 = car1; //Call the copy constructor
car2.changePaint("green");
//car2 is now green but car1 is still red.
それでは、奇妙なことをしましょう。car2が間違ってプログラムされているか、car1が構成されている実際のメモリを共有することを意図的に意図しているとしましょう。(これを行うのは通常間違いであり、クラスでは通常、それが説明されている包括的なものです。)car2について尋ねるときはいつでも、car1のメモリ空間へのポインタを実際に解決しているふりをしてください...それは多かれ少なかれ浅いコピーですは。
//Shallow copy example
//Assume we're in C++ because it's standard behavior is to shallow copy objects if you do not have a constructor written for an operation.
//Now let's assume I do not have any code for the assignment or copy operations like I do above...with those now gone, C++ will use the default.
Car car1 = new Car("ford", "mustang", "red");
Car car2 = car1;
car2.changePaint("green");//car1 is also now green
delete car2;/*I get rid of my car which is also really your car...I told C++ to resolve
the address of where car2 exists and delete the memory...which is also
the memory associated with your car.*/
car1.changePaint("red");/*program will likely crash because this area is
no longer allocated to the program.*/
したがって、どの言語で書いているかに関係なく、オブジェクトのコピーに関しては、ほとんどの場合、深いコピーが必要になるため、意味に十分注意してください。
コピーコンストラクターとコピー代入演算子とは何ですか?私はすでに上記でそれらを使用しました。Car car2 = car1;
基本的に、変数を宣言して1行で割り当てる場合など、コードを入力するとコピーコンストラクターが呼び出されます。これは、コピーコンストラクターが呼び出されるときです。代入演算子は、等号を使用したときに発生するものです-- car2 = car1;
。通知car2
は同じステートメントで宣言されていません。これらの操作のために作成するコードの2つのチャンクは、非常によく似ています。実際、典型的なデザインパターンには、最初のコピー/割り当てが正当であると納得したら、すべてを設定するために呼び出す別の関数があります。私が書いた長いコードを見ると、関数はほぼ同じです。
いつ自分で宣言する必要がありますか?共有または本番用のコードを何らかの方法で記述していない場合は、実際には、必要なときにのみ宣言する必要があります。「誤って」使用することを選択し、それを作成しなかった場合、つまりコンパイラのデフォルトを取得した場合は、プログラム言語が何をするかを知っておく必要があります。たとえば、コピーコンストラクターを使用することはめったにありませんが、代入演算子のオーバーライドは非常に一般的です。足し算や引き算などの意味も上書きできることをご存知ですか?
オブジェクトがコピーされないようにするにはどうすればよいですか?プライベート関数を使用してオブジェクトにメモリを割り当てることが許可されているすべての方法をオーバーライドすることは、妥当な出発点です。本当に他の人にコピーさせたくない場合は、それを公開して、例外をスローし、オブジェクトをコピーしないことでプログラマーに警告することができます。
いつ自分で宣言する必要がありますか?
三つのルールは、あなたが
- コピーコンストラクタ
- コピー代入演算子
- デストラクタ
次に、3つすべてを宣言する必要があります。コピー操作の意味を引き継ぐ必要性は、ほとんどの場合、ある種のリソース管理を実行するクラスに起因するという観察から生まれました。これは、ほとんどの場合、
一方のコピー操作で実行されていたリソース管理は、おそらくもう一方のコピー操作で実行する必要があり、
クラスデストラクタは、リソースの管理にも参加します(通常はリソースを解放します)。管理される従来のリソースはメモリでした。これが、メモリを管理するすべての標準ライブラリクラス(動的メモリ管理を実行するSTLコンテナなど)がすべて「ビッグ3」(コピー操作とデストラクタの両方)を宣言する理由です。
三つのルールの結果は、ユーザーが宣言したデストラクタの存在は、単純なメンバーごとのコピーがクラスのコピー操作に適切である可能性が低いことを示しています。つまり、クラスがデストラクタを宣言した場合、コピー操作は正しいことを行わないため、おそらく自動的に生成されるべきではないことを示唆しています。C ++ 98が採用された時点では、この一連の推論の重要性は十分に理解されていなかったため、C ++ 98では、ユーザーが宣言したデストラクタの存在は、コンパイラのコピー操作の生成意欲に影響を与えませんでした。これはC++11でも引き続き当てはまりますが、コピー操作が生成される条件を制限すると、レガシーコードが破損しすぎるためです。
オブジェクトがコピーされないようにするにはどうすればよいですか?
コピーコンストラクターとコピー代入演算子をプライベートアクセス指定子として宣言します。
class MemoryBlock
{
public:
//code here
private:
MemoryBlock(const MemoryBlock& other)
{
cout<<"copy constructor"<<endl;
}
// Copy assignment operator.
MemoryBlock& operator=(const MemoryBlock& other)
{
return *this;
}
};
int main()
{
MemoryBlock a;
MemoryBlock b(a);
}
C ++ 11以降では、コピーコンストラクタと代入演算子の削除を宣言することもできます
class MemoryBlock
{
public:
MemoryBlock(const MemoryBlock& other) = delete
// Copy assignment operator.
MemoryBlock& operator=(const MemoryBlock& other) =delete
};
int main()
{
MemoryBlock a;
MemoryBlock b(a);
}
既存の回答の多くは、すでにコピーコンストラクタ、代入演算子、およびデストラクタに触れています。ただし、ポストC ++ 11では、移動セマンティクスの導入により、これが3を超えて拡張される可能性があります。
最近、Michael Claisseがこのトピックに触れる講演を行いました:http: //channel9.msdn.com/events/CPP/C-PP-Con-2014/The-Canonical-Class
C ++の3つのルールは、次のメンバー関数の1つに明確な定義がある場合、プログラマーは他の2つのメンバー関数を一緒に定義する必要があるという3つの要件の設計と開発の基本原則です。つまり、デストラクタ、コピーコンストラクタ、コピー代入演算子の3つのメンバー関数が不可欠です。
C++のコピーコンストラクターは特別なコンストラクターです。これは、既存のオブジェクトのコピーに相当する新しいオブジェクトである新しいオブジェクトを構築するために使用されます。
コピー代入演算子は、同じタイプのオブジェクトの他のオブジェクトに既存のオブジェクトを指定するために通常使用される特別な代入演算子です。
簡単な例があります:
// default constructor
My_Class a;
// copy constructor
My_Class b(a);
// copy constructor
My_Class c = a;
// copy assignment operator
b = a;