本 Wiki 开启了 HTTPS。但由于同 IP 的 Blog 也开启了 HTTPS,因此本站必须要支持 SNI 的浏览器才能浏览。为了兼容一部分浏览器,本站保留了 HTTP 作为兼容。如果您的浏览器支持 SNI,请尽量通过 HTTPS 访问本站,谢谢!
这里会显示出您选择的修订版和当前版本之间的差别。
两侧同时换到之前的修订记录前一修订版后一修订版 | 前一修订版 | ||
cs:programming:cpp:cpp_primer:13_copy_control [2024/01/14 13:46] – 移除 - 外部编辑 (未知日期) 127.0.0.1 | cs:programming:cpp:cpp_primer:13_copy_control [2024/03/13 02:18] (当前版本) – codinghare | ||
---|---|---|---|
行 1: | 行 1: | ||
+ | | ||
+ | C++ Primer 笔记 第十三章\\ | ||
+ | ---- | ||
+ | 当一个类被定义的时候,有几种针对于该类型的操作需要被指定: | ||
+ | * 该类型如何被**拷贝**(// | ||
+ | * 该类型如何被**赋值**(// | ||
+ | * 该类型如何被**移动**(// | ||
+ | * 该类型如何被**销毁**(// | ||
+ | 我们通过如下类型的构造函数来决定这些操作如何进行: | ||
+ | * **拷贝构造函数**(// | ||
+ | * **移动构造函数**(// | ||
+ | * 拷贝赋值**运算符**(// | ||
+ | * 移动赋值**运算符**(// | ||
+ | * 析构函数(// | ||
+ | 定义以上这些操作的过程,被称为**拷贝控制**(// | ||
+ | ====拷贝,赋值与析构==== | ||
+ | ===拷贝构造函数=== | ||
+ | **拷贝构造函数**(// | ||
+ | * 该函数的第一个 parameter 类型是对应 class type 的**引用** | ||
+ | * 其余的 parameter 都有**默认值** | ||
+ | <code cpp> | ||
+ | class Foo { | ||
+ | public: | ||
+ | | ||
+ | | ||
+ | // ... | ||
+ | }; | ||
+ | </ | ||
+ | 为了同时支持常量 / 非常量应用,绝大部分情况下,第一个 parameter 的类型是 '' | ||
+ | <WRAP center round info 100%> | ||
+ | 拷贝构造函数之所以接收 //const classType&// | ||
+ | </ | ||
+ | ==合成拷贝构造函数== | ||
+ | 编译器会在拷贝构造函数没有被显式定义的情况下提供一个**合成拷贝构造函数**(// | ||
+ | * 以成员为单位,以拷贝的方式依次进行初始化 | ||
+ | * 初始化的方式取决于成员类型: | ||
+ | * class type:初始化方式取决于其自身的**拷贝构造函数** | ||
+ | * built-in type:直接拷贝 | ||
+ | * array:按元素进行拷贝 | ||
+ | 以 // | ||
+ | <code cpp> | ||
+ | class Sales_data { | ||
+ | public: | ||
+ | // other members and constructors as before | ||
+ | // declaration equivalent to the synthesized copy constructor | ||
+ | Sales_data(const Sales_data& | ||
+ | private: | ||
+ | std::string bookNo; | ||
+ | int units_sold = 0; | ||
+ | double revenue = 0.0; | ||
+ | }; | ||
+ | // equivalent to the copy constructor that would be synthesized for Sales_data | ||
+ | Sales_data:: | ||
+ | bookNo(orig.bookNo), | ||
+ | units_sold(orig.units_sold), | ||
+ | revenue(orig.revenue) | ||
+ | { } // empty body | ||
+ | </ | ||
+ | ==拷贝初始化== | ||
+ | <color # | ||
+ | * **直接初始化**(// | ||
+ | * **拷贝初始化**(// | ||
+ | <color # | ||
+ | // | ||
+ | \\ \\ <color # | ||
+ | * 使用 '' | ||
+ | * 使用对象本身作为 argument, 传递给一个**非引用**类型的 parameter | ||
+ | * 从某个函数返回**非引用**类型的对象 | ||
+ | * 使用初始化列表初始化数组或者聚合类的成员 | ||
+ | * 某些容器空间分配操作也使用拷贝的方式初始化元素,比如 // | ||
+ | ==拷贝初始化的限制== | ||
+ | 直接初始化与拷贝初始化是否合法是根据构造函数是否是 '' | ||
+ | <code cpp> | ||
+ | vector< | ||
+ | vector< | ||
+ | |||
+ | void f(vector< | ||
+ | f(10); // error: can't use an explicit constructor to copy an argument | ||
+ | f(vector< | ||
+ | </ | ||
+ | |||
+ | ==编译器会绕过拷贝构造函数== | ||
+ | 当然也存在一种情况,编译器会优先选择对对象进行直接初始化,而不是使用拷贝/ | ||
+ | <code cpp> | ||
+ | string null_book = " | ||
+ | </ | ||
+ | 结果被编译器转换为: | ||
+ | <code cpp> | ||
+ | string null_book(" | ||
+ | </ | ||
+ | 不过,即便是编译器绕过了拷贝构造函数,我们也需要保证**拷贝构造函数是存在并可访问的**。 | ||
+ | ===拷贝赋值运算符=== | ||
+ | **拷贝赋值运算符**(// | ||
+ | 需要注意的是: | ||
+ | * 拷贝赋值运算符需要被定义为**成员函数** | ||
+ | * 拷贝赋值运算符作为成员函数时,其左算子(第一个参数)与 //this// 绑定,其右算子需要显式指定,并与运算符所在类的类型一致,比如: | ||
+ | <code cpp> | ||
+ | class Foo { | ||
+ | public: | ||
+ | Foo& operator=(const Foo&); // assignment operator | ||
+ | // ... | ||
+ | }; | ||
+ | </ | ||
+ | <WRAP center round tip 100%> | ||
+ | 为了与默认的赋值运算保持一致,拷贝赋值运算符通常被定义为:接收一个 //const classtype&// | ||
+ | </ | ||
+ | ==合成拷贝赋值运算符== | ||
+ | 合成拷贝赋值运算符的功能与合成拷贝构造函数类似,也是使用 // | ||
+ | <code cpp> | ||
+ | // equivalent to the synthesized copy-assignment operator | ||
+ | Sales_data& | ||
+ | Sales_data:: | ||
+ | { | ||
+ | bookNo = rhs.bookNo; | ||
+ | units_sold = rhs.units_sold; | ||
+ | revenue = rhs.revenue; | ||
+ | return *this; | ||
+ | } | ||
+ | </ | ||
+ | ===析构函数=== | ||
+ | **析构函数**(// | ||
+ | * 析构函数负责释放被对象使用的资源 | ||
+ | * 析构函数负责摧毁对象的非静态成员 | ||
+ | 析构函数没有参数,也不可以被重载。一个类只能有一个析构函数。其写法如下: | ||
+ | <code cpp> | ||
+ | ~DSTR(); | ||
+ | </ | ||
+ | ==析构函数的作用== | ||
+ | 析构函数的作用分两部分诠释: | ||
+ | * **函数体**:首先执行。函数体属于程序最后执行的收尾内容,由类设计者自行设计。通常情况下用于释放动态分配的资源。 | ||
+ | * **析构部分**(// | ||
+ | 析构的部分是**隐式**的进行的。由于 bulit-in type 无需析构,因此该部分主要用于销毁类型为 class type 的成员。销毁的过程通过调用**该成员自身的析构函数**进行。 | ||
+ | <WRAP center round important 100%> | ||
+ | 析构部分不会对**普通指针**类型成员指向的对象进行释放。该部分**必须手动释放**(通常放到函数体中处理)。 | ||
+ | </ | ||
+ | ==析构函数什么时候被调用== | ||
+ | * 变量离开作用域时 | ||
+ | * 对象被销毁会调用析构函数,使得其成员一并被销毁 | ||
+ | * 容器被销毁时会调用析构函数,使其元素一并被销毁 | ||
+ | * 使用 '' | ||
+ | * 创建临时对象的表达式结束时 | ||
+ | 下面是一些详细的例子: | ||
+ | <code cpp> | ||
+ | { // new scope | ||
+ | // p and p2 point to dynamically allocated objects | ||
+ | Sales_data | ||
+ | auto p2 = make_shared< | ||
+ | Sales_data item(*p); | ||
+ | vector< | ||
+ | vec.push_back(*p2); | ||
+ | delete p; // destructor called on the object pointed to by p | ||
+ | } // exit local scope; destructor called on item, p2, and vec | ||
+ | // destroying p2 decrements its use count; if the count goes to 0, the object is freed | ||
+ | // destroying vec destroys the elements in vec | ||
+ | </ | ||
+ | 上述的所有的对象都包含了一个动态分配的 string 成员,但只有 '' | ||
+ | * '' | ||
+ | * '' | ||
+ | <WRAP center round info 100%> | ||
+ | 指针和引用对象离开作用域时不会调用析构函数。 | ||
+ | </ | ||
+ | ==合成析构函数== | ||
+ | 当我们没有自定义析构函数的时候,编译器会提供一个**合成析构函数**(// | ||
+ | <code cpp> | ||
+ | class Sales_data { | ||
+ | public: | ||
+ | // no work to do other than destroying the members, which happens automatically | ||
+ | ~Sales_data() { } | ||
+ | // other members as before | ||
+ | }; | ||
+ | </ | ||
+ | 由于函数体为空,合成析构函数只会执行析构的部分,也就是调用类成员自身的析构函数对其销毁并释放。 | ||
+ | <WRAP center round help 100%> | ||
+ | 函数体中的内容与析构的部分是分开的,属于额外的操作。析构的部分通过 member-wise 的方式来执行成员的销毁与释放。 | ||
+ | </ | ||
+ | ===三 / 五规则=== | ||
+ | **三规则**(// | ||
+ | * 考虑某个类是否需要 copy-control 成员的时候,需要首先考虑该类是否需要析构函数 | ||
+ | * 如果某个类需要拷贝构造函数,绝大部分情况下该类也需要拷贝赋值运算符 | ||
+ | 该规则在 C++ 11 中被延伸到了 “五”,因为移动构造函数与移动赋值运算符也加入到了 copy-control 成员中。 | ||
+ | ==需要析构函数的类也需要拷贝和赋值== | ||
+ | 通常情况下,类对析构函数的要求更明显。如果析构函数是必要的,那么拷贝构造函数与拷贝赋值运算符也是必要的。\\ \\ | ||
+ | 比如我们之前的习题例子 // | ||
+ | <code cpp> | ||
+ | class HasPtr { | ||
+ | public: | ||
+ | HasPtr(const std::string &s = std:: | ||
+ | ps(new std:: | ||
+ | ~HasPtr() { delete ps; } | ||
+ | // WRONG: HasPtr needs a copy constructor and copy-assignment operator | ||
+ | // other members as before | ||
+ | }; | ||
+ | </ | ||
+ | <color # | ||
+ | <code cpp> | ||
+ | HasPtr f(HasPtr hp) // HasPtr passed by value, so it is copied | ||
+ | { | ||
+ | HasPtr ret = hp; // copies the given HasPtr | ||
+ | // process ret | ||
+ | return ret; // ret and hp are destroyed | ||
+ | } | ||
+ | </ | ||
+ | 很明显,合成构造函数并不能处理拷贝初始化的需求。由于 '' | ||
+ | 不仅如此,即便不考虑 '' | ||
+ | <code cpp> | ||
+ | HasPtr p(" | ||
+ | f(p); // when f completes, the memory to which p.ps points is freed | ||
+ | HasPtr q(p); // now both p and q point to invalid memory! | ||
+ | </ | ||
+ | |||
+ | ==需要定义拷贝的类也需要赋值,反过来也一样== | ||
+ | 一个例子:假设我们有个产品类;其他的成员都相同,唯独序列号不同。可知: | ||
+ | * 独特的序列号需要通过自定义的拷贝构造函数生成 | ||
+ | * 拷贝构造的过程中需要自定义拷贝赋值运算符避免拷贝到独特的序列号 | ||
+ | 可见拷贝构造函数与拷贝赋值运算符是一定会配套出现的。 | ||
+ | <WRAP center round tip 100%> | ||
+ | 需要拷贝构造函数与拷贝赋值运算符的地方不一定需要析构函数。 | ||
+ | </ | ||
+ | ===使用 = default=== | ||
+ | C++ 11 中允许我们使用 '' | ||
+ | <code cpp> | ||
+ | // copy control; use defaults | ||
+ | Sales_data() = default; | ||
+ | Sales_data(const Sales_data& | ||
+ | Sales_data& | ||
+ | ~Sales_data() = default; | ||
+ | </ | ||
+ | 需要注意的是,在类中使用 '' | ||
+ | <WRAP center round info 100%> | ||
+ | //= default// 只能应用于有合成版本的成员函数。 | ||
+ | </ | ||
+ | ===阻止拷贝=== | ||
+ | 大多数类都(显式或隐式)的定义了拷贝三大件。但有些情况下也需要禁止其使用,比如某些无法被拷贝的类( '' | ||
+ | ==将函数定义为 delete== | ||
+ | C++11 中提供了一种函数的种类:**被删除的函数**(// | ||
+ | <code cpp> | ||
+ | truct NoCopy { | ||
+ | NoCopy() = default; | ||
+ | NoCopy(const NoCopy& | ||
+ | NoCopy & | ||
+ | ~NoCopy() = default; | ||
+ | // other members | ||
+ | }; | ||
+ | </ | ||
+ | == = deleted 与 = default 的区别== | ||
+ | * '' | ||
+ | * '' | ||
+ | ==析构函数不能是被删除函数== | ||
+ | 析构函数不能是被删除函数。如果析构函数被删除,那么 | ||
+ | * 对应的类对象将无法被删除 | ||
+ | * 该类型的变量或者临时对象将无法被定义: | ||
+ | * 成员中拥有被删除的析构函数也将导致该结果 | ||
+ | * 且成员自身无法被销毁会导致该类型对象无法被销毁 | ||
+ | 需要注意的是,动态分配对象的创建不受被删除的析构函数的限制,但是释放该对象受限制: | ||
+ | <code cpp> | ||
+ | struct NoDtor { | ||
+ | NoDtor() = default; | ||
+ | ~NoDtor() = delete; | ||
+ | }; | ||
+ | NoDtor nd; // error: NoDtor destructor is deleted | ||
+ | NoDtor *p = new NoDtor(); | ||
+ | delete p; // error: NoDtor destructor is deleted | ||
+ | </ | ||
+ | ==合成拷贝控制成员可能是被删除的== | ||
+ | 在如下情形中,合成拷贝控制成员会被定义为被删除的函数: | ||
+ | * 如果类成员的**析构函数**是被删除的(不可访问的),则类自身的**合成析构函数**是被删除的 | ||
+ | * 如果类成员的**拷贝构造函数或析构函数**是被删除的(不可访问的),则类自身的**合成拷贝构造函数**是被删除的 | ||
+ | * 如果类成员的**拷贝赋值运算符**是被删除的(不可访问的),或类中有 '' | ||
+ | * 如果类成员的**析构函数**是被删除的(不可访问的),或者 | ||
+ | * 拥有不能进行类内初始化的引用成员 | ||
+ | * 拥有 const 成员,该成员无法进行类内初始化,且未显式的定义默认构造函数 | ||
+ | 那么该类的**默认构造函数**是被删除的。\\ \\ | ||
+ | 这样做是基于逻辑上的原因: | ||
+ | * 被删除的析构函数会导致拷贝三大件被删除,这是为了防止被创建的对象无法被删除 | ||
+ | * 引用成员和 const 成员无法被默认初始化,隐含的意思则是默认构造函数无法使用 | ||
+ | * 类中有 const 成员代表该类无法被赋值,因此拷贝赋值运算符无法使用 | ||
+ | * 对于带有引用成员的类,将右边的引用赋值给左边的引用成员的结果是,该成员引用的对象永远不变。这与我们期望的拷贝赋值运算的结果不符,因此将拷贝赋值运算符定义为被删除。 | ||
+ | <WRAP center round info 100%> | ||
+ | 总的来说,如果某个类的类成员不能被**默认初始化,拷贝,赋值,销毁**,则其**对应类的拷贝控制成员则为被删除的**。 | ||
+ | </ | ||
+ | ==private 与拷贝控制== | ||
+ | 在 '' | ||
+ | <code cpp> | ||
+ | class PrivateCopy { | ||
+ | // no access specifier; following members are private by default; see § 7.2 (p. 268) | ||
+ | // copy control is private and so is inaccessible to ordinary user code | ||
+ | PrivateCopy(const PrivateCopy& | ||
+ | PrivateCopy & | ||
+ | // other members | ||
+ | public: | ||
+ | PrivateCopy() = default; // use the synthesized default constructor | ||
+ | ~PrivateCopy(); | ||
+ | }; | ||
+ | </ | ||
+ | 这个结构中: | ||
+ | * 析构函数是公有成员,因此可以正常创建类对象 | ||
+ | * 其他的拷贝控制成员是私有的,因此不能进行类拷贝赋值操作 | ||
+ | * 只声明而不定义这些拷贝控制成员,是为了防止友元函数以及类成员对其的访问 | ||
+ | 这种实现方式中,声明但不定义某个成员是合法的。这样的声明会有以下的效果: | ||
+ | * 任何对未定义成员的访问都会导致链接期的错误(防止成员函数以及友元函数的访问) | ||
+ | * 任何对类的拷贝与赋值都会导致编译器的错误 | ||
+ | <WRAP center round tip 100%> | ||
+ | 推荐使用新标准 //= delete// 来完成该类需求。 | ||
+ | </ | ||
+ | ====拷贝控制与资源管理==== | ||
+ | 某些获取了动态资源的类(通常带指针)需要通过析构函数来进行资源释放。根据三/ | ||
+ | * 类的行为像一个值,即类拷贝的数据与原来的类**相互独立**,比如标准库 '' | ||
+ | * 类的行为像一个指针,即类拷贝的数据与原来的类**共享**,比如 shared_ptr | ||
+ | <WRAP center round info 100%> | ||
+ | 某些不能拷贝的类,比如 // | ||
+ | </ | ||
+ | ===行为像值的类(值类)=== | ||
+ | 当一个类的行为与值相同时,其拷贝控制成员需要保证每个类对象都有一份独立的资源。比如之前提到 '' | ||
+ | * 其拷贝构造函数需要拷贝整个 string,而不仅仅是指针 | ||
+ | * 其拷贝赋值运算符需要将新的 string 拷贝进来,因此需要首先释放旧的内容,再添加新的内容。 | ||
+ | * 由于涉及动态资源,析构函数中需要定义相关的释放操作。 | ||
+ | |||
+ | < | ||
+ | 实现如下: | ||
+ | <code cpp> | ||
+ | class HasPtr { | ||
+ | public: | ||
+ | HasPtr(const std::string &s = std:: | ||
+ | ps(new std:: | ||
+ | // each HasPtr has its own copy of the string to which ps points | ||
+ | HasPtr(const HasPtr &p): | ||
+ | ps(new std:: | ||
+ | HasPtr& operator=(const HasPtr &); | ||
+ | ~HasPtr() { delete ps; } | ||
+ | private: | ||
+ | std::string *ps; | ||
+ | int i; | ||
+ | }; | ||
+ | </ | ||
+ | ==值类的拷贝赋值运算符== | ||
+ | 值类中的拷贝赋值运算符的功能是将 '' | ||
+ | - 将左边对象中的资源释放掉(也就是清空) | ||
+ | - 申请新的资源,并将右边对象中的资源拷贝到左边对象中 | ||
+ | 鉴于这样的运行顺序,我们可以写出一个实现版本: | ||
+ | <code cpp> | ||
+ | HasPtr& | ||
+ | HasPtr:: | ||
+ | { | ||
+ | delete ps; // frees the string to which this object points | ||
+ | // if rhs and *this are the same object, we're copying from deleted memory! | ||
+ | ps = new string(*(rhs.ps)); | ||
+ | i = rhs.i; | ||
+ | return *this; | ||
+ | } | ||
+ | </ | ||
+ | 但这样的实现有一个很严重的问题。如果我们使用该运算符进行对象的**自我赋值**的话: | ||
+ | * '' | ||
+ | * 由于是自我赋值,'' | ||
+ | * 之后再使用 '' | ||
+ | 因此,我们会得到一个**未定义**的结果。\\ \\ | ||
+ | 解决这个问题的办法是,**先分配空间进行拷贝,再释放掉原有的空间:** | ||
+ | <code cpp> | ||
+ | HasPtr& HasPtr:: | ||
+ | { | ||
+ | auto newp = new string(*rhs.ps); | ||
+ | delete ps; // free the old memory | ||
+ | ps = newp; // copy data from rhs into this object | ||
+ | i = rhs.i; | ||
+ | return *this; | ||
+ | } | ||
+ | </ | ||
+ | 上述代码可以很好的处理自我赋值的问题。我们通过创建新的空间进行拷贝,避免了释放原有空间导致指针失效的问题。\\ \\ | ||
+ | 可以看出来的是,设计拷贝赋值运算符有几个要点: | ||
+ | * 拷贝赋值运算符需要考虑**自我赋值**的情况 | ||
+ | * 拷贝赋值运算符通常是**析构函数**与**拷贝构造函数**的结合体。 | ||
+ | * 实现拷贝赋值运算符的时候,**首先**要考虑的是将**需要拷贝的内容存储到新的空间**中。 | ||
+ | ===行为像指针的类(指针类)=== | ||
+ | 与值类不同,指针类的拷贝意味着**共享**;因此,拷贝成员的操作对象应该是指向资源的**指针**,而不是资源本身。当前最好的办法是使用 shared_ptr 代为管理资源,拷贝工作只要基于 shared_ptr 实现即可。如果需要直接管理,那么保险的解决方案是仿照 shared_ptr 的原理来进行资源管理,也就是说,使用**引用计数**。 | ||
+ | ==引入引用计数== | ||
+ | 根据 shared_ptr 的原理,我们应该对类中的成员做如下设计: | ||
+ | * 所有普通的构造函数的默认计数都是 '' | ||
+ | * 拷贝构造函数会使默认计数**加** '' | ||
+ | * 析构函数会使默认计数**减** '' | ||
+ | * 拷贝赋值运算符: | ||
+ | * 由于是将右边的对象拷贝到左边,因此右边的对象需要加 '' | ||
+ | * 当左边的对象计数为 '' | ||
+ | ==引用计数实现方式== | ||
+ | 由于引用计数需要在所有的拷贝中共享,因此引用计数的存储需要放置到**动态内存**中。引用计数的分配与创建对象同时进行,而拷贝的时候只需要拷贝引用计数的指针即可实现当前计数的共享。 | ||
+ | ==指针类的实现== | ||
+ | 还是以之前的 '' | ||
+ | <code cpp> | ||
+ | class HasPtr { | ||
+ | public: | ||
+ | // constructor allocates a new string and a new counter, which it sets to 1 | ||
+ | HasPtr(const std::string &s = std:: | ||
+ | ps(new std:: | ||
+ | // copy constructor copies all three data members and increments the counter | ||
+ | HasPtr(const HasPtr &p): | ||
+ | ps(p.ps), i(p.i), use(p.use) { ++*use; } | ||
+ | HasPtr& operator=(const HasPtr& | ||
+ | ~HasPtr(); | ||
+ | private: | ||
+ | std::string *ps; | ||
+ | int i; | ||
+ | std::size_t *use; // member to keep track of how many objects share *ps | ||
+ | }; | ||
+ | </ | ||
+ | 这里 '' | ||
+ | 然后是析构函数;由于采用了计数的设计,因此析构函数需要在当前计数为 '' | ||
+ | <code cpp> | ||
+ | HasPtr:: | ||
+ | { | ||
+ | if (--*use == 0) { // if the reference count goes to 0 | ||
+ | delete ps; // delete the string | ||
+ | delete use; // and the counter | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | 接下来是拷贝赋值运算符。根据之前的设计,拷贝赋值运算符需要同时对右边的对象进行计数自增,并且对左边的对象进行计数自减,并在左边对象计数为 '' | ||
+ | <code cpp> | ||
+ | HasPtr& HasPtr:: | ||
+ | { | ||
+ | ++*rhs.use; | ||
+ | if (--*use == 0) { // then decrement this object' | ||
+ | delete ps; // if no other users | ||
+ | delete use; // free this object' | ||
+ | } | ||
+ | ps = rhs.ps; | ||
+ | i = rhs.i; | ||
+ | use = rhs.use; | ||
+ | return *this; | ||
+ | } | ||
+ | </ | ||
+ | ====Swap==== | ||
+ | 如果类会使用到包含排序的算法,那么定义一个**交换函数** //swap()// 是非常重要的(比如快速排序就会用到交换的手法)。 | ||
+ | <WRAP center round tip 100%> | ||
+ | 标准库中已经定义了 // | ||
+ | </ | ||
+ | 默认情况下,// | ||
+ | <code cpp> | ||
+ | HasPtr temp = v1; // make a temporary copy of the value of v1 | ||
+ | v1 = v2; // assign the value of v2 to v1 | ||
+ | v2 = temp; // assign the saved value of v1 to v2 | ||
+ | </ | ||
+ | 不过,这样做开销很大。上例中, | ||
+ | <code cpp> | ||
+ | string *temp = v1.ps; // make a temporary copy of the pointer in v1.ps | ||
+ | v1.ps = v2.ps; | ||
+ | v2.ps = temp; // assign the saved pointer in v1.ps to v2.ps | ||
+ | </ | ||
+ | ===外部 swap 的实现=== | ||
+ | 以 //HasPtr// 类为例子,自定义的 //swap()// 的实现如下: | ||
+ | <code cpp> | ||
+ | class HasPtr { | ||
+ | friend void swap(HasPtr&, | ||
+ | // other members as in § 13.2.1 (p. 511) | ||
+ | }; | ||
+ | inline | ||
+ | void swap(HasPtr &lhs, HasPtr &rhs) | ||
+ | { | ||
+ | using std::swap; | ||
+ | swap(lhs.ps, | ||
+ | swap(lhs.i, rhs.i); | ||
+ | } | ||
+ | </ | ||
+ | 有两点要注意: | ||
+ | * 由于需要交换私有成员,因此要将 //swap()// 声明为友元函数 | ||
+ | * //swap()// 在此的作用是优化代码,因此声明为 '' | ||
+ | ===优先调用类成员 swap === | ||
+ | 上面的例子中,我们在外部的 //swap()// 中调用了另外的 //swap()// 实现了交换。但需要注意的是,外部 //swap()// 函数中调用的 //swap()// 实际上是 '' | ||
+ | 如果我们在类中实现了针对类成员类型的 // | ||
+ | 下面是一个比较好的写法: | ||
+ | <code cpp> | ||
+ | void swap(Foo &lhs, Foo &rhs) | ||
+ | { | ||
+ | using std::swap; | ||
+ | swap(lhs.h, rhs.h); // uses the HasPtr version of swap | ||
+ | // swap other members of type Foo | ||
+ | } | ||
+ | </ | ||
+ | 这种写法下,如果类中存在着更匹配的 '' | ||
+ | <WRAP center round info 100%> | ||
+ | 上面的例子中,// | ||
+ | </ | ||
+ | ===赋值运算符 与 swap== | ||
+ | 赋值运算符有另外一个版本:该版本结合 swap 来定义赋值。这种技术被称为 //Copy and Swap// | ||
+ | <code cpp> | ||
+ | // note rhs is passed by value, which means the HasPtr copy constructor | ||
+ | // copies the string in the right-hand operand into rhs | ||
+ | HasPtr& HasPtr:: | ||
+ | { | ||
+ | // swap the contents of the left-hand operand with the local variable rhs | ||
+ | swap(*this, rhs); // rhs now points to the memory this object had used | ||
+ | return *this; | ||
+ | } | ||
+ | </ | ||
+ | 这个版本的赋值运算符的实现逻辑如下: | ||
+ | * 首先以拷贝的形式传递对象 '' | ||
+ | * 其次我们使用 swap 对左边的对象(也就是 '' | ||
+ | * 当 swap 完成之后,由于 '' | ||
+ | * 释放完毕之后,赋值操作完成。 | ||
+ | < | ||
+ | 这种实现实际上也能处理自我赋值:对象的自我赋值实际上实在与自己的拷贝做交换。 | ||
+ | ====实例:拷贝控制类==== | ||
+ | ===实例需求分析=== | ||
+ | 本实例需要实现邮件投递的功能。这种投递的关系是多对多的,比如一个文件夹可以有多封**不同的**邮件,而一封邮件可以投递到多个**不同的**文件夹中。下面是一个简单的两封邮件与两个文件夹多对多对应的关系图: | ||
+ | \\ \\ < | ||
+ | 清楚结构以后,我们还需要对整个系统设计一些功能: | ||
+ | * 邮件存储 / 删除功能:将邮件存储到目标文进夹中,或是从目标文件夹中删除 | ||
+ | * 邮件与文件夹的拷贝功能 | ||
+ | * 邮件与文件夹的赋值功能:赋值后左边的邮件/ | ||
+ | * 邮件之间的交换功能 | ||
+ | 上述的需求可以简单的分析一下: | ||
+ | * 首先,投递的关系可以用**指针与 //set//** 的组合来实现。如果我们将指向的关系视作为邮件与文件夹之间的位置关系,那么邮件在文件夹中的位置(或是文件夹中有哪些邮件)则可以使用**元素为指针的 set** 来实现。可见的是,邮件与文件夹都需要有自己的 set 来维护相互的关系。 | ||
+ | * 其次,邮件的存储/ | ||
+ | * 再次,针对类中的拷贝成员,由于拷贝 / 析构意味着建立 / 释放了一个对象,该对象在整个邮件和文件夹的关系网中的位置信息应该及时的**更新**。因此,拷贝成员需要附带更新对应对象位置的操作。 | ||
+ | * 最后,针对交换功能的位置更新,根据 copy-swap idiom 的流程,交换功能需要预先清理旧的关系网,再使用新对象中的关系信息建立新的关系网。 | ||
+ | ===Message 类的实现=== | ||
+ | //Message// 类用于实现**邮件**。大致的实现如下: | ||
+ | <code cpp> | ||
+ | class Message { | ||
+ | friend class Folder; | ||
+ | public: | ||
+ | // folders is implicitly initialized to the empty set | ||
+ | explicit Message(const std::string &str = "" | ||
+ | contents(str) { } | ||
+ | // copy control to manage pointers to this Message | ||
+ | Message(const Message& | ||
+ | Message& | ||
+ | ~Message(); | ||
+ | // add/remove this Message from the specified Folder' | ||
+ | void save(Folder& | ||
+ | void remove(Folder& | ||
+ | private: | ||
+ | std::string contents; | ||
+ | std:: | ||
+ | // utility functions used by copy constructor, | ||
+ | // add this Message to the Folders that point to the parameter | ||
+ | void add_to_Folders(const Message& | ||
+ | // remove this Message from every Folder in folders | ||
+ | void remove_from_Folders(); | ||
+ | }; | ||
+ | </ | ||
+ | //Message// 中包含了两个成员: | ||
+ | * '' | ||
+ | * '' | ||
+ | == save / remove 成员函数== | ||
+ | '' | ||
+ | \\ \\ < | ||
+ | 假设文件夹类 '' | ||
+ | <code cpp> | ||
+ | void Message:: | ||
+ | { | ||
+ | folders.insert(& | ||
+ | f.msgs.insert(this); | ||
+ | } | ||
+ | void Message:: | ||
+ | { | ||
+ | folders.erase(& | ||
+ | f.msg.earse(this); | ||
+ | } | ||
+ | </ | ||
+ | 上述的过程可以通过定义私有成员 // | ||
+ | <code cpp> | ||
+ | /* In Message class*/ | ||
+ | void addFdr(Folder *f) { folder.insert(f); | ||
+ | void remFdr(Folder *f) { folder.insert(f); | ||
+ | /* In Folder class */ | ||
+ | void addMsg(Message *m) { msgs.insert(m); | ||
+ | void remMsg(Message *m) { msgs.insert(m); | ||
+ | </ | ||
+ | 虽然 //save()// 与 // | ||
+ | ==方便实现拷贝控制的函数== | ||
+ | 根据之前的分析,所有的拷贝控制成员都需要**批量的**的更新某个添加或者移除的类对象。比如我拷贝了一份邮件,那么被拷贝的邮件之前在哪些文件夹,该邮件也应该在;文件夹同理:拷贝后的文件夹应该拥有拷贝前的文件夹一样的文件。因此,设计批量建立关系的成员函数可以让关系更新工作更加方便。\\ \\ | ||
+ | 以 //Message// 类为例。之间我们为单个邮件关联到目标文件夹的功能设计了私有函数 // | ||
+ | * 遍历当前 //Message// 对象中的 folders 成员,获得邮件所在的(当前)文件夹的指针 | ||
+ | * 使用该指针访问 // | ||
+ | < | ||
+ | 实现如下: | ||
+ | <code cpp> | ||
+ | // add this Message to Folders that point to m | ||
+ | void Message:: | ||
+ | { | ||
+ | for (auto f : m.folders) // for each Folder that holds m | ||
+ | f-> | ||
+ | } | ||
+ | </ | ||
+ | 同理,批量删除邮件关系的成员函数也可以以相同的方式实现: | ||
+ | <code cpp> | ||
+ | void Message:: | ||
+ | { | ||
+ | for (auto f : folders) // for each pointer in folders | ||
+ | f-> | ||
+ | folders.clear(); | ||
+ | } | ||
+ | </ | ||
+ | <WRAP center round box 100%> | ||
+ | 需要注意上述最后一行,对 '' | ||
+ | </ | ||
+ | ==Message 类的拷贝构造函数== | ||
+ | 通过之前的分析,Message 类的拷贝构造函数只需要做两件事: | ||
+ | * 拷贝邮件的内容,以及之前邮件所在文件夹的信息 | ||
+ | * 将当前邮件作为新的邮件,注册到 '' | ||
+ | 这里我们使用之前定义的 // | ||
+ | <code cpp> | ||
+ | Message:: | ||
+ | contents(m.contents), | ||
+ | { | ||
+ | add_to_Folders(m); | ||
+ | } | ||
+ | </ | ||
+ | ==Message 类的析构函数== | ||
+ | 析构函数 // | ||
+ | <code cpp> | ||
+ | Message:: | ||
+ | { | ||
+ | remove_from_Folders(); | ||
+ | } | ||
+ | </ | ||
+ | ==Message 类的拷贝赋值运算符== | ||
+ | 由于 Message 中,类的成员都是自动存储对象,因此无需像在堆上的对象一样申请一块临时空间进行先行存放。但拷贝赋值的过程中,我们需要对当前的对象进行类似的关系更新。按照 copy-assignment 的 idiom,应该先将旧的邮件从各个文件夹中删除,然后使用新的邮件内容对其进行覆盖,最后在注册新的邮件到各个文件夹: | ||
+ | <code cpp> | ||
+ | Message& | ||
+ | { | ||
+ | // handle self-assignment by removing pointers before inserting them | ||
+ | remove_from_Folders(); | ||
+ | contents = rhs.contents; | ||
+ | folders = rhs.folders; | ||
+ | add_to_Folders(rhs); | ||
+ | return *this; | ||
+ | } | ||
+ | </ | ||
+ | 需要注意的是,书上声称这样的顺序解决了自我赋值的问题,但实际上,由于 '' | ||
+ | 比较好的解决方法是**直接判断当前是否存在自我赋值**,如果是的话则直接返回当前对象: | ||
+ | <code cpp> | ||
+ | Message& | ||
+ | { | ||
+ | // handle self-assignment | ||
+ | if(this == & | ||
+ | return *this; | ||
+ | | ||
+ | remove_from_Folders(); | ||
+ | contents = rhs.contents; | ||
+ | folders = rhs.folders; | ||
+ | add_to_Folders(rhs); | ||
+ | return *this; | ||
+ | } | ||
+ | </ | ||
+ | ==用于 Message 的 swap()== | ||
+ | 根据之前的分析,// | ||
+ | * 拷贝一个 //Message// 类对象的**副本**作为交换对象 | ||
+ | * 将被交换对象与交换对象在文件夹中的信息全部清空 | ||
+ | * 交换当前对象的内容与其对应的文件夹信息 | ||
+ | * 根据交换后的文件夹信息更新各自在文件夹中的位置 | ||
+ | 也就是说,**交换邮件,不但交换了邮件的内容,也交换了邮件所处的文件夹位置**: | ||
+ | <code cpp> | ||
+ | void swap(Message &lhs, Message &rhs) | ||
+ | { | ||
+ | using std::swap; // not strictly needed in this case, but good habit | ||
+ | // remove pointers to each Message from their (original) respective Folders | ||
+ | for (auto f: lhs.folders) | ||
+ | f-> | ||
+ | for (auto f: rhs.folders) | ||
+ | f-> | ||
+ | // swap the contents and Folder pointer sets | ||
+ | swap(lhs.folders, | ||
+ | swap(lhs.contents, | ||
+ | // add pointers to each Message to their (new) respective Folders | ||
+ | for (auto f: lhs.folders) | ||
+ | f-> | ||
+ | for (auto f: rhs.folders) | ||
+ | f-> | ||
+ | } | ||
+ | </ | ||
+ | 除此之外,// | ||
+ | ==Folder 类的一些延伸== | ||
+ | //Folder// 类实际上与 //Message// 类基本上相同,唯一的不同有两点: | ||
+ | * //Folder// 类不需要存储内容 | ||
+ | * //Folder// 类不需要实现存储与删除功能(逻辑上是从文件夹中删除文件,而不是反着来) | ||
+ | ====实例:动态内存管理类==== | ||
+ | 本节的实例的内容是实现一个元素为 string 的 vector: //StrVec// 类。该类与 vector 的功能基本一致。大致的设计如下: | ||
+ | * 数据成员: | ||
+ | * 指针 '' | ||
+ | * 指针 '' | ||
+ | * 指针 '' | ||
+ | * // | ||
+ | {{ : | ||
+ | |||
+ | * 私有功能函数: | ||
+ | * '' | ||
+ | * '' | ||
+ | * '' | ||
+ | 除此之外还会一些常用的功能函数。 | ||
+ | ===StrVec 类的定义=== | ||
+ | 一个简单的 //StrVec// 类的定义如下: | ||
+ | <code cpp> | ||
+ | // simplified implementation of the memory allocation strategy for a vector-like class | ||
+ | class StrVec { | ||
+ | |||
+ | public: | ||
+ | |||
+ | StrVec(): // the allocator member is default initialized | ||
+ | elements(nullptr), | ||
+ | StrVec(const StrVec& | ||
+ | StrVec & | ||
+ | ~StrVec(); | ||
+ | void push_back(const std:: | ||
+ | size_t size() const { return first_free - elements; } | ||
+ | size_t capacity() const { return cap - elements; } | ||
+ | std::string *begin() const { return elements; } | ||
+ | std::string *end() const { return first_free; } | ||
+ | // ... | ||
+ | private: | ||
+ | |||
+ | static std:: | ||
+ | void chk_n_alloc() | ||
+ | { if (size() == capacity()) reallocate(); | ||
+ | |||
+ | // utilities used by the copy constructor, | ||
+ | std:: | ||
+ | (const std:: | ||
+ | void free(); | ||
+ | void reallocate(); | ||
+ | std::string *elements; | ||
+ | std::string *first_free; | ||
+ | std::string *cap; // pointer to one past the end of the array | ||
+ | |||
+ | }; | ||
+ | |||
+ | // alloc must be defined in the StrVec implmentation file | ||
+ | allocator< | ||
+ | </ | ||
+ | ===拷贝控制成员的设计=== | ||
+ | ==alloc_n_copy() 私有函数== | ||
+ | // | ||
+ | - 计算被拷贝内容需要多少空间,并分配足够的空间 | ||
+ | - 将被拷贝的内容复制到新分配的空间中,并返回该内容的头指针与 off-the-end 指针 | ||
+ | 由于我们需要同时返回两个指针,因此需要使用 '' | ||
+ | <code cpp> | ||
+ | pair< | ||
+ | StrVec:: | ||
+ | { | ||
+ | // allocate space to hold as many elements as are in the range | ||
+ | auto data = alloc.allocate(e - b); | ||
+ | // initialize and return a pair constructed from data and | ||
+ | // the value returned by uninitialized_copy | ||
+ | return {data, uninitialized_copy(b, | ||
+ | } | ||
+ | </ | ||
+ | 两个要点: | ||
+ | * '' | ||
+ | * '' | ||
+ | ==StrVec 拷贝构造函数== | ||
+ | 拷贝构造函数的逻辑与 '' | ||
+ | <code cpp> | ||
+ | StrVec:: | ||
+ | { | ||
+ | // call alloc_n_copy to allocate exactly as many elements as in s | ||
+ | auto newdata = alloc_n_copy(s.begin(), | ||
+ | elements = newdata.first; | ||
+ | first_free = cap = newdata.second; | ||
+ | } | ||
+ | </ | ||
+ | ==free() 私有函数== | ||
+ | //free()// 函数用于清理 '' | ||
+ | - 将 //StrVec// 中已有的成员依次摧毁 | ||
+ | - 将 '' | ||
+ | 需要注意的是,销毁的顺序与构造的顺序是相反的;因此需要从 off-the-end 的位置开始销毁,到头元素的位置结束。实现代码: | ||
+ | <code cpp> | ||
+ | void StrVec:: | ||
+ | { | ||
+ | // may not pass deallocate a 0 pointer; if elements is 0, there' | ||
+ | if (elements) { | ||
+ | // destroy the old elements in reverse order | ||
+ | for (auto p = first_free; p != elements; /* empty */) | ||
+ | alloc.destroy(--p); | ||
+ | alloc.deallocate(elements, | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | 如果使用算法 '' | ||
+ | <code cpp> | ||
+ | for_each(element, | ||
+ | [](cosnt string &s) | ||
+ | { alloc.destroy(& | ||
+ | </ | ||
+ | |||
+ | 两个要点: | ||
+ | * 由于从 off-the-end 指针开始,因此需要使用 '' | ||
+ | * deallocate 必须使用之前 allocate **使用过的指针** 进行内存空间的释放;因此要求检测参数指针**是否为空**。 | ||
+ | ==StrVec 析构函数== | ||
+ | 析构函数只需要对 '' | ||
+ | <code cpp> | ||
+ | StrVec:: | ||
+ | </ | ||
+ | ==StrVec 拷贝赋值运算符== | ||
+ | 根据拷贝赋值运算 的 idiom,可知拷贝赋值运算符通过组合使用 '' | ||
+ | * '' | ||
+ | * '' | ||
+ | 实现如下: | ||
+ | <code cpp> | ||
+ | StrVec & | ||
+ | { | ||
+ | if(this == &rhs) | ||
+ | return *this; | ||
+ | // call alloc_n_copy to allocate exactly as many elements as in rhs | ||
+ | auto data = alloc_n_copy(rhs.begin(), | ||
+ | free(); | ||
+ | | ||
+ | elements = data.first; | ||
+ | first_free = cap = data.second; | ||
+ | return *this; | ||
+ | } | ||
+ | </ | ||
+ | 当然,只要分配了新的空间,指针是一定要更新的。同时,自我赋值的检测也不能忘记。 | ||
+ | ===Reallocate() 函数 与 Move=== | ||
+ | // | ||
+ | 我们可以通过 “移动” 而不是拷贝的方式来优化该过程的性能。C++11 提供了**移动构造函数**(// | ||
+ | <WRAP center round tip 100%> | ||
+ | 对于 string, 这里的 “移动” 可以想象成使用拷贝 //const char*// 取代直接拷贝 string 的过程。 | ||
+ | </ | ||
+ | 需要注意的是: | ||
+ | * 普通的移动构造函数**只会**对 '' | ||
+ | * 使用 '' | ||
+ | ==reallocate 私有函数== | ||
+ | // | ||
+ | - 检查当前 //StrVec// 的对象大小,如果为 '' | ||
+ | - 按照新的 capacity 分配空间 | ||
+ | - 以旧(左手边)的 | ||
+ | - 释放旧对象 | ||
+ | - 更新指针到新的空间 | ||
+ | < | ||
+ | 实现如下: | ||
+ | <code cpp> | ||
+ | void StrVec:: | ||
+ | |||
+ | { | ||
+ | // we'll allocate space for twice as many elements as the current size | ||
+ | auto newcapacity = size() ? 2 * size() : 1; | ||
+ | |||
+ | // allocate new memory | ||
+ | auto newdata = alloc.allocate(newcapacity); | ||
+ | |||
+ | // move the data from the old memory to the new | ||
+ | auto dest = newdata; | ||
+ | auto elem = elements; // points to the next element in the old array | ||
+ | for (size_t i = 0; i != size(); ++i) | ||
+ | | ||
+ | | ||
+ | |||
+ | // update our data structure to point to the new elements | ||
+ | | ||
+ | | ||
+ | cap = elements + newcapacity; | ||
+ | } | ||
+ | </ | ||
+ | ===push_back() 成员=== | ||
+ | 由于 // | ||
+ | <code cpp> | ||
+ | void StrVec:: | ||
+ | { | ||
+ | if (size() == capacity()) | ||
+ | reallocate(); | ||
+ | } | ||
+ | </ | ||
+ | 保证了足够空间后,我们只需要在 off-the-end 位置进行新元素构造,即可达到 // | ||
+ | <code cpp> | ||
+ | void StrVec:: | ||
+ | { | ||
+ | chk_n_alloc(); | ||
+ | // construct a copy of s in the element to which first_free points | ||
+ | alloc.construct(first_free++, | ||
+ | } | ||
+ | </ | ||
+ | 这里需要先**构造再位移**,因此需要使用后置版本的自增。不然 off-the-end 位置的元素将不会被构造。 | ||
+ | ====移动对象==== | ||
+ | 之前在实现 // | ||
+ | 有两种典型的情况可以使用移动: | ||
+ | * 将对象从旧内存移动到新的内存 | ||
+ | * 转移**不可拷贝对象**的内容,比如 IO 类或 // | ||
+ | <WRAP center round info 100%> | ||
+ | C++ 11 之前,并没有提供直接移动的功能。传递很多较大的对象,或是需要分配内存的对象时,都会进行非常多的,但又不必要的拷贝。 | ||
+ | </ | ||
+ | ===右值引用=== | ||
+ | 为了支持移动操作,新标准提供了一种被称为**右值引用**(// | ||
+ | ==左值与右值== | ||
+ | 为了说明右值引用的功能,这里需要再次回顾一下**左值**(// | ||
+ | * 左值强调的是 identity。通常情况下,左值指的是对象,可以被取地址。 | ||
+ | * 右值强调的是 value。通常情况下,右值指的是**临时**对象,**不能**被取地址。 | ||
+ | 左值引用只能绑定与自身类型相同的对象,不能绑定需要转换类型的对象,// | ||
+ | <code cpp> | ||
+ | int i = 42; | ||
+ | int &r = i; // ok: r refers to i | ||
+ | int && | ||
+ | int &r2 = i * 42; // error: i * 42 is an rvalue | ||
+ | const int &r3 = i * 42; // ok: we can bind a reference to const to an rvalue | ||
+ | int && | ||
+ | </ | ||
+ | 常见的左值与右值有: | ||
+ | * **左值**: | ||
+ | * **变量**(变量的生存周期由 scope 决定;很明显是左值) | ||
+ | * string literal(字符串占用内存) | ||
+ | * 赋值操作的结果( '' | ||
+ | * 下标操作的结果 | ||
+ | * 解引用的结果 | ||
+ | * 前置递增 / 递减的结果 | ||
+ | * **右值**: | ||
+ | * **除了 string literal** 以外的 //Literal// | ||
+ | * 函数的**非引用**返回 | ||
+ | * 算术 / 关系运算的结果 | ||
+ | * 位运算的结果 | ||
+ | * 后置递增 / 递减的结果 | ||
+ | 需要注意的是,除了常量引用可以绑定右值的例外,**左值引用只能绑定左值,而右值引用同理**。 | ||
+ | <WRAP center round tip 100%> | ||
+ | const &T 是种例外。该引用是左值引用,但可以绑定能转换到 //T// 类型的右值。一般来说,该类引用绑定的是一个临时对象,临时对象中装载了对应的右值。 | ||
+ | </ | ||
+ | |||
+ | ==右值引用与右值的特性== | ||
+ | 从生存周期上来说,左值是长期持续的,而右值是临时(短暂)的。由于右值引用只能与右值(临时对象)绑定,因此右值引用指向的是一个**将要被销毁的,不能再被重复利用的对象**。利用这种性质,我们可以使用**接管**(或者说 steal)的方式来获取右值引用指向的资源,而**不是拷贝**。也就是说,右值引用可以使用在任何具有移动意义的数据传递中使用。由于该过程使用接管代替了拷贝,因此效率会大大提升。 | ||
+ | ==变量是左值== | ||
+ | 变量可以被视作表达式,因此也具有左右值的属性。总的来说,变量是左值: | ||
+ | <code cpp> | ||
+ | int && | ||
+ | int && | ||
+ | </ | ||
+ | 可以注意到,上例中,即便是变量被定义为了右值引用,而其自身仍然是一个左值。因此,该变量不能绑定到另外一个右值引用上。 | ||
+ | ==std::move 函数== | ||
+ | '' | ||
+ | <code cpp> | ||
+ | int && | ||
+ | int && | ||
+ | </ | ||
+ | 注意第一行,'' | ||
+ | ===移动构造与移动赋值=== | ||
+ | '' | ||
+ | ==移动构造函数== | ||
+ | 移动构造函数具有以下特点: | ||
+ | * 接收参数的类型为自身 class type 的**右值引用** | ||
+ | * 额外的参数必须有**默认初始值** | ||
+ | * 被移动的对象需要确保能安全销毁,也就是移动后,原有的(被移动的)对象**不能再指向被移动的资源**,管理全交给了移动后的新对象 | ||
+ | * 移动构造函数**不会分配任何新的内存** | ||
+ | 典型的移动构造函数的操作步骤: | ||
+ | - 移动参数对象的**内容**到局部对象(比如使用当前对象成员的指针指向参数对象的内容) | ||
+ | - 解除参数对象对被移动内容的控制(将所有参数对象的指针设为 '' | ||
+ | 一个 //StrVec// 的移动构造函数定义如下: | ||
+ | <code cpp> | ||
+ | StrVec:: | ||
+ | // member initializers take over the resources in s | ||
+ | : elements(s.elements), | ||
+ | { | ||
+ | // leave s in a state in which it is safe to run the destructor | ||
+ | s.elements = s.first_free = s.cap = nullptr; | ||
+ | } | ||
+ | </ | ||
+ | ==移动操作与异常== | ||
+ | 由于移动操作并不参与分配空间,理论上来说该操作不应该抛出分配异常。但在实际操作中,我们需要“通知”标准库我们的移动构造函数不会抛出异常,从而避免移动构造函数的异常处理带来的额外开销。\\ \\ | ||
+ | 通常,我们使用 '' | ||
+ | <code cpp> | ||
+ | StrVec(StrVec&& | ||
+ | StrVec:: | ||
+ | </ | ||
+ | < | ||
+ | 首先需要明确两点: | ||
+ | * 虽然从逻辑上来说,移动操作不参与分配内存,也就不会抛出异常,但所有的移动操作都被默认允许抛出异常。 | ||
+ | * 标准库容器有自己解决分配异常的方案 | ||
+ | 以 //vector// 作为例子。如果调用 // | ||
+ | * 由于移动操作异常,新创建的 //vector// 并没有成功获取之前的所有元素 | ||
+ | * 而此时原来的对象已经被释放掉了 | ||
+ | 当然我们可以通过拷贝而不是移动的方式来确保原来的对象的安全性;但如果希望仍然使用移动操作的话,我们必须**确保移动操作不会抛出任何异常**。因此,这种情况下必须手动的添加 '' | ||
+ | ==移动赋值运算符== | ||
+ | 移动赋值运算符也是接收一个当前类对象的**右值引用**,返回当前类类型的引用。其逻辑与拷贝赋值运算符类似,也需要首先清空当前对象: | ||
+ | * 释放当前(左手边)对象所占资源 | ||
+ | * 接管参数(右手边临时)对象内容 | ||
+ | * 解除(右手边临时)参数对象对内容的控制(空置) | ||
+ | //StrVec// 的实现: | ||
+ | <code cpp> | ||
+ | StrVec & | ||
+ | |||
+ | { | ||
+ | // direct test for self-assignment | ||
+ | if (this != &rhs) { | ||
+ | free(); | ||
+ | | ||
+ | elements = rhs.elements; | ||
+ | first_free = rhs.first_free; | ||
+ | cap = rhs.cap; | ||
+ | | ||
+ | // leave rhs in a destructible state | ||
+ | rhs.elements = rhs.first_free = rhs.cap = nullptr; | ||
+ | } | ||
+ | return *this; | ||
+ | } | ||
+ | </ | ||
+ | 如果是自我赋值,则直接返回当前对象。 | ||
+ | ==被移动的对象必须是可以析构的== | ||
+ | 我们注意到在移动版本的拷贝控制成员中,最后都将**被移动对象**(右手边)所有的指针设为了 '' | ||
+ | * 即便对象所管理的资源可能被接管了,但该对象的状态依然是**有效**的,也就是仍然可以被赋予新值,或是任何不依赖当前内容的操作 | ||
+ | * 移动的操作目标是对象,而不是对象管理的资源。因此,任何移动操作都不会依赖于对象的值(管理的资源) | ||
+ | 诸如此类的原因,我们需要将被移动的对象空置,保证该对象处于一个 “可以被析构” 的状态;但该对象所管理的资源(值)是不可控的,因此不应该对这些资源做任何假设。以 string 为例,在左边的对象接管了资源后,右边的对象任然可以使用 // | ||
+ | ==合成的移动操作== | ||
+ | 编译器在某些情况下也会为我们合成移动成员。但该合成的条件与 //Big Three// 有些不同: | ||
+ | * 即便是定义了 //Big Three// ,编译器也**不会合成**移动成员。这种情况下,所有的移动操作都会使用拷贝成员代替。 | ||
+ | * 只有满足以下两个前提: | ||
+ | * **类没有定义任何拷贝成员** | ||
+ | * **所有非静态成员都可以被移动,或是定义了的移动操作** | ||
+ | 编译器才会合成移动拷贝控制成员,比如 : | ||
+ | <code cpp> | ||
+ | // the compiler will synthesize the move operations for X and hasX | ||
+ | |||
+ | struct X | ||
+ | { | ||
+ | int i; // built-in types can be moved | ||
+ | std::string s; // string defines its own move operations | ||
+ | }; | ||
+ | |||
+ | struct hasX | ||
+ | { | ||
+ | X mem; // X has synthesized move operations | ||
+ | }; | ||
+ | |||
+ | X x, x2 = std:: | ||
+ | hasX hx, hx2 = std:: | ||
+ | </ | ||
+ | ==如何判断合成移动成员是否被删除== | ||
+ | 默认情况下,移动成员不会被隐式的定义为被删除的函数。只有在以下条件下,才会被定义为**被删除的移动成员**: | ||
+ | - 显式的使用 '' | ||
+ | - 类中定义了拷贝构造函数,但没有定义移动构造函数 | ||
+ | - 类中的成员没有定义自身的拷贝操作,且编译器无法为该类合成移动构造函数 | ||
+ | - 类中的**成员**拥有被删除的,或无法访问的移动构造函数 / 移动赋值运算符 | ||
+ | - 类中的**析构函数**是被删除的,或是无法访问的 | ||
+ | - 类中有 const 或是**引用**类型的成员 | ||
+ | <code cpp> | ||
+ | // assume Y is a class that defines its own copy constructor but not a move constructor | ||
+ | // Y has a deleted move constructor (rule 2) | ||
+ | |||
+ | struct hasY { | ||
+ | hasY() = default; | ||
+ | hasY(hasY&& | ||
+ | Y mem; // hasY will have a deleted move constructor (rule 4) | ||
+ | }; | ||
+ | |||
+ | hasY hy; | ||
+ | hasY hy2 = std:: | ||
+ | </ | ||
+ | 除此之外,定义移动成员也对拷贝成员有影响。如果类中**定义了任意一个移动成员**(移动构造函数或移动赋值运算符),则**合成的拷贝成员则是被删除的**。 | ||
+ | <WRAP center round info 100%> | ||
+ | 定义了移动成员的类也应该定义其拷贝成员。 | ||
+ | </ | ||
+ | ==移动右值,拷贝左值== | ||
+ | 当类中同时存在移动版本与拷贝版本的拷贝控制成员时,编译器会**通过函数匹配**来决定构造函数的使用。比如下面的例子: | ||
+ | <code cpp> | ||
+ | StrVec v1, v2; | ||
+ | v1 = v2; // v2 is an lvalue; copy assignment | ||
+ | StrVec getVec(istream &); // getVec returns an rvalue | ||
+ | v2 = getVec(cin); | ||
+ | </ | ||
+ | 上例中: | ||
+ | * '' | ||
+ | * '' | ||
+ | * 如果是拷贝赋值运算,需要将该返回值转换为 reference to const 的形式 | ||
+ | * 如果是移动赋值运算,则是完美匹配。因此此处选择移动赋值运算符。 | ||
+ | <WRAP center round important 100%> | ||
+ | copy-swap 赋值运算符也使用右值作为参数。如果类中有移动赋值运算符,会导致二义性。该版本通常使用移动 + 拷贝成员替代,后文有介绍。 | ||
+ | </ | ||
+ | ==未定义移动参数的情况== | ||
+ | 当类中只有拷贝构造函数,而没有移动构造函数时: | ||
+ | * 类不会合成移动构造函数 | ||
+ | * 右值引用将按值的方式将返回的右值绑定到 reference to const 类型的参数上,因此会调用拷贝构造函数: | ||
+ | <code cpp> | ||
+ | class Foo { | ||
+ | public: | ||
+ | Foo() = default; | ||
+ | Foo(const Foo& | ||
+ | // other members, but Foo does not define a move constructor | ||
+ | }; | ||
+ | |||
+ | Foo x; | ||
+ | Foo y(x); // copy constructor; | ||
+ | Foo z(std:: | ||
+ | </ | ||
+ | 上例中完成了 '' | ||
+ | <WRAP center round tip 100%> | ||
+ | 使用拷贝构造函数取代移动构造函数进行构造是**安全**的;因为拷贝构造的过程不会改变被拷贝对象的状态;这与移动构造的要求(被拷贝对象是有效的)是一致的。 | ||
+ | </ | ||
+ | ==拷贝交换赋值运算符与移动== | ||
+ | 如果类中定义了拷贝交换赋值运算符,同时定义移动构造函数也可以很好的提升该运算符的性能,比如下例: | ||
+ | <code cpp> | ||
+ | class HasPtr { | ||
+ | |||
+ | public: | ||
+ | // added move constructor | ||
+ | HasPtr(HasPtr && | ||
+ | // assinment operator is both the move- and copy-assignment operator | ||
+ | HasPtr& operator=(HasPtr rhs) | ||
+ | { swap(*this, rhs); return *this; } | ||
+ | // other members as in § 13.2.1 (p. 511) | ||
+ | }; | ||
+ | </ | ||
+ | 上例中,我们为 '' | ||
+ | <code cpp> | ||
+ | hp = hp2; // hp2 is an lvalue; copy constructor used to copy hp2 | ||
+ | hp = std:: | ||
+ | </ | ||
+ | 可见,当参数类型是右值时,参数的传递会使用移动构造函数进行构造;当参数是左值时,则会调用拷贝构造函数进行构造。因此该 copy-swap 运算符实际上可以同时接收左值与右值参数,即**左值拷贝构造,右值移动构造**。 | ||
+ | <WRAP center round tip 100%> | ||
+ | 本例中的移动构造函数通过 //p.ps = 0;// 来确保被移动对象处于可以被析构的状态。 | ||
+ | </ | ||
+ | <WRAP center round box 100%> | ||
+ | 推荐使用拷贝赋值运算符 + 移动赋值运算符的重载组合来代替 copy-swap。copy-swap 赋值运算符在将参数传递至内部 //swap()// 函数的过程中会不可避免的使用一次拷贝,这使得其效率会大大降低。课后题 13.53 为专门针对该问题的讨论。 | ||
+ | </ | ||
+ | <WRAP center round help 100%> | ||
+ | 新版本下的**五个**拷贝控制成员应该被视为一个整体来设计。如果某个类定义了拷贝操作,则应该定义所有的五个操作。 | ||
+ | </ | ||
+ | ==实例:Message 类的移动成员== | ||
+ | //Message// 类中包含了 string 类型的类容和用于存储文件夹关系的 set,因此实现移动成员可以有效的避免拷贝带来的额外开销。与其拷贝成员一致,移动成员的设计也需要维护当前邮件与文件夹的关系网。我们使用 '' | ||
+ | <code cpp> | ||
+ | // move the Folder pointers from m to this Message | ||
+ | |||
+ | void Message:: | ||
+ | { | ||
+ | folders = std:: | ||
+ | for (auto f : folders) | ||
+ | { // for each Folder | ||
+ | f-> | ||
+ | f-> | ||
+ | } | ||
+ | m-> | ||
+ | } | ||
+ | </ | ||
+ | 该函数的逻辑为: | ||
+ | - 移动参数邮件对象所在的文件夹信息到当前邮件对象 | ||
+ | - 按文件夹为单位,依次更新当前邮件对象的位置 | ||
+ | - 清空参数邮件对象的文件夹信息,确保其处于可以被析构的状态 | ||
+ | 几个要点: | ||
+ | * 该函数使用了移动的方式来转移 set 成员 | ||
+ | * '' | ||
+ | * 该函数在最后使用了 //clear()// 成员来清空参数邮件对象的关系网 | ||
+ | 对应的移动构造函数定义如下: | ||
+ | <code cpp> | ||
+ | Message:: | ||
+ | { | ||
+ | move_Folders(& | ||
+ | } | ||
+ | </ | ||
+ | 对应的移动赋值运算符定义如下: | ||
+ | <code cpp> | ||
+ | Message& | ||
+ | { | ||
+ | if (this != &rhs) { // direct check for self-assignment | ||
+ | remove_from_Folders(); | ||
+ | contents = std:: | ||
+ | move_Folders(& | ||
+ | } | ||
+ | return *this; | ||
+ | } | ||
+ | </ | ||
+ | ==移动迭代器== | ||
+ | 在 //StrVec// 类中,我们使用 '' | ||
+ | 比起使用 '' | ||
+ | 实现上,我们使用标准库函数 '' | ||
+ | <code cpp> | ||
+ | void StrVec:: | ||
+ | |||
+ | { | ||
+ | // allocate space for twice as many elements as the current size | ||
+ | auto newcapacity = size() ? 2 * size() : 1; | ||
+ | auto first = alloc.allocate(newcapacity); | ||
+ | |||
+ | // move the elements | ||
+ | auto last = uninitialized_copy(make_move_iterator(begin()), | ||
+ | | ||
+ | | ||
+ | |||
+ | free(); | ||
+ | |||
+ | elements = first; | ||
+ | first_free = last; | ||
+ | cap = elements + newcapacity; | ||
+ | } | ||
+ | </ | ||
+ | '' | ||
+ | 需要注意的是,标准库不能保证是否会对对被移动的元素进行复用。在使用算法之前,我们需要确保**该算法不会复用被移动的对象**。 | ||
+ | <WRAP center round tip 100%> | ||
+ | // | ||
+ | </ | ||
+ | ===右值引用与成员函数=== | ||
+ | 除了拷贝成员之外,成员函数也可以通过重载接收右值引用的版本来获得移动的性能提升。设计的方法与拷贝成员类似: | ||
+ | * 获取左值的版本的参数类型为 '' | ||
+ | * 获取右值的版本的参数类型为 '' | ||
+ | '' | ||
+ | <code cpp> | ||
+ | void push_back(const T&); // copy: binds to any kind of T | ||
+ | void push_back(T&& | ||
+ | </ | ||
+ | 该设计中,任何可以修改的右值参数都可以使用**移动构造函数**来完成 push_back 的操作;任意 non-const rvalue 对于移动版本的 // | ||
+ | 需要注意的是,我们不需要定义接收 '' | ||
+ | * 移动构造函数需要从被移动对象中“获取”资源,因此被移动对象(parameter)**必须可以被修改**,也就是必须使用 '' | ||
+ | * 逻辑上,拷贝构造函数不能修改参数对象。因此需要使用 '' | ||
+ | ==实例:StrVec 类中 push_back 的移动版本== | ||
+ | 简单的实现如下: | ||
+ | <code cpp> | ||
+ | class StrVec | ||
+ | { | ||
+ | public: | ||
+ | void push_back(const std:: | ||
+ | void push_back(std:: | ||
+ | //.... | ||
+ | }; | ||
+ | |||
+ | void | ||
+ | StrVec:: | ||
+ | { | ||
+ | chk_n_alloc(); | ||
+ | alloc.construct(first_free++, | ||
+ | } | ||
+ | |||
+ | void | ||
+ | StrVec:: | ||
+ | { | ||
+ | chk_n_alloc(); | ||
+ | alloc.construct(first_free++, | ||
+ | } | ||
+ | </ | ||
+ | 上述两种重载的实现中,唯一不同在于 '' | ||
+ | <code cpp> | ||
+ | StrVec vec; // empty StrVec | ||
+ | string s = "some string or another"; | ||
+ | vec.push_back(s); | ||
+ | vec.push_back(" | ||
+ | </ | ||
+ | ==右值与成员函数== | ||
+ | 早前版本的 C++ 中,成员函数的调用并不会考虑类对象是左值还是右值,比如如下的写法,右值对象既可以调用成员函数,也可以放到等号的左边: | ||
+ | <code cpp> | ||
+ | string s1 = "a value", | ||
+ | auto n = (s1 + s2).find(' | ||
+ | s1 + s2 = " | ||
+ | </ | ||
+ | 这种用法在 C++11 之前是没有办法禁止的。因此,C++11 中提供了**引用限定符** //Reference Qualifier// | ||
+ | <code cpp> | ||
+ | class Foo { | ||
+ | public: | ||
+ | Foo & | ||
+ | // other members of Foo | ||
+ | }; | ||
+ | |||
+ | Foo & | ||
+ | |||
+ | { | ||
+ | // do whatever is needed to assign rhs to this object | ||
+ | return *this; | ||
+ | } | ||
+ | </ | ||
+ | 除此之外,引用限定符也提供了右值的选项,来限定 '' | ||
+ | * 引用限定符只能修饰**非静态**的成员函数 | ||
+ | * 引用限定符必须在函数的**声明和定义**的时候**都出现** | ||
+ | |||
+ | <code cpp> | ||
+ | class Foo { | ||
+ | |||
+ | public: | ||
+ | Foo rvalueMem() &&; //may assign only to modifiable rvalues | ||
+ | Foo someMem() & const; | ||
+ | Foo anotherMem() const &; // ok: const qualifier comes first | ||
+ | |||
+ | }; | ||
+ | </ | ||
+ | 根据之前的设定,''&'' | ||
+ | <code cpp> | ||
+ | Foo & | ||
+ | Foo retVal(); | ||
+ | |||
+ | Foo i, j; // i and j are lvalues | ||
+ | |||
+ | i = j; // ok: i is an lvalue | ||
+ | retFoo() = j; // ok: retFoo() returns an lvalue | ||
+ | retVal() = j; // error: retVal() returns an rvalue | ||
+ | i = retVal(); | ||
+ | </ | ||
+ | ==基于引用限定符的重载== | ||
+ | 类成员函数可以根据加载的引用限定符的不同,以及是否为 const 进行重载;比如下例: | ||
+ | <code cpp> | ||
+ | class Foo { | ||
+ | public: | ||
+ | Foo sorted() &&; | ||
+ | Foo sorted() const &; | ||
+ | // other members of Foo | ||
+ | |||
+ | private: | ||
+ | vector< | ||
+ | }; | ||
+ | |||
+ | // this object is an rvalue, so we can sort in place | ||
+ | |||
+ | Foo Foo:: | ||
+ | { | ||
+ | sort(data.begin(), | ||
+ | return *this; | ||
+ | } | ||
+ | |||
+ | // this object is either const or it is an lvalue; either way we can't sort in place | ||
+ | |||
+ | Foo Foo:: | ||
+ | { | ||
+ | Foo ret(*this); | ||
+ | sort(ret.data.begin(), | ||
+ | return ret; // return the copy | ||
+ | } | ||
+ | </ | ||
+ | 第一个版本的 // | ||
+ | * value 可以转换为 reference to const | ||
+ | * plain reference 可以转换为 reference to const | ||
+ | 这意味着该版本可以接收所有类型的数据。这也很符合逻辑:因为拷贝版本的 // | ||
+ | <code cpp> | ||
+ | retVal().sorted(); | ||
+ | retFoo().sorted(); | ||
+ | </ | ||
+ | 需要注意的是,如果引用限定符与 const 在某个函数中出现,则所有**名称 & 参数列表**与该函数**相同**的**合法**重载版本都**必须提供引用限定符**。也就是说,以 const 区分带引用限定符的重载版本,都需要带上引用限定符: | ||
+ | <code cpp> | ||
+ | class Foo { | ||
+ | |||
+ | public: | ||
+ | |||
+ | Foo sorted() &&; | ||
+ | Foo sorted() const; // error: must have reference qualifier | ||
+ | |||
+ | // Comp is type alias for the function type (see § 6.7 (p. 249)) | ||
+ | // that can be used to compare int values | ||
+ | using Comp = bool(const int&, const int&); | ||
+ | Foo sorted(Comp*); | ||
+ | Foo sorted(Comp*) const; | ||
+ | }; | ||
+ | </ | ||
+ | 上例中: | ||
+ | * 参数列表相同的 // | ||
+ | * 名字相同但参数列表不同的则不用 |