-
09-10-2019 - |
题
- 有什么 复制对象 意思是?
- 什么是 复制构造函数 和 复制分配操作员?
- 我什么时候需要自己声明它们?
- 如何防止我的物体被复制?
解决方案
介绍
C ++使用用户定义类型的变量 价值语义。这意味着对象在各种上下文中被隐式复制,我们应该理解“复制对象”的实际含义。
让我们考虑一个简单的例子:
class person
{
std::string name;
int age;
public:
person(const std::string& name, int age) : name(name), age(age)
{
}
};
int main()
{
person a("Bjarne Stroustrup", 60);
person b(a); // What happens here?
b = a; // And here?
}
(如果您被 name(name), age(age)
部分,这称为 成员初始化列表.)
特殊会员功能
复制一个是什么意思 person
目的?这 main
功能显示两个不同的复制方案。初始化 person b(a);
由 复制构造函数。它的工作是根据现有对象的状态构建新的对象。那作业 b = a
由 复制分配操作员。它的工作通常更为复杂,因为目标对象已经处于需要处理的某种有效状态。
由于我们自己既没有自己宣布复制构造函数,也不宣布分配运营商(也不是毁灭者),因此这些是为我们隐含的定义。标准的报价:
...]复制构造函数和复制分配运算符[...]和Destructor是特殊的成员函数。 [[ 笔记: 当程序未明确声明它们时,该实现将隐式地为某些类类型声明这些成员功能。如果使用使用,则实现将隐式定义它们。 [... 结尾注 ] [N3126.PDF第12条第1节
默认情况下,复制对象意味着复制其成员:
非工会X类隐式定义的复制构造函数执行其子对象的成员副本。 [N3126.PDF第12.8§16
非工会X类隐式定义的副本分配运算符执行其子对象的成员复制分配。 [N3126.PDF第12.8§30
隐式定义
隐式定义的特殊会员功能 person
看起来像这样:
// 1. copy constructor
person(const person& that) : name(that.name), age(that.age)
{
}
// 2. copy assignment operator
person& operator=(const person& that)
{
name = that.name;
age = that.age;
return *this;
}
// 3. destructor
~person()
{
}
在这种情况下,成员的复制正是我们想要的:name
和 age
被复制,所以我们得到了一个独立的独立 person
目的。隐式定义的破坏者始终是空的。在这种情况下,这也很好,因为我们没有在构造函数中获得任何资源。成员的破坏者被隐式地调用 person
破坏者完成:
在执行击曲线的身体并破坏了身体内部分配的任何自动对象之后,X类的驱动器呼叫X的Direct [...]成员[N3126.pdf12.4§6]呼叫破坏者。
管理资源
那么,我们什么时候应该明确声明这些特殊会员功能?当我们的课时 管理资源, ,也就是说,当班级的对象是 负责任的 为此资源。这通常意味着资源是 获得 在构造函数中(或传递到构造函数)和 发行 在灾难中。
让我们及时回去预标准C ++。没有这样的东西 std::string
, ,程序员爱上了指针。这 person
课程可能看起来像这样:
class person
{
char* name;
int age;
public:
// the constructor acquires a resource:
// in this case, dynamic memory obtained via new[]
person(const char* the_name, int the_age)
{
name = new char[strlen(the_name) + 1];
strcpy(name, the_name);
age = the_age;
}
// the destructor must release this resource via delete[]
~person()
{
delete[] name;
}
};
即使在今天,人们仍然以这种风格写课并陷入麻烦。”我把一个人推入矢量,现在我遇到了疯狂的记忆错误!“请记住,默认情况下,复制对象意味着复制其成员,但要复制 name
成员只是复制指针, 不是 它指向的角色数组!这有几种不愉快的影响:
- 通过
a
可以通过b
. - 一次
b
被摧毁,a.name
是一个悬空的指针。 - 如果
a
被摧毁,删除了悬空的指针产量 不确定的行为. - 由于作业没有考虑什么
name
在作业之前,您迟早会在整个地方获得内存泄漏。
显式定义
由于会员的复制没有所需的效果,因此我们必须明确定义复制构造函数和复制分配运算符,以使角色数组的深层副本:
// 1. copy constructor
person(const person& that)
{
name = new char[strlen(that.name) + 1];
strcpy(name, that.name);
age = that.age;
}
// 2. copy assignment operator
person& operator=(const person& that)
{
if (this != &that)
{
delete[] name;
// This is a dangerous point in the flow of execution!
// We have temporarily invalidated the class invariants,
// and the next statement might throw an exception,
// leaving the object in an invalid state :(
name = new char[strlen(that.name) + 1];
strcpy(name, that.name);
age = that.age;
}
return *this;
}
注意初始化和分配之间的区别:我们必须在分配给之前拆除旧状态 name
为了防止记忆泄漏。另外,我们必须防止对形式的自我分配 x = x
。没有该检查, delete[] name
将删除包含的数组 资源 字符串,因为当你写 x = x
, , 两个都 this->name
和 that.name
包含相同的指针。
例外安全
不幸的是,如果此解决方案将失败 new char[...]
由于记忆力耗尽而引发异常。一种可能的解决方案是引入局部变量并重新排序语句:
// 2. copy assignment operator
person& operator=(const person& that)
{
char* local_name = new char[strlen(that.name) + 1];
// If the above statement throws,
// the object is still in the same state as before.
// None of the following statements will throw an exception :)
strcpy(local_name, that.name);
delete[] name;
name = local_name;
age = that.age;
return *this;
}
这也可以在没有明确检查的情况下照顾自我分配。解决这个问题的一个更强大的解决方案是 复制和交换成语,但我不会在这里介绍例外安全的细节。我只提到例外来提出以下要点: 编写管理资源的课程很难。
不可复制的资源
某些资源无法复制或不应该复制,例如文件处理或静音。在这种情况下,只需将复制构造函数和复制分配运营商声明为 private
没有给出定义:
private:
person(const person& that);
person& operator=(const person& that);
或者,您可以继承 boost::noncopyable
或将其声明为已删除(在C ++ 11及以上):
person(const person& that) = delete;
person& operator=(const person& that) = delete;
三个规则
有时,您需要实现管理资源的类。 (从不管理单个类中的多个资源,这只会导致痛苦。)在这种情况下,请记住 三个规则:
如果您需要明确声明删除器,复制构造函数或复制分配运算符,则可能需要明确声明所有三个。
(不幸的是,此“规则”并未由C ++标准或我知道的任何编译器强制执行。)
五个规则
从C ++ 11开始,一个对象具有2个额外的特殊成员功能:移动构造函数和移动分配。五个国家也实施这些职能的规则。
签名的一个例子:
class person
{
std::string name;
int age;
public:
person(const std::string& name, int age); // Ctor
person(const person &) = default; // Copy Ctor
person(person &&) noexcept = default; // Move Ctor
person& operator=(const person &) = default; // Copy Assignment
person& operator=(person &&) noexcept = default; // Move Assignment
~person() noexcept = default; // Dtor
};
零规则
3/5的规则也称为0/3/5的规则。该规则的零部分指出,在创建课程时,您不得写任何特殊成员功能。
建议
在大多数情况下,您不需要自己管理资源,因为 std::string
已经为您做了。只需使用一个简单的代码 std::string
使用A char*
而且您应该说服。只要您远离原始指针成员,三个规则就不太可能涉及您自己的代码。
其他提示
三巨头的定律如上所述。
简单的英语简单例子就是它解决的问题:
非默认破坏者
您在构造函数中分配了内存,因此您需要编写攻击函数以删除它。否则,您将导致内存泄漏。
您可能会认为这是工作。
问题将是,如果对您的对象进行了副本,则该副本将指向与原始对象相同的内存。
曾经,其中一个会删除其击曲子中的内存,另一个将在尝试使用它时具有无效记忆的指针(这称为悬挂指针)。
因此,您可以编写一个复制构造函数,以便将新对象分配给他们自己的内存片段销毁。
分配操作员和复制构造函数
您将构造函数中的内存分配给了班级的成员指针。当您复制此类的对象时,默认分配运算符和复制构造函数将将此成员指针的值复制到新对象。
这意味着新对象和旧对象将指向同一内存,因此当您在一个对象中更改它时,它也会更改另一个对象。如果一个对象删除此内存,另一个对象将继续尝试使用它-EEK。
为了解决此问题,您可以编写自己的版本的复制构造函数和分配运算符。您的版本将内存分配给新对象,并在第一个指针指向而不是其地址的值中复制。
基本上,如果您有攻击函数(不是默认驱动器),则意味着您定义的类具有内存分配。假设该类是由某些客户端代码或您在外面使用的。
MyClass x(a, b);
MyClass y(c, d);
x = y; // This is a shallow copy if assignment operator is not provided
如果MyClass只有一些原始打字成员,则默认分配运算符将有效,但是如果它具有一些指针成员和对象,而没有分配运算符,则结果将是无法预测的。因此,我们可以说,如果在班级的破坏者中删除了一些东西,我们可能需要一个深层的操作员,这意味着我们应该提供复制构造函数和分配运算符。
复制对象的含义是什么?您可以通过几种方法复制对象 - Loet谈论您最有可能引用的两种,即深副本和浅副本。
由于我们采用了面向对象的语言(或至少在假设的语言中),因此假设您分配了一段记忆。由于它是一个OO语言,因此我们可以轻松地指我们分配的大量内存,因为它们通常是原始变量(INT,Chars,Bytes)或我们定义的类型和原始类型的类别。假设我们有一类汽车,如下所示:
class Car //A very simple class just to demonstrate what these definitions mean.
//It's pseudocode C++/Javaish, I assume strings do not need to be allocated.
{
private String sPrintColor;
private String sModel;
private String sMake;
public changePaint(String newColor)
{
this.sPrintColor = newColor;
}
public Car(String model, String make, String color) //Constructor
{
this.sPrintColor = color;
this.sModel = model;
this.sMake = make;
}
public ~Car() //Destructor
{
//Because we did not create any custom types, we aren't adding more code.
//Anytime your object goes out of scope / program collects garbage / etc. this guy gets called + all other related destructors.
//Since we did not use anything but strings, we have nothing additional to handle.
//The assumption is being made that the 3 strings will be handled by string's destructor and that it is being called automatically--if this were not the case you would need to do it here.
}
public Car(const Car &other) // Copy Constructor
{
this.sPrintColor = other.sPrintColor;
this.sModel = other.sModel;
this.sMake = other.sMake;
}
public Car &operator =(const Car &other) // Assignment Operator
{
if(this != &other)
{
this.sPrintColor = other.sPrintColor;
this.sModel = other.sModel;
this.sMake = other.sMake;
}
return *this;
}
}
深副本是如果我们声明一个对象,然后创建对象的完全独立的副本……我们最终在2个完全内存中以2个对象形成。
Car car1 = new Car("mustang", "ford", "red");
Car car2 = car1; //Call the copy constructor
car2.changePaint("green");
//car2 is now green but car1 is still red.
现在让我们做一些奇怪的事情。假设CAR2是错误的,或者是故意分享CAR1所制成的实际记忆。 (这样做通常是一个错误是。
//Shallow copy example
//Assume we're in C++ because it's standard behavior is to shallow copy objects if you do not have a constructor written for an operation.
//Now let's assume I do not have any code for the assignment or copy operations like I do above...with those now gone, C++ will use the default.
Car car1 = new Car("ford", "mustang", "red");
Car car2 = car1;
car2.changePaint("green");//car1 is also now green
delete car2;/*I get rid of my car which is also really your car...I told C++ to resolve
the address of where car2 exists and delete the memory...which is also
the memory associated with your car.*/
car1.changePaint("red");/*program will likely crash because this area is
no longer allocated to the program.*/
因此,无论您用哪种语言写的语言,都要非常谨慎地复制对象的含义,因为大多数时候您想要深层副本。
什么是复制构造函数和复制分配运算符?我已经在上面使用了它们。当您键入代码(例如)时,调用复制构造函数 Car car2 = car1;
从本质上讲,如果您声明一个变量并将其分配为一行,则是调用复制构造函数的时候。分配运算符是当您使用平等符号时会发生什么 -car2 = car1;
. 。注意 car2
在同一陈述中没有声明。您为这些操作编写的两个代码可能非常相似。实际上,典型的设计模式具有您调用的另一个函数来设置所有满足后的所有内容,即初始副本/分配是合法的 - 如果您查看我编写的Longhand代码,则功能几乎相同。
我什么时候需要自己声明它们?如果您不编写要以某种方式共享或生产的代码,则实际上只需要在需要时声明它们。您确实需要意识到如果您选择“偶然”使用它,而没有制作程序语言,则您的程序语言会做什么 - 您会得到编译器默认值。我很少使用复制构造函数,但是分配运算符替代非常普遍。您是否知道您可以覆盖什么添加,减法等也意味着什么?
如何防止我的物体被复制?覆盖您允许使用私有功能为对象分配内存的所有方法都是合理的开始。如果您真的不希望人们复制它们,则可以将其公开并通过抛出异常并不复制对象来提醒程序员。
我什么时候需要自己声明它们?
三个规则,如果您声明任何一个
- 复制构造函数
- 复制分配操作员
- 驱动器
然后,您应该声明这三个。它从观察结果中得出的,即需要接管副本操作的含义几乎总是源于执行某种资源管理的班级,并且几乎总是暗示
在一个副本操作中进行的任何资源管理都可能需要在其他副本操作中进行,并且
班级驱动器还将参与资源管理(通常释放它)。要管理的经典资源是内存,这就是为什么所有管理内存的标准库类(例如,执行动态内存管理的STL容器)都声明“三巨头”:复制操作和驱动器。
三个规则的结果 是否存在用户宣布的破坏者表明简单的成员明智副本不太可能适合同类中的复制操作。反过来,这表明,如果一类声明灾难,则可能不应自动生成复制操作,因为它们不会做正确的事情。在采用了C ++ 98时,这种推理线的重要性尚未得到完全理解,因此在C ++ 98中,用户声明的destructor的存在对编译器生成复制操作的意愿没有影响。在C ++ 11中仍然是这种情况,但这仅仅是因为限制了生成复制操作的条件会破坏太多的遗留代码。
如何防止我的物体被复制?
将复制构造函数和复制分配操作员作为私人访问说明符。
class MemoryBlock
{
public:
//code here
private:
MemoryBlock(const MemoryBlock& other)
{
cout<<"copy constructor"<<endl;
}
// Copy assignment operator.
MemoryBlock& operator=(const MemoryBlock& other)
{
return *this;
}
};
int main()
{
MemoryBlock a;
MemoryBlock b(a);
}
在C ++ 11中,您还可以声明复制构造函数和分配操作员已删除
class MemoryBlock
{
public:
MemoryBlock(const MemoryBlock& other) = delete
// Copy assignment operator.
MemoryBlock& operator=(const MemoryBlock& other) =delete
};
int main()
{
MemoryBlock a;
MemoryBlock b(a);
}
许多现有答案已经触及复制构造函数,分配运算符和驱动器。但是,在C ++ 11次中,移动语义的引入可能会扩展到3。
最近,迈克尔·克莱斯(Michael Claisse)发表了一场演讲,涉及这个话题:http://channel9.msdn.com/events/cpp/c-ppp-con-2014/the-canonical-class
C ++中的三个规则是设计的基本原则,并且开发了三个要求,如果以下成员函数之一中有明确的定义,那么程序员应将其他两个成员定义在一起。即,以下三个成员函数是必不可少的:驱动器,复制构造函数,复制分配运算符。
C ++中的复制构造函数是一个特殊的构造函数。它用于构建一个新对象,该对象是等同于现有对象副本的新对象。
复制分配运算符是一个特殊的分配操作员,通常用于向相同类型对象的其他对象指定现有对象。
有快速的例子:
// default constructor
My_Class a;
// copy constructor
My_Class b(a);
// copy constructor
My_Class c = a;
// copy assignment operator
b = a;