什么是三法则?
- 复制对象是什么意思?
- 什么是复制构造函数和复制赋值运算符?
- 我什么时候需要自己申报?
- 如何防止我的对象被复制?
- What does copying an object mean?
- What are the copy constructor and the copy assignment operator?
- When do I need to declare them myself?
- How can I prevent my objects from being copied?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(8)
简介
C++ 使用值语义来处理用户定义类型的变量。
这意味着对象在各种上下文中隐式复制,
我们应该理解“复制对象”的实际含义。
让我们考虑一个简单的例子:(
如果您对
name(name),age(age) 部分感到困惑,
这称为成员初始值设定项列表。)
特殊成员函数
复制
人
意味着什么目的?main
函数显示了两种不同的复制场景。初始化
person b(a);
由复制构造函数执行。它的工作是根据现有对象的状态构造一个新的对象。
赋值
b = a
由复制赋值运算符执行。它的工作通常有点复杂
因为目标对象已经处于需要处理的某种有效状态。
由于我们自己既没有声明复制构造函数也没有声明赋值运算符(也没有声明析构函数),
这些是为我们隐式定义的。引用自标准:
默认情况下,复制对象意味着复制其成员:
隐式定义
person
的隐式定义的特殊成员函数如下所示:成员复制正是我们在这种情况下想要的:
name
和age
被复制,因此我们得到一个独立的、独立的person
对象。隐式定义的析构函数始终为空。
在这种情况下这也很好,因为我们没有在构造函数中获取任何资源。
在
person
析构函数完成后,隐式调用成员的析构函数:管理资源
那么我们什么时候应该显式声明这些特殊成员函数呢?
当我们的类管理资源时,即
当该类的对象负责该资源时。
这通常意味着资源是在构造函数中获取的
(或传递到构造函数中)并在析构函数中释放。
让我们回到 C++ 标准之前的时代。
根本不存在
std::string
这样的东西,而且程序员们都非常喜欢指针。person
类可能看起来像这样:即使在今天,人们仍然以这种风格编写类并遇到麻烦:
“我将一个人推入向量,现在我出现了疯狂的记忆错误!”
请记住,默认情况下,复制对象意味着复制其成员,
但是复制
name
成员只是复制一个指针,而不是它指向的字符数组!这会产生一些令人不快的影响:
a
进行的更改可以通过b
观察到。b
被销毁,a.name
就是一个悬空指针。a
被销毁,删除悬空指针会产生未定义的行为。name
指向的内容,迟早你会到处出现内存泄漏。
显式定义
由于成员复制没有达到预期的效果,因此我们必须显式定义复制构造函数和复制赋值运算符来对字符数组进行深层复制:
注意初始化和赋值之间的区别:
我们必须在将旧状态分配给
name
之前将其拆除,以防止内存泄漏。此外,我们还必须防止
x = x
形式的自分配。如果没有该检查,
delete[] name
将删除包含 source 字符串的数组,因为当您编写
x = x
时,this->name
和that.name
都包含相同的指针。异常安全
不幸的是,如果 new char[...] 由于内存耗尽而引发异常,则此解决方案将失败。
一种可能的解决方案是引入局部变量并对语句重新排序:
这也可以在没有显式检查的情况下处理自分配。
这个问题的一个更强大的解决方案是复制和交换惯用法,
但这里我不会详细讨论异常安全。
我只提到例外是为了表明以下观点: 编写管理资源的类很困难。
不可复制的资源
某些资源不能或不应该被复制,例如文件句柄或互斥体。
在这种情况下,只需将复制构造函数和复制赋值运算符声明为
private
而不给出定义:或者,您可以从
boost::noncopyable
继承或将它们声明为已删除 (在 C++11 及以上版本中):三法则
有时您需要实现一个管理资源的类。
(永远不要在一个类中管理多个资源,
这只会导致痛苦。)
在这种情况下,请记住三法则:
(不幸的是,这个“规则”并不是由 C++ 标准或我所知道的任何编译器强制执行的。)
五规则
从 C++11 开始,对象有 2 个额外的特殊成员函数:移动构造函数和移动赋值。统治五州也履行这些职能。
签名示例:
零规则
3/5 规则也称为 0/3/5 规则。规则的零部分指出,在创建类时不允许您编写任何特殊成员函数。
建议
大多数时候,您不需要自己管理资源,
因为现有的类(例如
std::string
)已经为您完成了这项工作。只需使用
std::string
成员比较简单的代码到使用
char*
的复杂且容易出错的替代方案,您应该相信。只要您远离原始指针成员,三法则就不太可能涉及您自己的代码。
Introduction
C++ treats variables of user-defined types with value semantics.
This means that objects are implicitly copied in various contexts,
and we should understand what "copying an object" actually means.
Let us consider a simple example:
(If you are puzzled by the
name(name), age(age)
part,this is called a member initializer list.)
Special member functions
What does it mean to copy a
person
object?The
main
function shows two distinct copying scenarios.The initialization
person b(a);
is performed by the copy constructor.Its job is to construct a fresh object based on the state of an existing object.
The assignment
b = a
is performed by the copy assignment operator.Its job is generally a little more complicated
because the target object is already in some valid state that needs to be dealt with.
Since we declared neither the copy constructor nor the assignment operator (nor the destructor) ourselves,
these are implicitly defined for us. Quote from the standard:
By default, copying an object means copying its members:
Implicit definitions
The implicitly-defined special member functions for
person
look like this:Memberwise copying is exactly what we want in this case:
name
andage
are copied, so we get a self-contained, independentperson
object.The implicitly-defined destructor is always empty.
This is also fine in this case since we did not acquire any resources in the constructor.
The members' destructors are implicitly called after the
person
destructor is finished:Managing resources
So when should we declare those special member functions explicitly?
When our class manages a resource, that is,
when an object of the class is responsible for that resource.
That usually means the resource is acquired in the constructor
(or passed into the constructor) and released in the destructor.
Let us go back in time to pre-standard C++.
There was no such thing as
std::string
, and programmers were in love with pointers.The
person
class might have looked like this:Even today, people still write classes in this style and get into trouble:
"I pushed a person into a vector and now I get crazy memory errors!"
Remember that by default, copying an object means copying its members,
but copying the
name
member merely copies a pointer, not the character array it points to!This has several unpleasant effects:
a
can be observed viab
.b
is destroyed,a.name
is a dangling pointer.a
is destroyed, deleting the dangling pointer yields undefined behavior.name
pointed to before the assignment,sooner or later you will get memory leaks all over the place.
Explicit definitions
Since memberwise copying does not have the desired effect, we must define the copy constructor and the copy assignment operator explicitly to make deep copies of the character array:
Note the difference between initialization and assignment:
we must tear down the old state before assigning it to
name
to prevent memory leaks.Also, we have to protect against the self-assignment of the form
x = x
.Without that check,
delete[] name
would delete the array containing the source string,because when you write
x = x
, boththis->name
andthat.name
contain the same pointer.Exception safety
Unfortunately, this solution will fail if
new char[...]
throws an exception due to memory exhaustion.One possible solution is to introduce a local variable and reorder the statements:
This also takes care of self-assignment without an explicit check.
An even more robust solution to this problem is the copy-and-swap idiom,
but I will not go into the details of exception safety here.
I only mentioned exceptions to make the following point: Writing classes that manage resources is hard.
Noncopyable resources
Some resources cannot or should not be copied, such as file handles or mutexes.
In that case, simply declare the copy constructor and copy assignment operator as
private
without giving a definition:Alternatively, you can inherit from
boost::noncopyable
or declare them as deleted (in C++11 and above):The rule of three
Sometimes you need to implement a class that manages a resource.
(Never manage multiple resources in a single class,
this will only lead to pain.)
In that case, remember the rule of three:
(Unfortunately, this "rule" is not enforced by the C++ standard or any compiler I am aware of.)
The rule of five
From C++11 on, an object has 2 extra special member functions: the move constructor and move assignment. The rule of five states to implement these functions as well.
An example with the signatures:
The rule of zero
The rule of 3/5 is also referred to as the rule of 0/3/5. The zero part of the rule states that you are allowed to not write any of the special member functions when creating your class.
Advice
Most of the time, you do not need to manage a resource yourself,
because an existing class such as
std::string
already does it for you.Just compare the simple code using a
std::string
memberto the convoluted and error-prone alternative using a
char*
and you should be convinced.As long as you stay away from raw pointer members, the rule of three is unlikely to concern your own code.
三法则基本上是 C++ 的经验法则说
原因是它们三个通常都用来管理资源,如果你的类管理资源,它通常需要管理复制和释放。
如果没有好的语义来复制类管理的资源,则考虑通过声明来禁止复制(而不是 定义)复制构造函数和赋值运算符为
私有
。(请注意,即将推出的新版本 C++ 标准(即 C++11)向 C++ 添加了移动语义,这可能会改变三法则。但是,我对此知之甚少,无法编写 C++11 部分关于三法则。)
The Rule of Three is a rule of thumb for C++, basically saying
The reasons for this is that all three of them are usually used to manage a resource, and if your class manages a resource, it usually needs to manage copying as well as freeing.
If there is no good semantic for copying the resource your class manages, then consider to forbid copying by declaring (not defining) the copy constructor and assignment operator as
private
.(Note that the forthcoming new version of the C++ standard (which is C++11) adds move semantics to C++, which will likely change the Rule of Three. However, I know too little about this to write a C++11 section about the Rule of Three.)
三巨头法则如上所述。
一个简单的例子,用简单的英语来说明它解决的问题:
非默认析构函数
您在构造函数中分配了内存,因此您需要编写一个析构函数来删除它。否则会导致内存泄漏。
您可能认为这已经完成了。
问题是,如果您的对象有一个副本,那么该副本将指向与原始对象相同的内存。
一旦其中一个在其析构函数中删除了内存,另一个将有一个指向无效内存的指针(这称为悬空指针),当它尝试使用它时,事情会变得很棘手。
因此,您编写一个复制构造函数,以便它为新对象分配自己的内存块以进行销毁。
赋值运算符和复制构造函数
您在构造函数中将内存分配给了类的成员指针。当您复制此类的对象时,默认赋值运算符和复制构造函数将将此成员指针的值复制到新对象。
这意味着新对象和旧对象将指向同一块内存,因此当您在一个对象中更改它时,另一个对象也会发生更改。如果一个对象删除了该内存,另一个对象将继续尝试使用它 - 哎呀。
要解决此问题,您可以编写自己版本的复制构造函数和赋值运算符。您的版本为新对象分配单独的内存,并复制第一个指针指向的值而不是其地址。
The law of the big three is as specified above.
An easy example, in plain English, of the kind of problem it solves:
Non default destructor
You allocated memory in your constructor and so you need to write a destructor to delete it. Otherwise you will cause a memory leak.
You might think that this is job done.
The problem will be, if a copy is made of your object, then the copy will point to the same memory as the original object.
Once, one of these deletes the memory in its destructor, the other will have a pointer to invalid memory (this is called a dangling pointer) when it tries to use it things are going to get hairy.
Therefore, you write a copy constructor so that it allocates new objects their own pieces of memory to destroy.
Assignment operator and copy constructor
You allocated memory in your constructor to a member pointer of your class. When you copy an object of this class the default assignment operator and copy constructor will copy the value of this member pointer to the new object.
This means that the new object and the old object will be pointing at the same piece of memory so when you change it in one object it will be changed for the other objerct too. If one object deletes this memory the other will carry on trying to use it - eek.
To resolve this you write your own version of the copy constructor and assignment operator. Your versions allocate separate memory to the new objects and copy across the values that the first pointer is pointing to rather than its address.
基本上,如果您有析构函数(不是默认析构函数),则意味着您定义的类具有一些内存分配。假设该类由某些客户端代码或您在外部使用。
如果 MyClass 仅具有一些基本类型成员,则默认赋值运算符将起作用,但如果它具有一些没有赋值运算符的指针成员和对象,则结果将是不可预测的。因此我们可以说,如果类的析构函数中有需要删除的内容,我们可能需要一个深复制运算符,这意味着我们应该提供一个复制构造函数和赋值运算符。
Basically if you have a destructor (not the default destructor) it means that the class that you defined has some memory allocation. Suppose that the class is used outside by some client code or by you.
If MyClass has only some primitive typed members a default assignment operator would work but if it has some pointer members and objects that do not have assignment operators the result would be unpredictable. Therefore we can say that if there is something to delete in destructor of a class, we might need a deep copy operator which means we should provide a copy constructor and assignment operator.
复制对象是什么意思?
有几种方法可以复制对象 - 让我们谈谈您最有可能提到的两种 - 深复制和浅复制。
由于我们使用面向对象的语言(或者至少假设如此),因此假设您分配了一块内存。由于它是面向对象语言,我们可以轻松引用我们分配的内存块,因为它们通常是原始变量(整数、字符、字节)或我们定义的由我们自己的类型和原语组成的类。假设我们有一个 Car 类,如下所示:
深层复制是指如果我们声明一个对象,然后创建该对象的完全独立的副本……我们最终会在 2 个完整的内存集中得到 2 个对象。
现在让我们做一些奇怪的事情。假设 car2 要么编程错误,要么故意共享 car1 的实际内存。 (这样做通常是错误的,在课堂上通常是讨论它的毯子。)假装每当你询问 car2 时,你实际上正在解析指向 car1 内存空间的指针......这或多或少是浅拷贝是。
因此,无论您使用哪种语言编写,在复制对象时都要非常小心您的含义,因为大多数时候您需要深层复制。
什么是复制构造函数和复制赋值运算符?
我已经在上面使用过它们了。当您键入诸如
Car car2 = car1;
之类的代码时,就会调用复制构造函数。本质上,如果您声明一个变量并在一行中对其进行赋值,那么复制构造函数就会被调用。赋值运算符就是使用等号时发生的情况 -car2 = car1;
。请注意car2
未在同一语句中声明。您为这些操作编写的两段代码可能非常相似。事实上,典型的设计模式有另一个函数,一旦您满意初始副本/分配是合法的,您就可以调用它来设置所有内容 - 如果您查看我编写的普通代码,这些函数几乎是相同的。我什么时候需要自己申报?
如果您编写的代码不是要共享或以某种方式用于生产,那么您实际上只需要在需要时声明它们。如果您“偶然”选择使用您的程序语言并且没有创建它,那么您确实需要了解您的程序语言的作用,即您获得了编译器默认值。例如,我很少使用复制构造函数,但赋值运算符覆盖非常常见。您知道您也可以覆盖加法、减法等的含义吗?
如何防止我的对象被复制?
重写允许使用私有函数为对象分配内存的所有方式是一个合理的开始。如果您确实不希望人们复制它们,您可以将其公开并通过抛出异常而不复制对象来警告程序员。
What does copying an object mean?
There are a few ways you can copy objects--let's talk about the 2 kinds you're most likely referring to--deep copy and shallow copy.
Since we're in an object-oriented language (or at least are assuming so), let's say you have a piece of memory allocated. Since it's an OO-language, we can easily refer to chunks of memory we allocate because they are usually primitive variables (ints, chars, bytes) or classes we defined that are made of our own types and primitives. So let's say we have a class of Car as follows:
A deep copy is if we declare an object and then create a completely separate copy of the object...we end up with 2 objects in 2 completely sets of memory.
Now let's do something strange. Let's say car2 is either programmed wrong or purposely meant to share the actual memory that car1 is made of. (It's usually a mistake to do this and in classes is usually the blanket it's discussed under.) Pretend that anytime you ask about car2, you're really resolving a pointer to car1's memory space...that's more or less what a shallow copy is.
So regardless of what language you're writing in, be very careful about what you mean when it comes to copying objects because most of the time you want a deep copy.
What are the copy constructor and the copy assignment operator?
I have already used them above. The copy constructor is called when you type code such as
Car car2 = car1;
Essentially if you declare a variable and assign it in one line, that's when the copy constructor is called. The assignment operator is what happens when you use an equal sign--car2 = car1;
. Noticecar2
isn't declared in the same statement. The two chunks of code you write for these operations are likely very similar. In fact the typical design pattern has another function you call to set everything once you're satisfied the initial copy/assignment is legitimate--if you look at the longhand code I wrote, the functions are nearly identical.When do I need to declare them myself?
If you are not writing code that is to be shared or for production in some manner, you really only need to declare them when you need them. You do need to be aware of what your program language does if you choose to use it 'by accident' and didn't make one--i.e. you get the compiler default. I rarely use copy constructors for instance, but assignment operator overrides are very common. Did you know you can override what addition, subtraction, etc. mean as well?
How can I prevent my objects from being copied?
Override all of the ways you're allowed to allocate memory for your object with a private function is a reasonable start. If you really don't want people copying them, you could make it public and alert the programmer by throwing an exception and also not copying the object.
三法则规定,如果您声明任何一个
,那么您应该声明所有三个。它源于这样一种观察:接管复制操作的含义的需要几乎总是源于执行某种资源管理的类,并且这几乎总是意味着
在一个复制操作中完成任何资源管理可能需要在另一个复制操作中完成,并且
类析构函数也将参与对资源的管理。资源(通常是释放它)。要管理的经典资源是内存,这就是为什么所有标准库类
管理内存(例如,执行动态内存管理的STL容器)都声明“三巨头”:复制操作和析构函数。
三法则的结果是用户声明的析构函数的存在表明简单的成员明智复制不太可能适合类中的复制操作。反过来,这表明如果一个类声明了析构函数,则复制操作可能不应该自动生成,因为它们不会做正确的事情。在采用 C++98 时,这种推理的重要性还没有被充分认识到,因此在 C++98 中,用户声明的析构函数的存在对编译器生成复制操作的意愿没有影响。 C++11 中的情况仍然如此,但这只是因为限制生成复制操作的条件会破坏太多遗留代码。
声明复制构造函数 &复制赋值运算符作为私有访问说明符。
在 C++11 中,您还可以声明复制构造函数 &赋值运算符已删除
The Rule of Three states that if you declare any of a
then you should declare all three. It grew out of the observation that the need to take over the meaning of a copy operation almost always stemmed from the class performing some kind of resource management, and that almost always implied that
whatever resource management was being done in one copy operation probably needed to be done in the other copy operation and
the class destructor would also be participating in management of the resource (usually releasing it). The classic resource to be managed was memory, and this is why all Standard Library classes that
manage memory (e.g., the STL containers that perform dynamic memory management) all declare “the big three”: both copy operations and a destructor.
A consequence of the Rule of Three is that the presence of a user-declared destructor indicates that simple member wise copy is unlikely to be appropriate for the copying operations in the class. That, in turn, suggests that if a class declares a destructor, the copy operations probably shouldn’t be automatically generated, because they wouldn’t do the right thing. At the time C++98 was adopted, the significance of this line of reasoning was not fully appreciated, so in C++98, the existence of a user declared destructor had no impact on compilers’ willingness to generate copy operations. That continues to be the case in C++11, but only because restricting the conditions under which the copy operations are generated would break too much legacy code.
Declare copy constructor & copy assignment operator as private access specifier.
In C++11 onwards you can also declare copy constructor & assignment operator deleted
许多现有的答案已经涉及复制构造函数、赋值运算符和析构函数。
然而,在 C++11 之后,移动语义的引入可能会将其扩展到 3 之外。
最近 Michael Claisse 做了一个涉及这个主题的演讲:
http://channel9.msdn.com/events /CPP/C-PP-Con-2014/The-Canonical-Class
Many of the existing answers already touch the copy constructor, assignment operator and destructor.
However, in post C++11, the introduction of move semantic may expand this beyond 3.
Recently Michael Claisse gave a talk that touches this topic:
http://channel9.msdn.com/events/CPP/C-PP-Con-2014/The-Canonical-Class
C++中的三法则是设计和开发的一个基本原则,规定了三个要求:如果下列成员函数之一有明确的定义,那么程序员应该将另外两个成员函数一起定义。即以下三个成员函数缺一不可:析构函数、复制构造函数、复制赋值运算符。
C++中的复制构造函数是一种特殊的构造函数。它用于构建一个新对象,该新对象相当于现有对象的副本。
复制赋值运算符是一种特殊的赋值运算符,通常用于将一个现有对象指定给其他相同类型的对象。
有一些简单的例子:
Rule of three in C++ is a fundamental principle of the design and the development of three requirements that if there is clear definition in one of the following member function, then the programmer should define the other two members functions together. Namely the following three member functions are indispensable: destructor, copy constructor, copy assignment operator.
Copy constructor in C++ is a special constructor. It is used to build a new object, which is the new object equivalent to a copy of an existing object.
Copy assignment operator is a special assignment operator that is usually used to specify an existing object to others of the same type of object.
There are quick examples: