Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
329 views
in Technique[技术] by (71.8m points)

c++ - 什么是三法则?(What is The Rule of Three?)

  • 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? (如何防止对象被复制?)
  ask by fredoverflow translate from so

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

Introduction (介绍)

C++ treats variables of user-defined types with value semantics . (C ++使用值语义处理用户定义类型的变量。) 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: (让我们考虑一个简单的示例:)

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?
}

(If you are puzzled by the name(name), age(age) part, this is called a member initializer list .) ((如果您对name(name), age(age)部分感到困惑,这称为成员初始化器列表 。))

Special member functions (特殊成员功能)

What does it mean to copy a person object? (复制person对象是什么意思?) The main function shows two distinct copying scenarios. (main功能显示了两种不同的复制方案。) The initialization person b(a); (初始化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 . (分配b = a副本分配运算符执行 。) 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: (从标准引用:)

The [...] copy constructor and copy assignment operator, [...] and destructor are special member functions. (复制构造函数和复制赋值运算符,析构函数是特殊的成员函数。) [ Note : The implementation will implicitly declare these member functions for some class types when the program does not explicitly declare them. ([ 注意如果程序未明确声明它们,则实现将隐式声明某些类类型的这些成员函数。) The implementation will implicitly define them if they are used. (如果使用它们,实现将隐式定义它们。) [...] end note ] [n3126.pdf section 12 §1] ([... 尾注 ] [n3126.pdf第12节§1])

By default, copying an object means copying its members: (默认情况下,复制对象意味着复制其成员:)

The implicitly-defined copy constructor for a non-union class X performs a memberwise copy of its subobjects. (非联合类X的隐式定义的复制构造函数执行其子对象的成员复制。) [n3126.pdf section 12.8 §16] ([n3126.pdf第12.8§16节])

The implicitly-defined copy assignment operator for a non-union class X performs memberwise copy assignment of its subobjects. (非联合类X的隐式定义的副本分配运算符执行其子对象的成员式副本分配。) [n3126.pdf section 12.8 §30] ([n3126.pdf第12.8§30节])

Implicit definitions (隐式定义)

The implicitly-defined special member functions for person look like this: (为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()
{
}

Memberwise copying is exactly what we want in this case: name and age are copied, so we get a self-contained, independent person object. (在这种情况下,按成员复制正是我们想要的:复制nameage ,因此我们得到了一个独立的,独立的person对象。) 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: (在person析构函数完成之后,将隐式调用成员的析构函数:)

After executing the body of the destructor and destroying any automatic objects allocated within the body, a destructor for class X calls the destructors for X's direct [...] members [n3126.pdf 12.4 §6] (在执行析构函数的主体并销毁主体中分配的所有自动对象之后,类X的析构函数调用X的直接成员的析构函数[n3126.pdf 12.4§6])

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++. (让我们回到过去的标准C ++。) There was no such thing as std::string , and programmers were in love with pointers. (没有诸如std::string这样的东西,程序员爱上了指针。) The person class might have looked like this: (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;
    }
};

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! (即使在今天,人们仍然以这种风格编写类并遇到麻烦:“ 我将一个人推到向量中,现在我出现了疯狂的内存错误! ”请记住,默认情况下,复制对象意味着复制其成员,但复制name成员仅复制一个指针, 而不是它指向的字符数组!) This has several unpleasant effects: (这有几个令人不愉快的影响:)

  1. Changes via a can be observed via b . (通过改变a可以通过观察到b 。)
  2. Once b is destroyed, a.name is a dangling pointer. (一旦b被销毁, a.name是一个悬空指针。)
  3. If a is destroyed, deleting the dangling pointer yields undefined behavior . (如果a被销毁,则删除悬空指针会产生未定义的行为 。)
  4. Since the assignment does not take into account what name pointed to before the assignment, sooner or later you will get memory leaks all over the place. (由于分配未考虑分配前所指的name ,因此迟早您会在各处发现内存泄漏。)

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: (由于逐成员复制没有达到预期的效果,因此我们必须显式定义复制构造函数和复制赋值运算符以制作字符数组的深层副本:)

// 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;
}

Note the difference between initialization and assignment: we must tear down the old state before assigning to name to prevent memory leaks. (注意初始化和赋值之间的区别:我们必须在分配给name之前拆除旧状态,以防止内存泄漏。) Also, we have to protect against self-assignment of the form x = x . (同样,我们必须防止x = x形式的自赋值。) Without that check, delete[] name would delete the array containing the source string, because when you write x = x , both this->name and that.name contain the same pointer. (如果没有该检查, delete[] name将删除包含字符串的数组,因为当您编写x = xthis->namethat.name都包含相同的指针。)

Exception safety (异常安全)

Unfortunately, this solution will fail if new char[...] throws an exception due to memory exhaustion. (不幸的是,如果new char[...]由于内存耗尽而引发异常,则该解决方案将失败。) One possible solution is to introduce a local variable and reorder the statements: (一种可能的解决方案是引入局部变量并对语句重新排序:)

// 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;
}

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: (在这种情况下,只需将复制构造函数和复制赋值运算符声明为priv


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...