tag 标签: copy控制

相关博文
  • 热度 7
    2016-4-25 23:28
    1001 次阅读|
    0 个评论
    今天正式进入Part III的学习:Tools for Class Authors Part III的内容都和class有着紧密的关系,它深入拓展了第七章的内容。 如果要使用基本的C++程序,Part I和Part II的内容已经够用了; 如果要更加深入的了解C++,还是要进行Part III的学习。 Part III共有四个章节,首先要学习的是第一个章节:Chapter 13 Copy Control copy constructor copy-assignment operator move constructor move-assignment operator destructor 这五个操作统称为copy control。 一般来说,编译器为我们定义了默认的copy constrol,但某些时候我们需要定义自己的版本。 13.1 Copy, Assign, and Destroy ---------------------------------------------------------------- 1. The Copy Constructor 第一个参数是类的类型的引用,而且其他的参数都为默认值。 比如: class Foo { public:     Foo();     Foo(const Foo); }; 第一个参数一定要是reference,且大多数时候是const的。 如果我们定义了其他的构造函数,那么就一定要显式的把默认构造函数写出来; 如果我们定义了其他的copy构造函数,编译器仍然会为我们生成默认的综合构造函数。 类的成员类会调用自身的copy函数,build-in变量则会直接复制; 类的成员array不能直接复制,需要在copy constuctor里面一个一个的进行操作。 string dots(10, '.');               // direct initialization string s(dots);                     // direct initialization string s2 = dots;                   // copy initialization string null_book = "9-999-99999-9"; // copy initialization string nines = string(100, '9');    // copy initialization direct initialization调用的是构造函数; copy initialization通常调用的是copy构造函数。 当我们使用某对象作为非引用的参数,或者使用某对象作为非引用的返回,或者brace initialize, 都会使用到copy initialization。 库容器的copy、insert、push成员使用了copy initialize, 而emplace则是direct initialize。 如果copy构造函数的参数不是引用的话,那么它本身就需要读取这个参数(而不是直接使用), 而读取并copy的过程又需要copy构造函数,这样就陷入了无穷的循化中。  * copy操作本身经常被使用,而且它的细节处理的内容非常繁琐,在实际中要多调试。 2. The Copy-Assignment Operator 编译器提供了默认的版本,我们可以定义自己的版本。 比如: class Foo { public:     Foo();     Foo(const Foo);     Foo operator=(const Foo); }; 这里的函数名称为operator=,它必须是类的成员函数,返回值绑定到了this指针上, 一般来说,assignment操作符必须返回一个引用作为左操作数。 copy-assignment operator使用同类型的类作为参数。 编译器提供了默认的版本,我们可以提供自定义的版本来禁止assign操作,或者执行其他的操作。 编译器提供了默认的版本,会使用assign操作来复制nonstatic成员变量。 3. The Destructor 析构函数做的是和构造函数相反的事情,释放nonstatic成员以及其他的资源。 比如: class Foo { public:     Foo();     Foo(const Foo);     Foo operator=(const Foo);     ~Foo(); }; 析构函数以~开始,参数表为空,因此它不可被重载。 每个类都只有一个析构函数。 构造函数是先按照类的存储顺序初始化变量,然后执行函数体; 析构函数是先执行函数体,再释放变量,释放的顺序和存储的顺序反向。 析构函数会删除指针类变量,但不会释放指针所指向的存储空间。 析构函数会删除smart指针变量,smart指针会控制资源的释放。 一般来说,凡是对象由于越过了scope而被释放的地方,析构函数会被自动的被调用。 我们不需要定义所有的copy control操作,需要什么定义什么就好, 但是一般来说,如果需要其中一个copy control操作,很可能也需要其他的操作。 如果一个类需要自定义的destructor函数,那它基本上是肯定需要copy构造和copy assign; 如果一个类需要copy构造,那它肯定需要copy assign,反之亦然(但不一样需要析构)。 怎样禁止copy操作? class Foo { public:     Foo() = default;     Foo(const Foo) = delete;     Foo operator=(const Foo) = delete;     ~Foo() = default; }; “= delete”告诉编译器,这个操作是被禁止的。 编译器可能会将默认的综合函数设置成“= delete”的,有以下几种情况:  * 如果类成员有deleted或者不可访问的析构函数,那么这个类的析构函数也是deleted。  * 如果类成员有deleted或者不可访问的copy函数,那么这个类的copy函数也是deleted; 或者析构函数是deleted或者不可访问,也是这样。  * 如果类成员有deleted或者不可访问的copy assign函数,那么类的copy assign为deleted; 或者类成员为const或者引用类型,也是这样。  * 如果类有未in-class初始化的引用成员,或者没有显式构造的const成员,析构函数deleted。 13.2 Copy Control and Resource Management ---------------------------------------------------------------- 当我们设计copy control的时候,要理解我们所设计的功能的实际物理含义是什么。 copy assign的功能设计很关键:  * 自己给自己赋值,也必须正常工作(在函数中保存副本)。  * assign一般会完成析构函数和copy函数的工作。 13.3 Swap ---------------------------------------------------------------- swap由两次copy组成。 除了创建对象的temp来copy之外,我们还可以copy指针。 我们也可以定义自己的swap函数: class Foo { public:     Foo();     friend void swap(Foo, Foo); private:     int i;     string *ps; }; inline void swap(Foo lhs, Foo rhs) {     using std::swap;     swap(lhs.ps, rhs.ps);     swap(lhs.i, lhs.i); } swap函数不是必须的,但是它可以极大的优化需要allocate资源的类。 assignment操作可以使用copy函数实现,也可以使用swap函数实现。 13.4 A Copy-Control Example ---------------------------------------------------------------- 13.5 Classes That Manage Dynamic Memory ---------------------------------------------------------------- 好吧,这两个章节分别设计了自己的example, 实际设计的时候再来参考它们,现在还是继续学习接下来的内容。 13.6 Moving Objects ---------------------------------------------------------------- 有时候,我们并不需要copy,而是需要move; 有时候,类似IO或者unique_ptr的类,不能被copy,只能被move。 库容器、string和shared_ptr类支持move操作,也支持copy操作; IO和unique_ptr只支持move操作。 这里我们引进了一个新的符号: 它表示rvalue references,它只能用来表示即将被释放的对象,这样我们才能move它们。 一般来说,lvalue代表的是对象的identity,rvalue代表的是对象的值。 lvalue是永久的,而rvalue是暂时的。 变量都是lvalues。 因此我们不能将一个rvalue引用绑定到一个rvalue引用上: int rr1 = 42;    // ok: literals are rvalues int rr2 = rr1;   // error: the expression rr1 is an lvalue! int rr3 = std::move(rr1);   // ok 我们可以释放一个moved-from对象,或者重新给它赋值,但不能使用这个moved-from对象的值。 假设有: int f(); vector vi(100); 那么应该: int r1 = f(); int r2 = vi ; int r3 = r1; int r4 = vi * f(); 其实判断的条件很简单,看右侧的返回值是identify还是value。 现在我们可以来设计第一个move函数了: class StrVec {     StrVec(StrVec s) noexcept; private:     string *elements;     string *first_free;     string *cap; }; StrVec::StrVec(StrVec s) noexcept         : elements(s.elements), first_free(s.first_free), cap(s.cap) {     s.elements = s.first_free = s.cap = nullptr; } noexcept是非常关键的,它告诉编译器不要产生异常,因为moved-from对象会被改变。 不论我们有没有定义自己的copy构造和copy-assignment函数,编译器始终会为我们生成它们; 当我们定义了自己的copy构造、copy-assignment或者destructor函数,编译器就不会生成move。 只有当我们没有定义这些copy*函数,并且每个nonstatic成员都可被move,编译器才会生成默认的move。 move从来不会隐式的被定义为deleted。 move不可用的时候,就会使用copy。 因为moved-from对象的状态会被改变,因此调用std::move()函数是一个非常危险的操作。 我们必须保证,我们调用它的时候并没有其他的用户会使用这个对象。 * 此章需加深理解。