我知道C++编译器会为类创建一个复制构造函数。在哪些情况下,我们需要编写自定义的复制构造函数?你能给出一些例子吗?
我知道C++编译器会为类创建一个复制构造函数。在哪些情况下,我们需要编写自定义的复制构造函数?你能给出一些例子吗?
编译器生成的拷贝构造函数进行成员逐一复制。有时这是不够的。例如:
class Class {
public:
Class( const char* str );
~Class();
private:
char* stored;
};
Class::Class( const char* str )
{
stored = new char[srtlen( str ) + 1 ];
strcpy( stored, str );
}
Class::~Class()
{
delete[] stored;
}
在这种情况下,对于 stored
成员的逐个复制不会复制缓冲区(只会复制指针),因此首先被销毁的副本将共享缓冲区并成功调用 delete[]
,而第二个副本将遇到未定义行为。您需要进行深层复制的复制构造函数(以及赋值运算符)。Class::Class( const Class& another )
{
stored = new char[strlen(another.stored) + 1];
strcpy( stored, another.stored );
}
void Class::operator = ( const Class& another )
{
char* temp = new char[strlen(another.stored) + 1];
strcpy( temp, another.stored);
delete[] stored;
stored = temp;
}
delete stored[];
,我认为应该改成 delete [] stored;
。 - Peter Ajtaistd::string
。一般的想法是只有管理资源的实用类需要重载三个特殊成员函数(拷贝构造函数、赋值运算符和析构函数),而所有其他类都应该使用这些实用类,从而无需定义任何特殊成员函数。 - GManNickG我有点生气,因为没有引用“五法则”。
这个法则非常简单:
五法则:
当你编写析构函数、复制构造函数、赋值运算符、移动构造函数或移动赋值运算符中任何一种时,你可能需要编写其他四个。
但有一个更通用的指南,你应该遵循,它源于编写安全异常代码的需要:
每个资源都应由专门的对象管理
这里 @sharptooth 的代码仍然(大部分)没问题,但是如果他向类中添加第二个属性,就会出现问题。考虑以下类:
class Erroneous
{
public:
Erroneous();
// ... others
private:
Foo* mFoo;
Bar* mBar;
};
Erroneous::Erroneous(): mFoo(new Foo()), mBar(new Bar()) {}
new Bar
抛出异常会发生什么?如何删除指向 mFoo
的对象?虽然有一些解决方案(例如函数级别的try/catch...),但它们并不具备可扩展性。
处理这种情况的正确方法是使用适当的类而不是裸指针。
class Righteous
{
public:
private:
std::unique_ptr<Foo> mFoo;
std::unique_ptr<Bar> mBar;
};
使用相同的构造函数实现(或者实际上使用make_unique
),现在我可以免费获得异常安全性!这不是很令人兴奋吗?最重要的是,我不再需要担心正确的析构函数了!虽然我确实需要编写自己的Copy Constructor
和Assignment Operator
,因为unique_ptr
没有定义这些操作...但这在这里并不重要 ;)
因此,sharptooth
的类得到了重新审查:
class Class
{
public:
Class(char const* str): mData(str) {}
private:
std::string mData;
};
我不知道你怎么想,但我觉得我的更容易 ;)
我可以回忆起我的实践经验并想到以下情况,当需要明确声明/定义复制构造函数时,必须处理这些情况。我将这些情况分成两类:
我在这个部分中列出了一些情况,在这些情况下,声明/定义复制构造函数对于使用该类型的程序的正确操作是必要的。
阅读完这个部分后,您将了解到允许编译器自动生成复制构造函数的几个陷阱。因此,正如seand在他的答案中指出的那样,对于一个新类关闭可复制性,并在真正需要时故意启用它总是安全的。
声明一个私有复制构造函数并不为其提供实现(这样,即使在类的自身范围内或由其朋友复制该类型的对象,构建也会在链接阶段失败)。
在copy-constructor后面声明=delete
。
这是最易理解的情况,实际上也是其他答案中唯一提到的情况。 shaprtooth已经很好地解释了。我只想补充说,深度复制应该专门拥有对象的资源(包括动态分配的内存)类型的任何资源。如果需要,深度复制一个对象还可能需要:
考虑一个类,其中所有对象 - 无论它们是如何构造的 - 必须以某种方式进行注册。一些示例:
最简单的例子:维护当前存在对象的总计数。对象注册只涉及增加静态计数器。
更复杂的例子是具有单例注册表,其中存储了该类型所有现有对象的引用(以便可以向它们所有人发送通知)。
参考计数的智能指针可以被认为是该类别中的一个特殊情况:新指针会向共享资源注册自己,而不是在全局注册表中。
这种自注册操作必须由类型的任何构造函数执行,而复制构造函数也不例外。
某些对象可能具有非平凡的内部结构,并且其不同子对象之间存在直接交叉引用(实际上,只需要一个这样的内部交叉引用即可触发此情况)。编译器提供的复制构造函数将打破内部对象内的关联,并将它们转换为对象间关联。
例如:
struct MarriedMan;
struct MarriedWoman;
struct MarriedMan {
// ...
MarriedWoman* wife; // association
};
struct MarriedWoman {
// ...
MarriedMan* husband; // association
};
struct MarriedCouple {
MarriedWoman wife; // aggregation
MarriedMan husband; // aggregation
MarriedCouple() {
wife.husband = &husband;
husband.wife = &wife;
}
};
MarriedCouple couple1; // couple1.wife and couple1.husband are spouses
MarriedCouple couple2(couple1);
// Are couple2.wife and couple2.husband indeed spouses?
// Why does couple2.wife say that she is married to couple1.husband?
// Why does couple2.husband say that he is married to couple1.wife?
只有符合特定标准的对象可以被复制。当新对象不得继承源对象的历史记录,而是要以单个历史记录项"从<OTHER_OBJECT_ID>于<TIME>复制"开始时,对象的历史记录(例如撤销/重做堆栈)的历史记录。
在这种情况下,复制构造函数必须跳过复制相应的子对象。
编译器提供的复制构造函数的签名取决于子对象可用的复制构造函数。如果至少有一个子对象没有真正的复制构造函数(通过常量引用接收源对象),而是具有变异复制构造函数(通过非常量引用接收源对象),那么编译器将别无选择,只能隐式声明并然后定义一个变异复制构造函数。
现在,如果子对象类型的"变异"复制构造函数实际上没有改变源对象(仅由不知道const
关键字的程序员编写)呢?如果我们不能通过添加缺少的const
来修复该代码,则另一个选项是声明我们自己的用户定义的复制构造函数,其中包含正确的签名,并犯罪使用const_cast
。
一种已经放弃其内部数据的COW容器必须在构造时进行深度复制,否则它可能会像引用计数句柄一样行为。
尽管COW是一种优化技术,但复制构造函数中的此逻辑对于其正确实现至关重要。这就是为什么我将此案例放在这里而不是下一个"Optimization"部分的原因。
在以下情况下,您可能希望/需要出于优化考虑定义自己的复制构造函数:
考虑支持元素删除操作的容器,但可能通过简单地将已删除的元素标记为已删除并稍后重新使用其插槽来执行此操作。当复制这样的容器时,将幸存数据压缩而不是保留"已删除"插槽可能是有意义的。
对象可以包含不属于其可观察状态的数据。通常,这是在对象的生命周期中积累的缓存/记忆数据,以加速对象执行的某些慢查询操作。跳过复制该数据是安全的,因为当(并且如果!)执行相关操作时,它将被重新计算。复制这些数据可能是不合理的,因为如果对象的可观察状态(从中派生缓存数据)由变异操作修改(如果我们不打算修改对象,那么为什么要创建深度复制?)则可能会很快失效。
只有在辅助数据相对于表示可观察状态的数据很大的情况下,才可以证明这种优化是合理的。
C++ 允许通过声明复制构造函数为 explicit
来禁用隐式复制。然后该类的对象将无法按值传递到函数中或从函数中返回。这个技巧可以用于看起来轻量级但实际上非常昂贵的类型的情况(虽然,使其准可复制可能是更好的选择)。
在 C++03 中,声明复制构造函数需要同时定义它(当然,如果您想要使用它的话)。因此,仅出于正在讨论的原因而去使用这样的复制构造函数意味着您必须编写编译器会自动为您生成的相同代码。
C++11 和更新的标准允许声明特殊成员函数(默认和复制构造函数、复制赋值运算符和析构函数)并使用 默认实现的显式请求(只需将声明结尾处添加
=default
)。
可以通过以下方式改进此答案:
- 添加更多示例代码
- 说明“具有内部交叉引用的对象”情况
- 添加一些链接
title = new char[length+1]
,然后是strcpy(title, titleIn)
。复制构造函数只会进行“浅层”复制。当对象被按值传递、按值返回或显式复制时,将调用复制构造函数。如果没有复制构造函数,C++会创建一个默认的复制构造函数,它会进行浅拷贝。如果对象没有指向动态分配内存的指针,则浅拷贝就足够了。
让我们考虑下面的代码片段:
class base{
int a, *p;
public:
base(){
p = new int;
}
void SetData(int, int);
void ShowData();
base(const base& old_ref){
//No coding present.
}
};
void base :: ShowData(){
cout<<this->a<<" "<<*(this->p)<<endl;
}
void base :: SetData(int a, int b){
this->a = a;
*(this->p) = b;
}
int main(void)
{
base b1;
b1.SetData(2, 3);
b1.ShowData();
base b2 = b1; //!! Copy constructor called.
b2.ShowData();
return 0;
}
Output:
2 3 //b1.ShowData();
1996774332 1205913761 //b2.ShowData();
b2.ShowData();
输出垃圾数据,因为创建了一个用户定义的复制构造函数,但没有编写显式复制数据的代码。因此编译器不会创建相同的函数。
只是想与大家分享这个知识,虽然你们中的大多数人已经知道了。
干杯... 愉快的编码!!!