经过大量搜索,至少这个问题帮助我理解了使用复制构造函数和赋值运算符的区别
我的问题是关于这一行
instance has to be destroyed and re-initialized if it has internal dynamic memory
如果我初始化一个实例
Object copyObj = null;
,然后赋值,copyObj = realObj
那么这个开销(销毁和重新初始化)仍然存在?
如果不是,那么现在在这种情况下,我为什么要使用 Copy Constructor 而不是直接分配对象
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 构造函数允许您保留两个引用;一个到“旧”对象,一个到“新”对象。这些对象是独立的(或者应该取决于您允许副本的深度)
如果您进行重新分配,您只有对“新”对象的引用。“旧”对象将不再可访问(假设没有其他对它的引用)并且将有资格进行垃圾收集。
这取决于你想要达到的目标。如果您想要对象的精确副本,并且希望该对象拥有自己的独立生命,请使用复制构造函数。如果您只想要一个新对象而不关心旧对象,请重新分配变量。
PS-我不得不承认,我没有阅读您链接到的问题..
首先是关于 C++ 和 Java 中的复制构造和复制分配的一些基础知识
由于 C++ 中的对象语义和 Java 中的引用语义,C++ 和 Java 是两个截然不同的野兽。我的意思是:
SomeClass obj = expr;
在 C++ 中,这一行表示一个用 初始化的新对象expr
。在 Java 中,这一行创建的不是新对象,而是对对象的新引用,并且该引用指的是表达式给出的任何内容。Java 引用可以为空,表示“没有对象”。C++ 对象是,所以没有“无对象”-对象 ;-) Java 引用非常像 C++ 指针。唯一会使区分变得困难的是,虽然 C++ 有指针和对象,并且用 取消引用指针->
,但在 Java 中,一切都是引用(除了 int 和一些其他基本类型),通过引用访问对象使用.
,很容易与在 C++ 中访问“直接”对象相混淆。“一切都是引用”意味着任何对象(除了 int & Co.)在概念上都是在堆上创建的。
话虽如此,让我们看看两种语言的作业和副本。
复制构造在两种语言中的含义相同,本质上是创建一个新对象,该对象是另一个对象的副本。复制构造函数定义类似:
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=
wich(通常)将对象的值分配给已经存在的对象,丢弃您分配的对象的旧值。如果您自己不定义它,编译器会尽力为您生成一个,对对象的元素进行简单的元素复制。在 Java 中,您不能重载运算符,因此您必须定义一个名为 eg 的方法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
值,而 while已更改。
在 Java(第 2 行)中,赋值是引用的赋值,这意味着之后两个引用都引用了同一个对象,而之前引用的对象 ba不再被引用,因此它将被垃圾回收。打电话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 对象分配它自己的动态整数数组,指向它的指针存储在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
}
现在到问题中的 qoute:考虑两个对象 x1 和 x2 以及 assignment 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 以来,它已成为“五法则”,因为您还必须考虑移动分配和移动构造。