pen*_*uru 80 c++ copy-constructor
我知道C++编译器为类创建了一个复制构造函数.在这种情况下,我们必须编写用户定义的复制构造函数吗?你能举一些例子吗?
sha*_*oth 70
编译器生成的复制构造函数执行成员复制.有时这还不够.例如:
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;
}
Run Code Online (Sandbox Code Playgroud)
在这种情况下,成员的成员复制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;
}
Run Code Online (Sandbox Code Playgroud)
Mat*_* M. 44
我有点恼火,Rule of Five没有引用的规则.
这个规则很简单:
规则五:
无论何时编写析构函数,复制构造函数,复制赋值运算符,移动构造函数或移动赋值运算符,您可能需要编写其他四个.
但是有一个更一般的指导原则,你应该遵循,这源于编写异常安全代码的需要:
每个资源都应由专用对象管理
这里@sharptooth的代码仍然(大部分)都很好,但是如果他要在他的班级中添加第二个属性则不会.考虑以下课程:
class Erroneous
{
public:
Erroneous();
// ... others
private:
Foo* mFoo;
Bar* mBar;
};
Erroneous::Erroneous(): mFoo(new Foo()), mBar(new Bar()) {}
Run Code Online (Sandbox Code Playgroud)
如果发生什么new Bar抛出?你如何删除指向的对象mFoo?有解决方案(功能级别try/catch ...),它们只是不扩展.
处理这种情况的正确方法是使用适当的类而不是原始指针.
class Righteous
{
public:
private:
std::unique_ptr<Foo> mFoo;
std::unique_ptr<Bar> mBar;
};
Run Code Online (Sandbox Code Playgroud)
使用相同的构造函数实现(或实际使用make_unique),我现在免费获得异常安全!这不是很令人兴奋吗?最重要的是,我不再需要担心正确的析构函数!我确实需要编写自己的Copy Constructor,Assignment Operator但是因为unique_ptr没有定义这些操作......但这并不重要;)
因此,sharptooth课程重新审视:
class Class
{
public:
Class(char const* str): mData(str) {}
private:
std::string mData;
};
Run Code Online (Sandbox Code Playgroud)
我不了解你,但我发现我更容易;)
Leo*_*eon 28
我可以回想一下我的实践,并在必须处理明确声明/定义复制构造函数时考虑以下情况.我将案例分为两类
我在本节中介绍了使用该类型正确操作程序所需的声明/定义复制构造函数的情况.
阅读完本节后,您将了解允许编译器自行生成复制构造函数的几个缺陷.因此,seand在他指出的答案,它始终是安全地关闭复制能力的一个新的类,并刻意启用它以后真的需要的时候.
声明一个私有的复制构造函数,并且不为它提供一个实现(这样即使该类的对象在类本身或其朋友中被复制,构建在链接阶段也会失败).
=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?
Run Code Online (Sandbox Code Playgroud)
在某些状态下可能存在可以安全复制对象的类(例如,默认构造状态),否则无法安全复制.如果我们想允许复制安全复制对象,那么 - 如果编程是防御性的 - 我们需要在用户定义的复制构造函数中进行运行时检查.
有时,应该可复制的类聚合不可复制的子对象.通常,对于具有不可观察状态的对象会发生这种情况(这种情况将在下面的"优化"部分中详细讨论).编译器只是帮助识别这种情况.
应该是可复制的类可以聚合准可复制类型的子对象.准可复制类型在严格意义上不提供复制构造函数,但具有另一个允许创建对象的概念副本的构造函数.使类型准可复制的原因是当没有关于该类型的复制语义的完全一致时.
例如,重新访问对象自注册案例,我们可以争辩说,可能存在这样的情况:只有当对象是完整的独立对象时,才必须向全局对象管理器注册该对象.如果它是另一个对象的子对象,那么管理它的责任在于它的包含对象.
或者,必须支持浅层和深层复制(它们都不是默认值).
然后最终决定留给该类型的用户 - 在复制对象时,他们必须明确指定(通过附加参数)预期的复制方法.
在采用非防御性编程方法的情况下,也可能存在常规复制构造函数和准复制构造函数.当在绝大多数情况下应该应用单一复制方法时,这是合理的,而在罕见但很好理解的情况下,应该使用替代复制方法.然后编译器不会抱怨它无法隐式定义复制构造函数; 用户应自行负责记住并检查是否应通过准复制构造函数复制该类型的子对象.
在极少数情况下,对象的可观察状态的子集可能构成(或被认为)对象身份的不可分割部分,不应转移到其他对象(尽管这可能有些争议).
例子:
对象的UID(但是这个也属于上面的"自注册"案例,因为id必须在自行注册的行为中获得).
在新对象不能继承源对象的历史记录但是以单个历史记录项" 从<OTHER_OBJECT_ID> <TIME>复制 "的情况下,对象的历史记录(例如,撤销/重做堆栈).
在这种情况下,复制构造函数必须跳过复制相应的子对象.
编译器提供的复制构造函数的签名取决于可用于子对象的复制构造函数.如果至少有一个子对象没有真正的复制构造函数(通过常量引用获取源对象),而是有一个变异的复制构造函数(通过非常量引用获取源对象),那么编译器将别无选择但要隐式声明然后定义一个变异的复制构造函数.
现在,如果子对象类型的"变异"复制构造函数实际上没有改变源对象(并且只是由不知道const关键字的程序员编写),该怎么办?如果我们不能通过添加缺失修复该代码const,那么另一个选项是声明我们自己的用户定义的复制构造函数具有正确的签名并承诺转向a const_cast.
已经放弃直接引用其内部数据的COW容器必须在构造时进行深度复制,否则它可能表现为引用计数句柄.
尽管COW是一种优化技术,但复制构造函数中的这种逻辑对于其正确实现至关重要.这就是为什么我把这个案例放在这里,而不是在"优化"部分,我们接下来.
在以下情况下,您可能需要/需要根据优化问题定义自己的复制构造函数:
考虑一个支持元素删除操作的容器,但可以通过简单地将删除的元素标记为已删除,并稍后再循环其插槽来实现.当制作这样一个容器的副本时,压缩幸存数据而不是按原样保留"已删除"的插槽可能是有意义的.
对象可能包含不属于其可观察状态的数据.通常,这是在对象的生命周期内累积的缓存/记忆数据,以加速对象执行的某些慢速查询操作.跳过复制该数据是安全的,因为在执行相关操作时(以及如果!)将重新计算该数据.复制此数据可能是不合理的,因为如果通过改变操作来修改对象的可观察状态(从中派生缓存数据),它可能会很快失效(如果我们不打算修改对象,为什么我们要创建一个深层然后复制?)
仅当辅助数据与表示可观察状态的数据相比较大时,才优化该优化.
C++允许通过声明复制构造函数来禁用隐式复制explicit.然后,该类的对象不能传递给函数和/或通过值从函数返回.这个技巧可以用于看似轻量级但复制起来确实非常昂贵的类型(但是,使其成为可复制的可能是更好的选择).
在C++ 03中声明一个复制构造函数也需要定义它(当然,如果你打算使用它).因此,仅仅考虑所讨论的问题,这样的复制构造函数意味着您必须编写编译器为您自动生成的相同代码.
C++ 11和更新的标准允许声明特殊的成员函数(默认和复制构造函数,复制赋值运算符和析构函数),并使用默认实现的显式请求 (只是结束声明
=default).
这个答案可以改进如下:
- 添加更多示例代码
- 说明"具有内部交叉引用的对象"的情况
- 添加一些链接
如果您有一个动态分配内容的类.例如,您将书籍的标题存储为char*并使用new设置标题,副本将不起作用.
你必须写一个复制构造函数,title = new char[length+1]然后strcpy(title, titleIn).复制构造函数只会执行"浅"复制.
| 归档时间: |
|
| 查看次数: |
66583 次 |
| 最近记录: |