- オブジェクトのコピーとはどういう意味ですか?
- 何でコピーコンストラクタとコピー代入演算子は?
- いつ自分で申告する必要がありますか?
- オブジェクトがコピーされないようにするにはどうすればよいですか?
回答:
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.pdf 12.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;
場合によっては、リソースを管理するクラスを実装する必要があります。(単一のクラスで複数のリソースを管理しないでください。これは痛みにつながるだけです。)その場合、3つのルールを覚えておいてくださいください。
デストラクタ、コピーコンストラクタ、またはコピー代入演算子のいずれかを自分で明示的に宣言する必要がある場合は、おそらく3つすべてを明示的に宣言する必要があります。
(残念ながら、この「規則」は、C ++標準や、私が知っているコンパイラーによって強制されていません。)
C ++ 11以降、オブジェクトには2つの特別なメンバー関数(移動コンストラクターと移動割り当て)があります。これらの機能を実装するための5つの州のルール。
署名の例:
class person
{
std::string name;
int age;
public:
person(const std::string& name, int age); // Ctor
person(const person &) = default; // Copy Ctor
person(person &&) noexcept = default; // Move Ctor
person& operator=(const person &) = default; // Copy Assignment
person& operator=(person &&) noexcept = default; // Move Assignment
~person() noexcept = default; // Dtor
};
3/5のルールは0/3/5のルールとも呼ばれます。ルールのゼロ部分は、クラスの作成時に特別なメンバー関数を一切記述できないことを示しています。
ほとんどの場合、リソースを自分で管理する必要はありませんstd::string
。これは、などの既存のクラスがすでに行っているためです。std::string
メンバーを使用した単純なコードと、aを使用した複雑でエラーが発生しやすい代替コードを比較するだけで、char*
納得できるはずです。生のポインターメンバーから離れている限り、3つのルールが自分のコードに関係することはほとんどありません。
クラスで次のいずれかが必要な場合
- コピーコンストラクタ、
- 代入演算子、
- またはデストラクタ、
明示的に定義すると、3つすべてが必要になる可能性があります。
これは、通常、3つすべてがリソースの管理に使用され、クラスがリソースを管理する場合、通常はコピーと解放を管理する必要があるためです。
クラスが管理するリソースをコピーするための適切なセマンティクスがない場合は、コピーコンストラクターと代入演算子をとして宣言する(定義しない)ことで、コピーを禁止することを検討してくださいprivate
。
(C ++標準の新しいバージョン(C ++ 11)がC ++に移動セマンティクスを追加することに注意してください。これにより、3つのルールが変更される可能性があります。しかし、C ++ 11セクションを書くには、これについてあまり詳しくありません。三則について。)
boost::noncopyable
)から(プライベートに)継承することです。また、より明確にすることができます。C ++ 0xと関数を「削除」する可能性はここで役立つと思いますが、構文を忘れていました:/
noncopyable
、それがstd libの一部でない限り、私はそれをあまり改善とは考えていません。(ああ、削除構文を忘れた場合は、私が知っていたよりも忘れました。:)
)
ビッグスリーの法則は上記のとおりです。
それが解決する種類の問題の簡単な例、平易な英語:
デフォルト以外のデストラクタ
あなたはコンストラクタにメモリを割り当てたので、それを削除するためにデストラクタを書く必要があります。そうしないと、メモリリークが発生します。
これは仕事であると思うかもしれません。
問題は、オブジェクトのコピーが作成された場合、コピーは元のオブジェクトと同じメモリをポイントすることです。
これらの1つがデストラクタ内のメモリを削除すると、もう1つが無効なメモリへのポインタ(これはダングリングポインタと呼ばれます)を使用しようとすると、毛むくじゃらになります。
したがって、コピーコンストラクタを作成して、新しいオブジェクトに独自のメモリを割り当てて破棄します。
代入演算子とコピーコンストラクター
クラスのメンバーポインターにコンストラクターでメモリを割り当てました。このクラスのオブジェクトをコピーすると、デフォルトの代入演算子とコピーコンストラクターによって、このメンバーポインターの値が新しいオブジェクトにコピーされます。
つまり、新しいオブジェクトと古いオブジェクトは同じメモリを指しているため、1つのオブジェクトで変更すると、他のオブジェクトでも変更されます。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にプリミティブ型付きメンバーがいくつかしかない場合、デフォルトの代入演算子は機能しますが、MyClassにいくつかのポインターメンバーと、代入演算子を持たないオブジェクトがある場合、結果は予測できません。したがって、クラスのデストラクタで削除するものがある場合は、ディープコピー演算子が必要になる可能性があります。つまり、コピーコンストラクタと代入演算子を提供する必要があります。
オブジェクトのコピーとはどういう意味ですか?オブジェクトをコピーする方法はいくつかあります。詳細に言及している可能性が最も高い2種類について、ディープコピーとシャローコピーについて話しましょう。
オブジェクト指向言語を使用している(または少なくともそう想定している)ため、メモリの一部が割り当てられているとしましょう。OO言語なので、割り当てたメモリのチャンクは簡単に参照できます。これらは通常、プリミティブ変数(int、char、byte)または独自の型とプリミティブで構成されるクラスとして定義されているためです。したがって、次のようなCarクラスがあるとします。
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つのルールでは、次のいずれかを宣言した場合、
次に、3つすべてを宣言する必要があります。コピー操作の意味を引き継ぐ必要性は、ほとんどの場合、ある種のリソース管理を実行するクラスから生じたという観察から生まれました。
1つのコピー操作で実行されていたリソース管理は、おそらく他のコピー操作でも実行する必要があり、
クラスのデストラクタもリソースの管理に参加します(通常はリソースを解放します)。管理される古典的なリソースはメモリでした。これが、メモリを管理するすべての標準ライブラリクラス(たとえば、動的メモリ管理を実行するSTLコンテナ)がすべて「大きな3つ」、つまりコピー操作とデストラクタの両方を宣言する理由です。
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 ++の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;
c++-faq
ください。