我知道C ++编译器为类创建了一个复制构造函数。在这种情况下,我们必须编写用户定义的复制构造函数吗?你能举一些例子吗?
答案 0 :(得分:68)
编译器生成的复制构造函数执行成员复制。有时这还不够。例如:
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;
}
答案 1 :(得分:43)
我有点恼火的是Rule of Five
的规则没有被引用。
这条规则非常简单:
五法则:
无论何时编写析构函数,复制构造函数,复制赋值运算符,移动构造函数或移动赋值运算符,您可能需要编写其他四个。
但是你应该遵循一个更一般的指导原则,这是因为需要编写异常安全的代码:
每个资源都应由专用对象管理
这里@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;
};
我不了解你,但我发现我更容易;)
答案 2 :(得分:28)
我可以回想一下我的实践,并在必须处理明确声明/定义复制构造函数时考虑以下情况。我已将案例分为两类
我在本节中说明了使用该类型正确操作程序所需的声明/定义复制构造函数的情况。
阅读完本节后,您将了解允许编译器自行生成复制构造函数的几个缺陷。因此,正如seand中提到的answer一样,关闭新类的可复制性始终是安全的,并且故意在以后真正需要时启用它。
声明一个私有的复制构造函数,并且不为它提供一个实现(这样即使该类的对象被复制到类自己的范围内或由其朋友复制,构建在链接阶段也会失败)。
在末尾声明带有=delete
的复制构造函数。
这是最容易理解的案例,实际上是其他答案中提到的唯一案例。 shaprtooth covered非常好。我只想补充说,深度复制应该由对象专有的资源可以应用于任何类型的资源,其中动态分配的内存只是一种。如果需要,可能还需要深度复制对象
考虑一个类,其中所有对象 - 无论它们是如何构造的 - 必须以某种方式注册。一些例子:
最简单的示例:维护当前现有对象的总数。对象注册就是增加静态计数器。
更复杂的例子是拥有单例注册表,其中存储了对该类型的所有现有对象的引用(以便可以将通知传递给所有这些对象)。
引用计数智能指针只能被视为此类别中的一个特例:新指针&#34;寄存器&#34;本身使用共享资源而不是全局注册表。
这样的自注册操作必须由类型的任何构造函数执行,复制构造函数也不例外。
某些对象可能具有非平凡的内部结构,并且在它们的不同子对象之间具有直接的交叉引用(事实上,只有一个这样的内部交叉引用足以触发这种情况)。编译器提供的复制构造函数将破坏内部对象内关联,将它们转换为对象间关联。
一个例子:
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?
在某些状态下可能存在可以安全复制对象的类(例如,默认构造状态),否则可以安全 安全。如果我们想允许复制安全复制对象,那么 - 如果编程是防御性的 - 我们需要在用户定义的复制构造函数中进行运行时检查。
有时,应该可复制的类会聚合不可复制的子对象。 通常,对于具有不可观察状态的对象会发生这种情况(这种情况将在下面的&#34;优化&#34;部分中详细讨论)。编译器只是帮助识别这种情况。
应该是可复制的类可以聚合准可复制类型的子对象。准可复制类型在严格意义上不提供复制构造函数,但具有另一个允许创建对象的概念副本的构造函数。制作类型可复制的原因是当没有关于该类型的复制语义的完全一致时。
例如,重新审视对象自我注册案例,我们可以争辩说 可能存在必须在全局中注册对象的情况 对象管理器,只有它是一个完整的独立对象。如果是的话 另一个对象的子对象,那么管理它的责任就在于 它包含的对象。
或者,必须支持浅色和深色复制(它们都不是默认值)。
然后最终决定留给该类型的用户 - 在复制对象时,他们必须明确指定(通过附加参数)预期的复制方法。
如果采用非防御性编程方法,也可能存在常规复制构造函数和准复制构造函数。当在绝大多数情况下应该应用单一复制方法时,这是合理的,而在罕见但很好理解的情况下,应该使用替代复制方法。然后编译器不会抱怨它无法隐式定义复制构造函数;这将是用户&#39;我们有责任记住并检查是否应该通过准复制构造函数复制该类型的子对象。
在极少数情况下,对象的可观察状态的子集可能构成(或被视为)对象身份的不可分割部分,不应转移到其他对象(虽然这可能有点争议)。
示例:
对象的UID(但是这个也属于&#34;自我注册&#34;来自上面的情况,因为必须在自行注册的行为中获取id。)
当新对象不能继承源对象的历史记录而是以单个历史记录项开始时,对象的历史记录(例如撤销/重做堆栈)&#34; 在&lt; TIME&gt;复制了来自&lt; OTHER_OBJECT_ID&gt; &#34;。
在这种情况下,复制构造函数必须跳过复制相应的子对象。
编译器提供的复制构造函数的签名取决于可用于子对象的复制构造函数。如果至少有一个子对象没有真实拷贝构造函数(通过常量引用获取源对象),而是有一个变异拷贝构造函数(取通过非常量引用来源对象)然后编译器别无选择,只能隐式声明然后定义一个变异的拷贝构造函数。
现在,如果&#34;变异&#34;子对象类型的拷贝构造函数实际上并没有改变源对象(并且只是由一个不了解const
关键字的程序员编写的)?如果我们不能通过添加缺少的const
来修复该代码,那么另一个选择是声明我们自己的用户定义的复制构造函数具有正确的签名并承诺转向{{1 }}
已经放弃对其内部数据的直接引用的COW容器必须在构造时进行深度复制,否则它可能表现为引用计数句柄。
虽然COW是一种优化技术,但这种逻辑在复制构造函数中 对于正确实施至关重要。这就是我把这个案子放在这里的原因 而不是在优化&#34;部分,我们接下来。
在以下情况下,您可能需要/需要根据优化问题定义自己的复制构造函数:
考虑一个支持元素删除操作的容器,但可以通过简单地将删除的元素标记为已删除,并稍后再循环其插槽来实现。当制作这样一个容器的副本时,压缩幸存数据可能是有意义的,而不是保留&#34;删除的&#34;插槽按原样。
对象可能包含不属于其可观察状态的数据。通常,这是在对象的生命周期内累积的缓存/记忆数据,以加速对象执行的某些慢速查询操作。跳过复制该数据是安全的,因为在执行相关操作时(以及如果!)将重新计算该数据。复制此数据可能是不合理的,因为如果通过改变操作来修改对象的可观察状态(从中导出缓存数据),它可能会很快失效(如果我们不打算修改对象,为什么是我们创建一个深层副本呢?)
只有当辅助数据与表示可观察状态的数据相比较大时,才能证明此优化是正确的。
C ++允许通过声明复制构造函数const_cast
来禁用隐式复制。然后,该类的对象不能传递给函数和/或通过值从函数返回。这个技巧可以用于看似轻量级但复制起来确实非常昂贵的类型(但是,使其成为可复制的可能是更好的选择)。
在C ++ 03中声明一个复制构造函数也需要定义它(当然,如果 你打算用它)。因此,仅仅是出于这样的复制构造函数 正在讨论的问题意味着你必须编写相同的代码 编译器会自动为你生成。
C ++ 11和更新标准允许声明特殊成员函数( 默认和复制构造函数,复制赋值运算符和 析构函数)与an explicit request to use the default implementation (只需使用
explicit
)结束声明。
这个答案可以改进如下:
- 添加更多示例代码
- 用内部交叉引用来说明&#34;对象&#34;情况下
- 添加一些链接
答案 3 :(得分:6)
如果您有一个动态分配内容的类。例如,您将书籍的标题存储为char *并使用new设置标题,副本将不起作用。
您必须编写一个执行title = new char[length+1]
然后strcpy(title, titleIn)
的复制构造函数。复制构造函数只会执行“浅”复制。
答案 4 :(得分:2)
当对象按值传递,按值返回或显式复制时,将调用复制构造函数。如果没有复制构造函数,c ++会创建一个默认的复制构造函数,它会生成一个浅复制。如果对象没有动态分配内存的指针,那么浅拷贝就可以了。
答案 5 :(得分:0)
禁用copy ctor和operator =通常是一个好主意,除非该类特别需要它。这可以防止低效率,例如在预期引用时通过值传递arg。编译器生成的方法也可能无效。
答案 6 :(得分:-1)
让我们考虑以下代码段:
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();
给出了垃圾输出,因为创建了一个用户定义的复制构造函数,没有编写任何代码来显式复制数据。因此,编译器不会创建相同的文件。
尽管您大多数人都已经知道,但只是想与所有人共享此知识。
干杯... 编码愉快!