在搜索了很多之后,至少这个问题帮助我理解了使用复制构造函数和赋值运算符的区别
我的问题是关于这行instance has to be destroyed and re-initialized if it has internal dynamic memory
如果我初始化一个像Object copyObj = null;
这样的实例,然后分配copyObj = realObj
然后仍然这个开销(销毁和重新初始化)仍然存在?在这种情况下,为什么我要使用复制构造函数而不是直接赋值对象
通过覆盖=
来使用复制构造函数的概念在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
=
操作符也做同样的事情:将对象赋值给变量。它不产生开销。在运行时不同的是构造函数调用。
复制构造函数允许你保留两个引用;一个指向"旧"对象,一个指向"新"对象。这些对象是独立的(或者应该取决于您允许复制的深度)
如果执行重赋值操作,则只有对"new"对象的引用。"旧"对象将不再可访问(假设没有其他对它的引用),并且将有资格进行垃圾收集。
归根结底是你想要达到的目标。如果需要对象的精确副本,并且希望该对象具有独立的生命周期,请使用复制构造函数。如果你只想要一个新对象,而不关心旧对象,请重新分配变量。
PS -我必须承认,我没有读你链接的问题…
首先是关于c++和Java中拷贝构造和拷贝赋值的一些基础知识
由于c++中的对象语义和Java中的引用语义,c++和Java是两种非常不同的东西。我的意思是:SomeClass obj = expr;
在c++中,这一行表示一个用expr
初始化的新的对象。在Java中,这一行不是创建一个新对象,而是创建一个指向对象的新引用,该引用引用表达式给出的内容。Java引用可以是空的,意思是"没有对象"。c++对象是,所以不存在"无对象"——object;-) Java引用非常像c++指针。唯一使这种区别变得困难的是,c++有指针和对象,并使用->
解引用指针,而在Java中,所有都是引用(除了int和其他一些基本类型),通过引用访问对象使用.
,这很容易与c++中对"直接"对象的访问混淆。"一切都是引用"意味着任何对象(除了int &
说到这里,让我们来看看两种语言的赋值和拷贝。
复制构造在两种语言中的含义相同,本质上是创建一个新对象,该对象是另一个对象的副本。复制构造函数定义是类似的:
SomeClass(SomeClass obj) { /* ... */ } //Java
SomeClass(SomeClass const& obj) { /* ... */ } //C++
唯一的区别是c++必须显式地将形参声明为引用,而在Java中无论如何都是引用。在c++中编写第一行将定义一个构造函数,该构造函数通过copy来获取参数,即编译器必须使用复制构造函数已经创建了一个副本,它必须为其创建一个副本,…-不是个好主意。
使用复制构造在这两种语言中看起来像这样:
SomeClass newObj = new SomeClass(oldObj); //Java
SomeClass newObj = oldObj; //C++ object
SomeClass* ptrNewObj = new SomeClass(oldObj); //C++ pointer
当你看第一行和第三行时,它们看起来基本上是一样的。这是因为它们本质上是相同的,因为Java引用本质上类似于c++中的指针。这两个表达式都会创建一个新对象,该对象的生存期可以超过创建它的函数作用域。第二行在堆栈上创建一个普通的c++对象,这在Java中不存在。在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();
这里的前两行看起来完全一样,但却截然不同。请记住,在c++中,objA
和objB
表示对象本身,而在Java中,它们只是引用。在c++中,这是对象的复制赋值,这意味着你完成了两个具有相同内容的对象。在更改objB
之后,您将拥有objA
和objB
在分配之前的值,而objB
已经更改。
在Java(第2行)中,该赋值是引用的赋值,这意味着之后两个引用objA
和objB
引用了同一个对象,而之前在objA
中引用的对象不再被引用,因此它将被垃圾收集。调用objB.change()
将更改两个引用所指向的单个对象,通过引用objA
访问它将显示这些更改。
这与c++指针(几乎)相同。你看,你无法区分对象和指针赋值的语法,它们都是由被赋值的类型决定的。与c++的不同之处在于它没有垃圾收集器,你最终会出现内存泄漏,因为ptrObjA
指向的对象不能再被删除了。
class X {
int* pi;
unsigned count;
public:
X(X const&);
X& operator= (X const&);
~X();
};
假设每个X对象分配它自己的动态int数组,指向它的指针存储在pi
中。由于c++没有垃圾收集,X对象必须自己负责分配的内存,也就是说,它们必须手动销毁内存:
X::~X() { delete[] pi; }
复制构造函数将复制原始动态数组,因此在使用同一个数组时两者不会冲突。这被称为深度复制,在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
}
现在看看你问题中的那句话:考虑两个对象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
}
在这里你可以看到引用的意思:首先,对象被"清理",即释放内存,本质上做析构函数所做的("实例必须被销毁")。然后,执行深度复制,执行复制构造函数所做的工作("…和重新初始化")。
这被称为"三规则":如果你必须编写自己的复制构造函数(因为生成的构造函数不是你想要的),那么你也必须编写自己的析构函数和赋值操作符。从c++ 11开始,它就变成了"五法则",因为你必须同时考虑move赋值和move构造。