たくさん検索した後、少なくともこの質問は、コピーコンストラクターと代入演算子の使用の違いを理解するのに役立ちました私の
質問はこの行についてです? そうでない場合、このシナリオでは、オブジェクトを直接割り当てるのではなく、コピー コンストラクターを使用する必要があるのはなぜですか
instance has to be destroyed and re-initialized if it has internal dynamic memory
Object copyObj = null;
copyObj = realObj
3 に答える
単純にオーバーライドしてコピー コンストラクターを使用するという概念は=
、Java には存在しません。演算子をオーバーライドすることはできません。Java でのコピー コンストラクターの概念は、次のように機能します。
public class MyType {
private String myField;
public MyType(MyType source) {
this.myField = source.myField;
}
}
コピー コンストラクターは、同じ型のパラメーターを受け取り、そのすべての値をコピーするコンストラクターです。同じ状態の新しいオブジェクトを取得するために使用されます。
MyType original = new MyType();
MyType copy = new MyType(original);
// After here orginal == copy will be false and original.equals(copy) should be true
MyType referenceCopy = original
// After here orginal == referenceCopy will be true and original.equals(referenceCopy) will also be true
演算子は同じことを行います=
: オブジェクトを変数に割り当てます。オーバーヘッドは発生しません。実行時に異なる可能性があるのは、コンストラクターの呼び出しです。
Copy コンストラクターを使用すると、2 つの参照を保持できます。1 つは「古い」オブジェクトに、もう 1 つは「新しい」オブジェクトに。これらのオブジェクトは独立しています (または、コピーを許可する深さに依存する必要があります)。
再割り当てを行うと、「新しい」オブジェクトへの参照しかありません。「古い」オブジェクトにはアクセスできなくなり (他の参照がないと仮定して)、ガベージ コレクションの対象になります。
それはあなたが何を達成しようとしているのかにかかっています。オブジェクトの正確なコピーが必要で、このオブジェクトに独自の独立した寿命を持たせたい場合は、コピー コンストラクターを使用します。新しいオブジェクトだけが必要で、古いオブジェクトは気にしない場合は、変数を再割り当てします。
PS - 認めざるを得ません。リンク先の質問を読んでいませんでした..
最初に、C++ と Java でのコピーの作成とコピーの代入に関するいくつかの基本事項
C++ と Java は、C++ のオブジェクト セマンティクスと Java の参照セマンティクスにより、2 つの非常に異なる獣です。これが意味することは次のとおりです。
SomeClass obj = expr;
C++ では、この行は で初期化される新しいオブジェクトexpr
を示します。Java では、この行は新しいオブジェクトではなくオブジェクトへの新しい参照を作成し、その参照は式が与えるものを参照します。Java 参照は null にすることができます。これは、「オブジェクトがない」ことを意味します。C++ オブジェクトは. 区別を難しくする可能性がある唯一のことは、C++ にはポインターとオブジェクトがあり、ポインターを で参照解除する->
のに対し、Java ではすべてが参照であり (int と他のいくつかの基本型を除く)、参照を使用してオブジェクトにアクセスすることです。.
これは、C++ の「直接」オブジェクトへのアクセスと簡単に混同される可能性があります。「すべてが参照である」とは、任意のオブジェクト (int & Co. を除く) が概念的にヒープ上に作成されることを意味します。
そうは言っても、両方の言語で課題とコピーを見てみましょう。
コピー構築は、両方の言語で同じことを意味します。基本的に、別のオブジェクトのコピーである新しいオブジェクトを作成します。コピー コンストラクターの定義も同様です。
SomeClass(SomeClass obj) { /* ... */ } //Java
SomeClass(SomeClass const& obj) { /* ... */ } //C++
違いは、C++ ではパラメータを参照として明示的に宣言する必要があることだけですが、Java ではすべてがとにかく参照です。C++ で最初の行を記述すると、引数を copyで受け取るコンストラクターが定義されます。つまり、コンパイラーは、コピーを作成する必要があるコピー コンストラクターを使用して、既にコピーを作成する必要があります... - 良い考えではありません。 .
2 つの言語でコピー構築を使用すると、次のようになります。
SomeClass newObj = new SomeClass(oldObj); //Java
SomeClass newObj = oldObj; //C++ object
SomeClass* ptrNewObj = new SomeClass(oldObj); //C++ pointer
1 行目と 3 行目を見ると、本質的に同じように見えます。これは、Java 参照は基本的に C++ のポインターに似ているため、それらが本質的に同じであるためです。両方の式は、それが作成された関数スコープより長く存続できる新しいオブジェクトを作成します。2 行目は、Java には存在しないプレーンな C++ オブジェクトをスタック上に作成します。C++ では、コピーもコンパイラによって暗黙的に作成されます。参照ではなく値でパラメータを受け取る関数にオブジェクトが渡された場合。
コピー代入の定義: C++ では、operator=
オブジェクトの値を (通常は) 既存のオブジェクトに代入し、代入先のオブジェクトの古い値を破棄するように定義できます。自分で定義しない場合は、オブジェクトの要素の単純な要素ごとのコピーを実行して、コンパイラが生成するのが最善です。Java では、演算子をオーバーロードできないため、次のようなメソッドを定義する必要がありますassign
。
void assign(SomeObject other) {/* ... */} //Java
SomeObject& operator=(SomeObject const& other) {/* ... */} //C++
ここでも、C++ ではパラメーターを参照として明示的に宣言していますが、Java では宣言していないことに注意してください。
コピー代入の使用:
objA = objB; //C++ copy assignment
objA = objB; //Java ref assignment
ptrObjA = ptrObjB; //C++ pointer assignment
objA.assign(objB); //Java
objB.change();
ここで、最初の 2 行はまったく同じように見えますが、これ以上の違いはありません。C++ ではオブジェクト自体objA
をobjB
deonte しますが、Java ではオブジェクトは単なる参照であることを思い出してください。したがって、C++ では、これはオブジェクトに対するコピー代入です。つまり、同じ内容を持つ 2 つのオブジェクトで終了します。変更後、割り当て前の値が変更されobjB
ます。
Java (2 行目) では、その代入は参照の代入です。つまり、その後の 2 つの参照と参照はまったく同じオブジェクトを意味しますが、以前に参照されたオブジェクトはもはや参照されないため、ガベージ コレクトされます。通話中objA
objB
objB
objA
objB
objA
objB.change()
両方の参照が指す単一のオブジェクトを変更し、参照を介してそれにアクセスすると、objA
これらの変更が明らかになります。
ここでも、C++ ポインターの場合と (ほぼ) 同じです。オブジェクトとポインターの割り当ての構文を区別できないことがわかります。すべて、割り当てられる型によって決まります。C++ との違いは、ガベージ コレクターがなく、ptrObjA
指しているオブジェクトを削除できなくなるため、メモリ リークが発生することです。
あなたの質問について:
C++ クラスを考えてみましょう:
class X {
int* pi;
unsigned count;
public:
X(X const&);
X& operator= (X const&);
~X();
};
各 X オブジェクトが独自の int の動的配列を割り当てると仮定すると、そのポインタは に格納されpi
ます。C++ にはガベージ コレクションがないため、X オブジェクトは割り当てられたメモリを管理する必要があります。つまり、手動で破棄する必要があります。
X::~X() { delete[] pi; }
コピー コンストラクターは、元の動的配列をコピーするため、同じ配列を使用しているときに 2 つが競合することはありません。これはディープ コピーと呼ばれ、Java と C++ で等しく使用されます。
X::X(X const& other) : pi(NULL), count(0) {
pi = new int[other.count]; //allocates own memory
count = other.count;
std::copy(other.pi, other.pi+count, pi); //copies the contents of the array
}
あなたの質問の引用に移りましょう: 2 つのオブジェクト x1 と x2 と割り当て を考えてみましょうx1 = x2
。すべてをコンパイラに任せると、次のような代入演算子が生成されます。
X& X::operator=(X const& other) {
pi = other.pi;
count = other.count;
}
最初の行x1.pi
で のポインター値を取得しますx2.pi
。コピー代入に関するセクションで説明したように、これにより両方のポインターが同じ配列を指すようになり、以前に所有されていた配列x1
がスペースで失われます。つまり、両方のオブジェクトが共有配列で動作するときにリークと奇妙な動作が発生します。 .
正しい実装は次のようになります。
X& X::operator=(X const& other) {
delete[] pi; //1
pi = new int[other.count]; //allocates own memory
count = other.count;
std::copy(other.pi, other.pi+count, pi); //copies the contents of the array
}
ここに引用の内容が表示されます。まず、オブジェクトが「クリーンアップ」されます。つまり、メモリが解放され、基本的にデストラクタが行うこと (「インスタンスを破棄する必要がある」) が行われます。次に、ディープ コピーが実行され、コピー コンストラクターと同じ処理が行われます (「...そして再初期化」)。
これは「Rule of Three」と呼ばれます: 独自のコピー コンストラクターを作成する必要がある場合 (生成されたコンストラクターが意図したとおりに動作しないため)、ほとんどの場合、独自のデストラクターと代入演算子も作成する必要があります。C++11 以降、ムーブ代入とムーブ構築も考慮する必要があるため、「5 つのルール」になりました。