复制ctor基本上使用以前创建的对象创建对象。 但是如果我们只是创建一个对象然后使用' ='逐个元素分配。如果动态创建对象,这甚至可以工作。那么复制构造函数可以做什么,赋值运算符不能呢?
答案 0 :(得分:0)
使用复制构造函数而不是默认构造空对象并将新对象分配给某个已存在的对象有很多很好的理由。
首先,让我们扮演一个魔鬼拥护者并考虑无关紧要的情况:
struct X {
int i;
};
X x;
x.i = 10;
X y;
y = x; // Option 1
X y(x) // Option 2
在上面的示例中,除了更好的可读性和代码推理之外,选项1和选项2之间确实没有区别。但是,如果我们想让我们的y
对象成为const
对象,那该怎么办?因为我们不希望它改变它的生命周期?比选项1无效:
const X y;
y = x; // Compilation error!
所以,即使在最简单的例子中,我们也可以说,即使对于非常简单的类型,区分赋值和复制构造对于 const-correctness 也非常重要。 (可能还有其他相同的例子)。
接下来是性能影响。让我们考虑一个非常破碎和错误的字符串类实现(仅限插图,除此之外在生活中没有位置):
struct VeryBrokenString {
char* buffer;
VeryBrokenString() buffer(new char[1]) { strcpy(buffer, ""); }
~VeryBrokenString() { delete[] buffer; }
VeryBrokerString& operator= (const VeryBrokenString& rhs) {
delete[] buffer;
buffer = new [strlen(rhs.buffer) + 1];
strcpy(buffer, rhs.buffer);
return *this;
}
VeryBrokenString(const VeryBrokenString& rhs) {
buffer = new [strlen(rhs.buffer) + 1];
strcpy(buffer, rhs.buffer);
}
};
VeryBrokenString one; // init it somehow :)
VeryBrokenString two;
two = one; // Option 1
VeryBrokenString two(one); //Option 2
在上面的示例中,选项1导致重复工作 - 缓冲区首先被分配,而不是解除分配,只是再次分配。在选项2中,缓冲区仅分配一次。所以,第二点:复制构造函数有助于生成更高效的代码。
现在,让我们考虑上面破碎的字符串。虽然在许多层面上都是错误的,但是一个特别糟糕的代码是它的赋值运算符。它删除原始缓冲区,然后分配一个新缓冲区,从rhs
复制数据。两个主要问题是,如果rhs
与this
相同,则删除后将无需复制任何内容。第二个问题是,如果出于任何原因,新分配会抛出异常(例如,内存不足),我们就会松开原始缓冲区。虽然可以通过检查自我分配(if (&rhs == this) return *this;
)解决第一个问题,但第二个问题不能以直接的方式轻松解决。人们可以阅读有关如何在不同的文章中编写赋值运算符的方法,但是同意的方法将是(对于这个类)以下几行:
// Rest of BrokenString remains the same
NotSoBrokenString& operator= (const NotSoBrokenString& rhs) {
NotSoBrokenString temp(rhs);
char* const buf = buffer;
buffer = temp.buffer;
temp.buffer = buf;
}
这要好得多,因为所有分配都在temp
构造函数内。如果失败 - 抛出异常 - 原始缓冲区仍然有效且可用。 (它还解决了自我分配的问题 - 当this
与rhs
相同时,不会造成任何伤害,尽管效率很低。
所以,第三点 - 赋值运算符通常是根据拷贝构造函数完成的!
现在这个答案变得非常大,所以我不会有这样的例子,但是类的语义通常允许它被复制构造,但是没有分配。
复制构造函数在赋值运算符上使用的好处包括const-correctness,效率,代码正确性以及对非平凡语义的支持。