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?
}
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;
}
};
// 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;
}
// 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;
}
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;
}
}
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.
//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.*/
8条答案
按热度按时间6mzjoqzu1#
简介
C++用 * 值语义 * 来处理用户定义类型的变量。这意味着对象在各种上下文中被隐式复制,我们应该理解“复制对象”的实际含义。
让我们考虑一个简单的例子:
(If您对
name(name), age(age)
部分感到困惑,这被称为member initializer list。)特殊成员函数
复制
person
对象意味着什么?main
函数显示了两种不同的复制场景。初始化person b(a);
由 * 复制构造函数 * 执行。它的工作是基于现有对象的状态构造一个新对象。赋值b = a
由 * 复制赋值运算符 * 执行。它的工作通常稍微复杂一些,因为目标对象已经处于需要处理的某种有效状态。因为我们自己既没有声明复制构造函数,也没有声明赋值运算符(或析构函数),所以它们都是为我们隐式定义的。
[...]复制构造函数和复制赋值运算符,[...]和析构函数是特殊的成员函数。[ * 注意 *:**当程序没有显式声明这些成员函数时,实现将隐式声明它们。**如果使用它们,实现将隐式定义它们。[...] end note ] [n3126.pdf section 12 §1]
默认情况下,复制对象意味着复制其成员:
非联合类X的隐式定义复制构造函数执行其子对象的成员式复制。
非联合类X的隐式定义的复制赋值运算符执行其子对象的成员式复制赋值。[n3126.pdf 12.8 §30]
隐式定义
person
的隐式定义的特殊成员函数如下所示:在这种情况下,成员级复制正是我们想要的:
name
和age
被复制,所以我们得到了一个自包含的、独立的person
对象。隐式定义的析构函数总是空的。这在本例中也很好,因为我们没有在构造函数中获取任何资源。成员的析构函数在person
析构函数完成后被隐式调用:在执行析构函数的主体并销毁主体中分配的所有自动对象之后,类X的析构函数调用X的直接[...]成员的析构函数[n3126.pdf 12.4 §6]
管理资源
那么我们什么时候应该显式声明那些特殊的成员函数呢?当我们的类 * 管理一个资源 * 时,也就是说,当类的一个对象 * 负责 * 那个资源时。这通常意味着资源在构造函数中被 * 获取 *(或者传递到构造函数中),在析构函数中被 * 释放 *。
让我们回到标准之前的C++时代。那时还没有
std::string
这样的东西,程序员们都喜欢指针。person
类可能看起来像这样:即使在今天,人们仍然用这种风格编写类,并陷入困境:“* 我把一个人推到一个向量中,现在我得到了疯狂的内存错误!”请记住,在默认情况下,复制一个对象意味着复制它的成员,但复制
name
成员只是复制一个指针, 而不是 * 它所指向的字符数组!这会产生一些不愉快的影响:1.通过
a
的变化可以通过b
观察。1.一旦
b
被销毁,a.name
就是一个悬空指针。1.如果
a
被破坏,删除悬空指针将产生undefined behavior。1.由于赋值没有考虑
name
在赋值之前指向什么,所以迟早会到处出现内存泄漏。明确定义
由于成员复制没有达到预期的效果,我们必须显式定义复制构造函数和复制赋值运算符,以生成字符数组的深层副本:
请注意初始化和赋值之间的区别:在赋值给
name
之前,我们必须删除旧的状态,以防止内存泄漏。同时,我们必须防止x = x
形式的自赋值。如果不进行该检查,delete[] name
将删除包含 source 字符串的数组,因为当您写入x = x
时,this->name
和that.name
包含相同的指针。异常安全
不幸的是,如果
new char[...]
由于内存耗尽而抛出异常,则此解决方案将失败。一个可能的解决方案是引入一个局部变量并重新排序语句:copy-and-swap idiom是解决这个问题的一个更健壮的方法,但是我不会在这里详细讨论异常安全性。我提到异常只是为了说明下面的观点:编写管理资源的类很难。
不可复制的资源
有些资源不能或不应该被复制,例如文件句柄或互斥锁。在这种情况下,只需将复制构造函数和复制赋值运算符声明为
private
,而不给出定义:或者,您可以从
boost::noncopyable
继承或将它们声明为已删除(在C++11及更高版本中):三的法则
有时你需要实现一个管理资源的类。(永远不要在一个类中管理多个资源,这只会带来痛苦。)在这种情况下,记住三个规则:
如果您需要自己显式声明析构函数、复制构造函数或复制赋值运算符,则可能需要显式声明所有这三个函数。
(不幸的是,这个“规则”并没有被C标准或我所知道的任何编译器强制执行。)
五的法则
从C11开始,一个对象有两个额外的特殊成员函数:移动构造函数和移动赋值函数。五态规则也实现了这些函数。
签名示例:
零的法则
3/5规则也被称为0/3/5规则。该规则的零部分声明,在创建类时,不允许编写任何特殊的成员函数。
建议
大多数情况下,您不需要自己管理资源,因为已有的类(如
std::string
)已经为您完成了这一工作。只需将使用std::string
成员的简单代码与使用char*
的复杂且容易出错的替代代码进行比较,您就会相信这一点。只要您远离原始指针成员,三规则不太可能涉及您自己代码。9lowa7mx2#
Rule of Three是C++的经验法则,基本上就是说
如果您的类需要
*赋值运算符,
明确定义,则可能需要所有这三个。
这是因为这三种方法通常都用于管理资源,如果您的类管理资源,则通常需要管理复制和释放。
如果没有好的语义来复制类管理的资源,那么考虑通过声明(而不是 defining)复制构造函数和赋值运算符为
private
来禁止复制。(Note即将到来的C标准的新版本(即C11)在C中添加了移动语义,这可能会改变三规则。但是,我对此知之甚少,无法在C11中编写关于三规则的章节。)
e5nszbig3#
三大定律如上所述。
用简单的英语来说,它解决的问题是:
非默认析构函数
您在构造函数中分配了内存,因此需要编写析构函数来删除它。否则将导致内存泄漏。
您可能会认为这是工作完成。
问题是,如果一个副本是由你的对象,那么副本将指向相同的内存作为原始对象。
一旦其中一个删除了它的析构函数中的内存,另一个将有一个指向无效内存的指针(这被称为悬空指针),当它试图使用它时,事情会变得很棘手。
因此,您可以编写一个复制构造函数,以便它为新对象分配它们自己的内存块来销毁。
赋值运算符和复制构造函数
您在构造函数中为类的成员指针分配了内存。当您复制该类的对象时,默认的赋值运算符和复制构造函数会将此成员指针的值复制到新对象。
这意味着新对象和旧对象将指向同一块内存,所以当你在一个对象中更改它时,另一个对象也会更改它。如果一个对象删除了这个内存,另一个对象将继续尝试使用它- eek。
为了解决这个问题,你可以编写自己的复制构造函数和赋值运算符,你的版本为新对象分配单独的内存,并复制第一个指针指向的值,而不是它的地址。
kzmpq1sx4#
基本上,如果你有一个析构函数(不是默认的析构函数),这意味着你定义的类有一些内存分配。
如果MyClass只有一些基本类型的成员,默认的赋值运算符可以工作,但如果它有一些指针成员和对象没有赋值运算符,结果将是不可预测的。因此,我们可以说,如果在类的析构函数中有东西要删除,我们可能需要一个深复制运算符,这意味着我们应该提供一个复制构造函数和赋值运算符。
k97glaaz5#
复制对象是什么意思?有几种方法可以复制对象--让我们谈谈你最可能提到的两种--深度复制和浅复制。
由于我们使用的是面向对象语言(或者至少假设是这样),假设你分配了一块内存。由于这是一种面向对象语言,我们可以很容易地引用我们分配的内存块,因为它们通常是原始变量(int、char、bytes)或者我们定义的由我们自己的类型和原始变量组成的类。因此,假设我们有一个Car类,如下所示:
深层副本是指如果我们声明一个对象,然后创建该对象的一个完全独立的副本......我们最终在两个完全相同的内存集中拥有两个对象。
现在让我们做一些奇怪的事情。让我们假设car 2要么是编程错误,要么是故意要共享car 1的实际内存。(这样做通常是错误的,在课堂上通常是讨论它的毯子。)假装无论何时你问car 2,你实际上是在解析指向car 1内存空间的指针......这或多或少就是浅副本。
所以不管你用什么语言写,在复制对象时要非常小心你的意思,因为大多数时候你需要一个深副本。
什么是复制构造函数和复制赋值运算符?我已经在上面用过了。当你输入
Car car2 = car1;
这样的代码时,复制构造函数会被调用。本质上,如果你在一行中声明一个变量并对其赋值,这就是调用复制构造函数的时候。赋值运算符是当你使用等号--car2 = car1;
时发生的。注意car2
不是'为这些操作编写的两段代码很可能非常相似。实际上,典型的设计模式有另一个函数,一旦你对初始的复制/赋值合法感到满意,你就可以调用它来设置一切--如果你看一下我编写的普通代码,你会发现这两个函数几乎是一样的。我什么时候需要自己声明它们呢?如果你写的代码不是要共享的,也不是要以某种方式用于生产,那么你只需要在需要它们的时候声明它们。如果你“偶然”地选择使用你的程序语言,而没有创建一个,你需要知道你的程序语言会做什么--也就是说,你得到了编译器的默认值。例如,我很少使用复制构造函数。但是赋值运算符覆盖是非常常见的。2你知道你也可以覆盖加法、减法等的含义吗?
如何防止我的对象被复制?一个合理的做法是用一个私有函数覆盖所有允许为对象分配内存的方法。如果你真的不想让别人复制它们,你可以将其公开,并通过抛出异常来警告程序员,同时也不要复制对象。
8qgya5xd6#
我什么时候需要自己申报?
“三规则”指出,如果您声明
1.复制构造函数
1.复制赋值运算符
1.析构器
那么您应该声明所有这三个操作。它源于这样一种观察:接管复制操作含义的需要几乎总是源于执行某种资源管理的类,而这几乎总是意味着
三原则的一个结果是,用户声明的析构函数的存在表明简单的成员智能复制不太可能适用于类中的复制操作。这反过来又表明,如果一个类声明了析构函数,复制操作可能不应该自动生成,因为它们不会做正确的事情。在采用C98时,这种推理的重要性并没有被完全理解,所以在C98中,用户声明的析构函数的存在并没有影响编译器生成复制操作的意愿。在C++11中仍然是这样,但这仅仅是因为限制生成复制操作的条件会破坏太多的遗留代码。
如何防止对象被复制?
将复制构造函数和复制赋值运算符声明为私有访问说明符。
在C++11以后的版本中,您还可以声明复制构造函数和赋值运算符已删除
r8uurelv7#
许多现有的答案已经触及到复制构造函数、赋值运算符和析构函数。然而,在postC ++11中,移动语义的引入可能会将其扩展到3以上。
最近,迈克尔·克莱斯(Michael Claisse)做了一个演讲,谈到了这个主题:http://channel9.msdn.com/events/CPP/C-PP-Con-2014/The-Canonical-Class
zhte4eai8#
规则三在C中是一个基本的设计和开发原则,要求三个成员函数中如果有明确定义的下列其中一个成员函数,那么程序员就应该一起定义另外两个成员函数。即下列三个成员函数是不可缺少的:析构函数、复制构造函数、复制赋值运算符。
C中的复制构造函数是一种特殊的构造函数,用来构建一个新的对象,这个新的对象等价于一个现有对象的副本。
复制赋值运算符是一种特殊的赋值运算符,通常用于将现有对象指定给其他相同类型的对象。
以下是一些快速示例: