我们什么时候必须使用复制构造函数?
我知道C ++编译器为类创build一个拷贝构造函数。 在这种情况下,我们必须编写一个用户定义的拷贝构造函数吗? 你能举一些例子吗?
编译器生成的复制构造函数执行成员智能复制。 有时候这是不够的。 例如:
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; }
我有点厌恶Rule of Five
的统治没有被引用。
这个规则很简单:
五法则 :
无论何时你正在编写析构函数,复制构造函数,复制赋值运算符,移动构造函数或移动赋值运算符,都可能需要编写另外四个。
但是,您应该遵循一个更一般的指导原则,这个指导原则需要编写exception安全的代码:
每个资源应由专用对象pipe理
这里@sharptooth
的代码仍然(大部分)是好的,但是如果他要为他的类添加第二个属性, @sharptooth
。 考虑以下课程:
class Erroneous { public: Erroneous(); // ... others private: Foo* mFoo; Bar* mBar; }; Erroneous::Erroneous(): mFoo(new Foo()), mBar(new Bar()) {}
如果new Bar
投掷会发生什么? 你如何删除mFoo
指向的对象? 有解决scheme(function级别尝试/赶上…),他们只是不缩放。
处理这种情况的正确方法是使用正确的类而不是原始指针。
class Righteous { public: private: std::unique_ptr<Foo> mFoo; std::unique_ptr<Bar> mBar; };
使用相同的构造函数实现(或实际上,使用make_unique
),我现在有免费的exception安全! 这不是令人兴奋吗? 而最重要的是,我不再需要担心一个适当的析构函数! 我确实需要编写我自己的Copy Constructor
和Assignment Operator
,因为unique_ptr
没有定义这些操作…但这里没关系;)
所以,重新sharptooth
课堂:
class Class { public: Class(char const* str): mData(str) {} private: std::string mData; };
我不知道你,但我觉得我更容易;)
我可以从我的实践中回想起来,并想到以下情况下,必须处理显式声明/定义复制构造函数。 我已经把案例分为两类
- 正确性/语义 – 如果您没有提供用户定义的复制构造函数,则使用该types的程序可能无法编译,或者可能无法正常工作。
- 优化 – 为编译器生成的拷贝构造函数提供了一个很好的select,可以使程序更快。
正确/语义
我在这一节中将声明/定义复制构造函数的情况放在正确使用该types的程序的操作上。
阅读本节后,您将了解到允许编译器自行生成复制构造函数的几个缺陷。 因此,正如他在答复中所指出的那样,closures一个新课程的可复制性总是安全的,并且在真正需要的时候会故意启用它。
如何使一个类在C ++ 03中不可复制
声明一个私有的复制构造函数,不要为它提供一个实现(所以即使这个types的对象被复制到这个类的自己的作用域或者其朋友中,构build在连接阶段也会失败)。
如何使一个类在C ++ 11或更新版本中不可复制
在末尾用=delete
声明复制构造函数。
浅vs深层复制
这是最好理解的情况,而且是其他答案中提到的唯一一个。 shaprtooth已经覆盖了很好。 我只想补充一点,那就是应该由对象专有的拷贝资源可以适用于任何types的资源,其中dynamic分配的内存只是一种。 如果需要,也可能需要深度复制一个对象
- 复制磁盘上的临时文件
- 打开一个单独的networking连接
- 创build一个单独的工作线程
- 分配一个单独的OpenGL帧缓冲区
- 等等
自注册对象
考虑一个类,所有对象 – 不pipe它们是如何被构造的 – 都必须以某种方式注册。 一些例子:
-
最简单的例子:维护当前存在的对象的总数。 对象注册只是增加了静态计数器。
-
一个更复杂的例子是有一个单一的registry,其中存储对该types的所有现有对象的引用(以便通知可以被传递给所有对象)。
-
引用计数的智能指针可以被认为只是这个类别中的一个特例:新指针“注册”自己与共享资源而不是全局registry。
这种自注册操作必须由ANYtypes的构造函数来执行,并且拷贝构造函数也不例外。
具有内部交叉引用的对象
有些对象可能有不平凡的内部结构,在不同的子对象之间有直接的交叉引用(事实上,只有一个这样的内部交叉引用足以触发这种情况)。 编译器提供的拷贝构造函数将打破内部对象关联,将它们转换为对象间关联。
一个例子:
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?
只有符合特定条件的对象才能被复制
在某些状态下(例如默认构造状态),可能存在对象可以安全复制的类,否则不安全。 如果我们想要允许复制安全拷贝的对象,那么 – 如果在防御方面编程 – 我们需要在用户定义的拷贝构造函数中进行运行时检查。
不可复制的子对象
有时,应该可复制的类聚合不可复制的子对象。 通常情况下,对于具有不可观察状态的对象会发生这种情况(这种情况将在下面的“优化”一节中详细讨论)。 编译器只是帮助识别这种情况。
准可复制的子对象
一个应该可复制的类可以聚合一个准可复制types的子对象。 准可复制types不提供严格意义上的复制构造函数,但是具有允许创build对象的概念副本的另一个构造函数。 types准可复制的原因是当对于types的复制语义没有完全一致的时候。
例如,重新访问对象自注册情况,我们可以争辩说,可能会出现这样的情况,只有在对象是一个完整的独立对象的情况下,对象才必须在全局对象pipe理器中注册。 如果它是另一个对象的子对象,那么pipe理它的责任是包含它的对象。
或者,必须支持浅度和深度复制(它们都不是默认值)。
然后最后的决定留给这种types的用户 – 当复制对象时,他们必须明确地指定(通过附加的参数)预定的复制方法。
在编程的非防御方式的情况下,也可能存在常规的拷贝构造函数和准拷贝构造函数。 当绝大多数情况下应用单一的复制方法时,这是可以certificate的,而在罕见但是很好理解的情况下,应该使用替代的复制方法。 那么编译器就不会抱怨它无法隐式地定义拷贝构造函数; 记住和检查是否应该通过准复制构造器来复制该types的子对象将是用户的唯一责任。
不要复制与对象身份强烈关联的状态
在极less数情况下,对象可观察状态的一个子集可能构成(或被认为)对象身份的不可分割部分,不应该转移到其他对象(尽pipe这可能有点争议)。
例子:
-
对象的UID(但是这个也属于上面的“自注册”的情况,因为这个id必须通过自注册的方式获得)。
-
在新对象不能inheritance源对象的历史的情况下,对象的历史(例如,撤销/重做堆栈),而是从单个历史项目“ 从<OTHER_OBJECT_ID>的<时间>复制 ”开始。
在这种情况下,复制构造函数必须跳过复制相应的子对象。
强制复制构造函数的正确签名
编译器提供的拷贝构造函数的签名取决于哪些拷贝构造函数可用于子对象。 如果至less有一个子对象没有真正的拷贝构造函数 (通过常量引用获取源对象),而是有一个变异的拷贝构造函数 (通过非常量引用来获取源对象),那么编译器将没有select但要隐式声明,然后定义一个变异的复制构造函数。
现在,如果子对象types的“变异”拷贝构造函数实际上不会改变源对象(并且只是由不知道const
关键字的程序员写的),那又该怎么办呢? 如果我们不能通过添加缺less的const
来修改那个代码,那么另外一个select就是声明我们自己的用户定义的拷贝构造函数,并且提供一个正确的签名,并承担转向const_cast
的罪过。
写时复制(COW)
一个COW容器直接引用了它的内部数据,必须在构build时进行深度复制,否则它可能会作为一个引用计数句柄。
尽pipeCOW是一种优化技术,但是在拷贝构造函数中的这个逻辑对于它的正确实现是至关重要的。 这就是为什么我把这个案例放在这里,而不是在我们下一步的“优化”部分。
优化
在以下情况下,您可能需要/不需要为优化问题定义您自己的拷贝构造函数:
复制期间的结构优化
考虑一个支持元素删除操作的容器,但是可以通过简单地将删除的元素标记为已删除的元素并稍后重新使用它的插槽来实现。 当制作这样一个容器的副本时,压缩存活的数据而不是按原样保留“已删除”的插槽可能是有意义的。
跳过复制不可观察状态
对象可能包含不属于其可观察状态的数据。 通常,这是在对象的生命周期中累积的caching/记忆数据,以加速对象执行某些缓慢的查询操作。 跳过复制数据是安全的,因为当执行相关操作时(以及如果!)将重新计算它。 复制这些数据可能是不合理的,因为如果对象的可观察状态(从中导出caching的数据)被修改操作(如果我们不打算修改对象,为什么我们要创build一个深层那么复制?)
只有当辅助数据比表示可观察状态的数据大时,这种优化才是合理的。
禁用隐式复制
C ++允许通过声明复制构造函数来禁用隐式复制。 那么这个类的对象不能被传入函数和/或被函数返回值。 这个技巧可以用于一个看似轻量级的types,但是确实是非常昂贵的(尽pipe如此,使其准复制也许是一个更好的select)。
在C ++ 03中声明一个拷贝构造函数也是需要的(当然,如果你打算使用它的话)。 因此,仅仅从讨论的问题出发去寻找这样一个复制构造器就意味着你必须编写相同的代码,编译器会自动为你生成这些代码。
C ++ 11和更新的标准允许声明特殊的成员函数(默认和复制构造函数,复制赋值运算符和析构函数),并使用默认实现的明确请求 (仅以
=default
结束声明)。
待办事项
这个答案可以改进如下:
- 添加更多示例代码
- 说明“具有内部交叉引用的对象”情况
- 添加一些链接
如果你有一个dynamic分配内容的类。 例如,您将书名作为char *存储,并使用new来设置标题,则复制将不起作用。
你将不得不写一个复制构造函数title = new char[length+1]
,然后strcpy(title, titleIn)
。 复制构造函数只会做一个“浅”的副本。
复制构造函数在对象按值传递,按值返回或显式复制时调用。 如果没有拷贝构造函数,c ++会创build一个默认的拷贝构造函数来创build一个浅拷贝。 如果对象没有指向dynamic分配的内存的指针,那么浅拷贝将会执行。
禁用copy ctor和operator =常常是一个好主意,除非这个类特别需要它。 这可能会防止低效率,例如在引用意图时按值传递参数。 编译器生成的方法也可能是无效的。