Skip to content

面向对象编程基础

本课程入选教育部产学合作协同育人项目 课程主页:http://cpp.njuer.org 课程老师:陈明 http://cv.mchen.org

ppt和代码下载地址
git clone https://gitee.com/cpp-njuer-org/book

第III部分 类设计者的工具

第13章拷贝控制 第14章操作重载与类型转换 第15章面向对象程序设计 第16章模板与泛型编程

第13章

拷贝控制

拷贝控制

当定义一个类时我们显式地或隐式地指定
在此类型的对象拷贝移动赋值和销毁时做什么
一个类通过定义五种特殊的成员函数来控制这些操作包括
- 拷贝构造函数copy constructor)、
- 拷贝赋值运算符copy-assignmentoperator)、
- 移动构造函数move constructor)、
- 移动赋值运算符move-assignment operator
- 析构函数destructor)。
拷贝和移动构造函数定义了当用同类型的另一个对象初始化本对象时做什么
拷贝和移动赋值运算符定义了将一个对象赋予同类型的另一个对象时做什么
析构函数定义了当此类型对象销毁时做什么
我们称这些操作为拷贝控制操作copy control)。

拷贝控制

如果一个类没有定义所有这些拷贝控制成员编译器会自动为它定义缺失的操作
因此很多类会忽略这些拷贝控制操作
但是对一些类来说依赖这些操作的默认定义会导致灾难
通常实现拷贝控制操作最困难的地方是首先认识到什么时候需要定义这些操作

在定义任何C++类时拷贝控制操作都是必要部分
必须定义对象拷贝移动赋值或销毁时做什么这常常令人感到困惑
如果我们不显式定义这些操作编译器也会为我们定义
- 但编译器定义的版本的行为可能并非我们所想

拷贝、赋值与销毁

拷贝构造函数

如果一个构造函数的第一个参数是自身类类型的引用且任何额外参数都有默认值
则此构造函数是拷贝构造函数
class Foo {
public:
   Foo();             // default constructor
   Foo(const Foo&);   // copy constructor
   // ...
};
拷贝构造函数的第一个参数必须是一个引用类型
- 虽然我们可以定义一个接受非const引用的拷贝构造函数
  但此参数几乎总是一个const的引用
拷贝构造函数在几种情况下都会被隐式地使用
因此拷贝构造函数通常不应该是explicit的

合成拷贝构造函数

如果我们没有为一个类定义拷贝构造函数编译器会为我们定义一个
与合成默认构造函数不同
即使我们定义了其他构造函数编译器也会为我们合成一个拷贝构造函数

对某些类来说合成拷贝构造函数synthesized copy constructor
用来阻止我们拷贝该类类型的对象
而一般情况合成的拷贝构造函数会将其参数的成员逐个拷贝到正在创建的对象中
编译器从给定对象中依次将每个非static成员拷贝到正在创建的对象中

合成拷贝构造函数

每个成员的类型决定了它如何拷贝对类类型的成员会使用其拷贝构造函数来拷贝
内置类型的成员则直接拷贝
虽然我们不能直接拷贝一个数组但合成拷贝构造函数会逐元素地拷贝一个数组类型的成员
- 如果数组元素是类类型则使用元素的拷贝构造函数来进行拷贝
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::Sales_data(const Sales_data &orig):
    bookNo(orig.bookNo),         // uses the string copy constructor
    units_sold(orig.units_sold), // copies orig.units_sold
    revenue(orig.revenue)        // copies orig.revenue
    {    }                       // empty body

拷贝初始化

现在我们可以完全理解直接初始化和拷贝初始化之间的差异了
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

当使用直接初始化时我们实际上是
- 要求编译器使用普通的函数匹配来选择与我们提供的参数最匹配的构造函数
当我们使用拷贝初始化copy initialization我们
- 要求编译器将右侧运算对象拷贝到正在创建的对象中
- 如果需要的话还要进行类型转换
拷贝初始化通常使用拷贝构造函数来完成但是如果一个类有一个移动构造函数
- 则拷贝初始化有时会使用移动构造函数而非拷贝构造函数来完成
- 但现在我们只需了解拷贝初始化何时发生以及
    - 拷贝初始化是依靠拷贝构造函数或移动构造函数来完成的就可以了

拷贝初始化

拷贝初始化不仅在我们用=定义变量时会发生在下列情况下也会发生· 
- 将一个对象作为实参传递给一个非引用类型的形参· 
- 从一个返回类型为非引用类型的函数返回一个对象· 
- 用花括号列表初始化一个数组中的元素或一个聚合类中的成员
- 某些类类型还会对它们所分配的对象使用拷贝初始化
    - 例如当我们初始化标准库容器或是调用其insert或push成员时
      容器会对其元素进行拷贝初始化
      与之相对用emplace成员创建的元素都进行直接初始化

参数和返回值

在函数调用过程中具有非引用类型的参数要进行拷贝初始化
类似的当一个函数具有非引用的返回类型时返回值会被用来初始化调用方的结果

拷贝构造函数被用来初始化非引用类类型参数
- 这一特性解释了为什么拷贝构造函数自己的参数必须是引用类型
- 如果其参数不是引用类型则调用永远也不会成功——为了调用拷贝构造函数
  我们必须拷贝它的实参但为了拷贝实参我们又需要调用拷贝构造函数
  如此无限循环

拷贝初始化的限制

如果我们使用的初始化值要求通过一个explicit的构造函数来进行类型转换
那么使用拷贝初始化还是直接初始化就不是无关紧要的了
vector<int> v1(10);  // ok: direct initialization
vector<int> v2 = 10; // error: constructor that takes a size is explicit
void f(vector<int>); // f's parameter is copy initialized
f(10); // error: can't use an explicit constructor to copy an argument
f(vector<int>(10));  // ok: directly construct a temporary vector from an int
//直接初始化v1是合法的,但看起来与之等价的拷贝初始化v2则是错误的,
//因为vector的接受单一大小参数的构造函数是explicit的。
//出于同样的原因,当传递一个实参或从函数返回一个值时,
//我们不能隐式使用一个explicit构造函数。如果我们希望使用一个explicit构造函数
//就必须显式地使用,像此代码中最后一行那样。

编译器可以绕过拷贝构造函数

在拷贝初始化过程中编译器可以但不是必须跳过拷贝/移动构造函数直接创建对象
编译器被允许将下面的代码
string null_book = "9-999-99999-9"; // copy initialization
改写为
string null_book("9-999-99999-9");//compiler omits the copy constructor
但是即使编译器略过了拷贝/移动构造函数但在这个程序点上
拷贝/移动构造函数必须是存在且可访问的例如不能是private的

练习

拷贝构造函数是什么什么时候使用它

如果一个构造函数的第一个参数是自身类类型的引用且任何额外参数都有默认值
则此构造函数是拷贝构造函数
当使用拷贝初始化时我们会用到拷贝构造函数

练习

解释为什么下面的声明是非法的
Sales_data::Sales_data(Sales_data rhs);

参数类型应该是引用类型

练习

当我们拷贝一个StrBlob时会发生什么拷贝一个StrBlobPtr呢

当我们拷贝StrBlob时会使 shared_ptr 的引用计数加1
当我们拷贝 StrBlobPtr 引用计数不会变化

练习

假定 Point 是一个类类型它有一个public的拷贝构造函数
指出下面程序片段中哪些地方使用了拷贝构造函数
Point global;
Point foo_bar(Point arg) // 1
{
    Point local = arg, *heap = new Point(global); 
    // 2: Point local = arg,  3: Point *heap = new Point(global) 
    *heap = local; 
    Point pa[4] = { local, *heap }; // 4, 5
    return *heap;  // 6
}


上面有6处地方使用了拷贝构造函数

练习

给定下面的类框架编写一个拷贝构造函数拷贝所有成员你的构造函数应该
动态分配一个新的string并将对象拷贝到ps所指向的位置而不是拷贝ps本身
class HasPtr {
public:
    HasPtr(const std::string& s = std::string()):
        ps(new std::string(s)), i(0) { }
private:
    std::string *ps;
    int i;
}
#include <string>
class HasPtr {
public:
    HasPtr(const std::string &s = std::string()) : 
        ps(new std::string(s)), i(0) { }
    HasPtr(const HasPtr& hp) : ps(new std::string(*hp.ps)), i(hp.i) { }
private:
    std::string *ps;
    int i;
};

拷贝赋值运算符

类也可以控制其对象如何赋值
Sales_data trans, accum;
trans = accum; // uses the Sales_data copy-assignment operator

与拷贝构造函数一样如果类未定义自己的拷贝赋值运算符编译器会为它合成一个

重载赋值运算符

重载运算符overloadedoperator
- 本质上是函数其名字由operator关键字后接表示要定义的运算符的符号组成
- 因此赋值运算符就是一个名为operator=的函数
  类似于任何其他函数运算符函数也有一个返回类型和一个参数列表

重载运算符的参数表示运算符的运算对象
某些运算符包括赋值运算符必须定义为成员函数
如果一个运算符是一个成员函数其左侧运算对象就绑定到隐式的this参数
对于一个二元运算符例如赋值运算符其右侧运算对象作为显式参数传递

重载赋值运算符

拷贝赋值运算符接受一个与其所在类相同类型的参数
class Foo {
public:
    Foo& operator=(const Foo&); // assignment operator
   // ...
};
为了与内置类型的赋值保持一致赋值运算符通常返回一个指向其左侧运算对象的引用
标准库通常要求保存在容器中的类型要具有赋值运算符且其返回值是左侧运算对象的引用

赋值运算符通常应该返回一个指向其左侧运算对象的引用

合成拷贝赋值运算符

与处理拷贝构造函数一样如果一个类未定义自己的拷贝赋值运算符编译器会为它
- 生成一个合成拷贝赋值运算符synthesized copy-assignment operator)。
类似拷贝构造函数对于某些类合成拷贝赋值运算符用来禁止该类型对象的赋值
如果拷贝赋值运算符并非出于此目的它会将右侧运算对象的每个非static成员赋予
左侧运算对象的对应成员这一工作是通过成员类型的拷贝赋值运算符来完成的
对于数组类型的成员逐个赋值数组元素
合成拷贝赋值运算符返回一个指向其左侧运算对象的引用
下面的代码等价于Sales_data的合成拷贝赋值运算符
// equivalent to the synthesized copy-assignment operator
Sales_data&
Sales_data::operator=(const Sales_data &rhs)
{
    bookNo = rhs.bookNo;          // calls the string::operator=
    units_sold = rhs.units_sold;  // uses the built-in int assignment
    revenue = rhs.revenue;        // uses the built-in double assignment
    return *this;                 // return a reference to this object
}

练习

拷贝赋值运算符是什么什么时候使用它合成拷贝赋值运算符完成什么工作
什么时候会生成合成拷贝赋值运算符

拷贝赋值运算符是一个名为 operator= 的函数当赋值运算发生时就会用到它
合成拷贝赋值运算符可以用来禁止该类型对象的赋值
如果一个类未定义自己的拷贝赋值运算符编译器会为它生成一个合成拷贝赋值运算符

练习

当我们将一个 StrBlob 赋值给另一个 StrBlob 会发生什么赋值 StrBlobPtr 

会发生浅层复制

练习

为HasPtr 类编写赋值运算符
类似拷贝构造函数你的赋值运算符应该将对象拷贝到ps指向的位置
#include <string>
class HasPtr {
public:
    HasPtr(const std::string &s = std::string()) :
                        ps(new std::string(s)), i(0) { }
    HasPtr(const HasPtr &hp) : ps(new std::string(*hp.ps)), i(hp.i) { }
    HasPtr& operator=(const HasPtr &rhs_hp) {
        if(this != &rhs_hp){
            std::string *temp_ps = new std::string(*rhs_hp.ps);
            delete ps;
            ps = temp_ps;
            i = rhs_hp.i;
        }
        return *this;
    }
private:
    std::string *ps;
    int i;
};

析构函数

析构函数执行与构造函数相反的操作
- 构造函数初始化对象的非static数据成员还可能做一些其他工作
- 析构函数释放对象使用的资源并销毁对象的非static数据成员
析构函数是类的一个成员函数名字由波浪号接类名构成
它没有返回值也不接受参数
class Foo {
public:
    ~Foo();    // destructor
   // ...
};
由于析构函数不接受参数因此它不能被重载
对一个给定类只会有唯一一个析构函数

析构函数完成什么工作

如同构造函数有一个初始化部分和一个函数体析构函数也有一个函数体和一个析构部分
在一个构造函数中成员的初始化是在函数体执行之前完成的且按照它们在类中
出现的顺序进行初始化
在一个析构函数中首先执行函数体然后销毁成员成员按初始化顺序的逆序销毁

在对象最后一次使用之后析构函数的函数体可执行类设计者希望执行的任何收尾工作
通常析构函数释放对象在生存期分配的所有资源

在一个析构函数中不存在类似构造函数中初始化列表的东西来控制成员如何销毁
析构部分是隐式的成员销毁时发生什么完全依赖于成员的类型
销毁类类型的成员需要执行成员自己的析构函数
内置类型没有析构函数因此销毁内置类型成员什么也不需要做

隐式销毁一个内置指针类型的成员不会delete它所指向的对象
与普通指针不同智能指针是类类型所以具有析构函数
因此与普通指针不同智能指针成员在析构阶段会被自动销毁

什么时候会调用析构函数

无论何时一个对象被销毁就会自动调用其析构函数:· 
- 变量在离开其作用域时被销毁。· 
- 当一个对象被销毁时其成员被销毁。·
- 容器无论是标准库容器还是数组被销毁时其元素被销毁。· 
- 对于动态分配的对象当对指向它的指针应用delete运算符时被销毁·
- 对于临时对象当创建它的完整表达式结束时被销毁

由于析构函数自动运行我们的程序可以按需要分配资源
通常无须担心何时释放这些资源

什么时候会调用析构函数

{ // new scope
    // p and p2 point to dynamically allocated objects
    Sales_data  *p = new Sales_data;     // p is a built-in pointer
    auto p2 = make_shared<Sales_data>(); // p2 is a shared_ptr
    Sales_data item(*p);     // copy constructor copies *p into item
    vector<Sales_data> vec;  // local object
    vec.push_back(*p2);      // copies the object to which p2 points
    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

当指向一个对象的引用或指针离开作用域时析构函数不会执行

合成析构函数

当一个类未定义自己的析构函数时编译器会为它
- 定义一个合成析构函数synthesized destructor)。
类似拷贝构造函数和拷贝赋值运算符对于某些类合成析构函数被用来阻止该类型
的对象被销毁如果不是这种情况合成析构函数的函数体就为空

下面的代码片段等价于Sales_data的合成析构函数
class Sales_data {
public:
   // no work to do other than destroying the members, which happens automatically
    ~Sales_data() { }
   // other members as before
};
//在(空)析构函数体执行完毕后,成员会被自动销毁。
//特别的,string的析构函数会被调用,它将释放bookNo成员所用的内存。

认识到析构函数体自身并不直接销毁成员是非常重要的
成员是在析构函数体之后隐含的析构阶段中被销毁的
在整个对象销毁过程中析构函数体是作为成员销毁步骤之外的另一部分而进行的

练习

析构函数是什么合成析构函数完成什么工作什么时候会生成合成析构函数

析构函数是类的一个成员函数名字由波浪号接类名构成
它没有返回值也不接受参数合成析构函数可被用来阻止该类型的对象被销毁
当一个类未定义自己的析构函数时编译器会为它生成一个合成析构函数

练习

当一个 StrBlob 对象销毁时会发生什么一个 StrBlobPtr 对象销毁时呢

当一个 StrBlob 对象被销毁时shared_ptr 的引用计数会减少
 StrBlobPtr 对象被销毁时不影响引用计数

练习

为前面练习中的 HasPtr 类添加一个析构函数
#include <string>
class HasPtr {
public:
    HasPtr(const std::string &s = std::string()) : ps(new std::string(s)), i(0) { }
    HasPtr(const HasPtr &hp) : ps(new std::string(*hp.ps)), i(hp.i) { }
    HasPtr& operator=(const HasPtr &hp) {
        std::string *new_ps = new std::string(*hp.ps);
        delete ps;
        ps = new_ps;
        i = hp.i;
        return *this;
    }
    ~HasPtr() {
        delete ps;
    }
private:
    std::string *ps;
    int i;
};

练习

在下面的代码片段中会发生几次析构函数调用
bool fcn(const Sales_data *trans, Sales_data accum)
{
    Sales_data item1(*trans), item2(accum);
    return item1.isbn() != item2.isbn();
}

三次分别是 accumitem1和item2

练习

理解拷贝控制成员和构造函数的一个好方法的定义一个简单的类
为该类定义这些成员每个成员都打印出自己的名字
struct X {
    X() {std::cout << "X()" << std::endl;}
    X(const X&) {std::cout << "X(const X&)" << std::endl;}
}
 X 添加拷贝赋值运算符和析构函数并编写一个程序以不同的方式使用 X 的对象
将它们作为非引用参数传递动态分配它们将它们存放于容器中诸如此类
观察程序的输出直到你确认理解了什么时候会使用拷贝控制成员以及为什么会
使用它们当你观察程序输出时记住编译器可以略过对拷贝构造函数的调用

练习

#include <iostream>
#include <vector>
#include <initializer_list>
struct X {
    X() { std::cout << "X()" << std::endl; }
    X(const X&) { std::cout << "X(const X&)" << std::endl; }
    X& operator=(const X&) { std::cout << "X& operator=(const X&)" 
                                        << std::endl; return *this; }
    ~X() { std::cout << "~X()" << std::endl; }
};
void f(const X &rx, X x){
    std::vector<X> vec;
    vec.reserve(2);
    vec.push_back(rx);
    vec.push_back(x);}
int main(){
    X *px = new X;
    f(*px, *px);
    delete px;
    return 0;
}

三/五法则

有三个基本操作可以控制类的拷贝操作
- 拷贝构造函数拷贝赋值运算符和析构函数
而且在新标准下一个类还可以定义一个移动构造函数和一个移动赋值运算符

C++语言并不要求我们定义所有这些操作可以只定义其中一个或两个而不必定义所有
但是这些操作通常应该被看作一个整体
通常只需要其中一个操作而不需要定义所有操作的情况是很少见的

需要析构函数的类也需要拷贝和赋值操作

当我们决定一个类是否要定义它自己版本的拷贝控制成员时一个基本原则是
- 首先确定这个类是否需要一个析构函数
- 通常对析构函数的需求要比对拷贝构造函数或赋值运算符的需求更为明显
- 如果这个类需要一个析构函数我们几乎可以肯定它也需要一个拷贝构造函数
  和一个拷贝赋值运算符

我们在练习中用过的HasPtr类是一个好例子这个类在构造函数中分配动态内存
- 合成析构函数不会delete一个指针数据成员
- 因此此类需要定义一个析构函数来释放构造函数分配的内存
- 基本原则告诉我们HasPtr也需要一个拷贝构造函数和一个拷贝赋值运算符

需要析构函数的类也需要拷贝和赋值操作

如果我们为HasPtr定义一个析构函数
但使用合成版本的拷贝构造函数和拷贝赋值运算符考虑会发生什么
class HasPtr {
public:
    HasPtr(const std::string &s = std::string()):
        ps(new std::string(s)), i(0) { }
    ~HasPtr() { delete ps; }
    // WRONG: HasPtr needs a copy constructor and copy-assignment operator
    // other members as before
};

需要析构函数的类也需要拷贝和赋值操作

在这个版本的类定义中构造函数中分配的内存将在HasPtr对象销毁时被释放
但不幸的是我们引入了一个严重的错误
这个版本的类使用了合成的拷贝构造函数和拷贝赋值运算符
这些函数简单拷贝指针成员这意味着多个HasPtr对象可能指向相同的内存
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
}
当f返回时hp和ret都被销毁在两个对象上都会调用HasPtr的析构函数
此析构函数会delete ret和hp中的指针成员但这两个对象包含相同的指针值
此代码会导致此指针被delete两次这显然是一个错误将要发生什么是未定义的

需要析构函数的类也需要拷贝和赋值操作

此外f的调用者还会使用传递给f的对象
HasPtr p("some values");
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!

p以及q指向的内存不再有效在hp或ret!)销毁时它就被归还给系统了

如果一个类需要自定义析构函数几乎可以肯定
它也需要自定义拷贝赋值运算符和拷贝构造函数

需要拷贝操作的类也需要赋值操作,反之亦然

虽然很多类需要定义所有或是不需要定义任何拷贝控制成员
但某些类所要完成的工作只需要拷贝或赋值操作不需要析构函数
- 作为一个例子考虑一个类为每个对象分配一个独有的唯一的序号
  这个类需要一个拷贝构造函数为每个新创建的对象生成一个新的独一无二的序号
  除此之外这个拷贝构造函数从给定对象拷贝所有其他数据成员
  这个类还需要自定义拷贝赋值运算符来避免将序号赋予目的对象
  但是这个类不需要自定义析构函数
  这个例子引出了第二个基本原则
如果一个类需要一个拷贝构造函数几乎可以肯定它也需要一个拷贝赋值运算符
反之亦然
如果一个类需要一个拷贝赋值运算符几乎可以肯定它也需要一个拷贝构造函数
然而无论是需要拷贝构造函数还是需要拷贝赋值运算符都不必然意味着也需要析构函数

练习

假定 numbered 是一个类它有一个默认构造函数能为每个对象生成一个唯一的序号
保存在名为 mysn 的数据成员中
假定 numbered 使用合成的拷贝控制成员并给定如下函数
void f (numbered s) { cout << s.mysn < endl; }
则下面代码输出什么内容

numbered a, b = a, c = b;
f(a); f(b); f(c);

输出3个完全一样的数

练习

假定numbered 定义了一个拷贝构造函数能生成一个新的序列号
这会改变上一题中调用的输出结果吗如果会改变为什么新的输出结果是什么

会输出3个不同的数并且这3个数并不是abc当中的数

练习

如果 f 中的参数是 const numbered&将会怎样
这会改变输出结果吗如果会改变为什么新的输出结果是什么

会输出 abc的数

练习

分别编写前三题中所描述的 numbered  f验证你是否正确预测了输出结果
#include <iostream>
class numbered{
public:
    numbered(){mysn = unique++;}
    int mysn;
    static int unique;
};
int numbered::unique = 10;
void f(numbered s){
    std::cout << s.mysn << std::endl;
}
int main(){
    numbered a, b = a, c = b;
    f(a);
    f(b);
    f(c);
}

练习

#include <iostream>
class numbered {
public:
    numbered() {mysn = unique++;}
    numbered(const numbered& n){mysn = unique++;}
    int mysn;
    static int unique;
};
int numbered::unique = 10;
void f(numbered s) {
    std::cout << s.mysn << std::endl;
}
int main(){
    numbered a, b = a, c = b;
    f(a);
    f(b);
    f(c);
}

练习

#include <iostream>
class numbered
{
public:
    numbered() {mysn = unique++;}
    numbered(const numbered& n){mysn = unique++;}
    int mysn;
    static int unique;
};
int numbered::unique = 10;
void f(const numbered& s){
    std::cout << s.mysn << std::endl;
}
int main(){
    numbered a, b = a, c = b;
    f(a);
    f(b);
    f(c);
}

使用=default

我们可以通过将拷贝控制成员定义为=default来显式地要求编译器生成合成的版本
class Sales_data {
public:
    // copy control; use defaults
    Sales_data() = default;
    Sales_data(const Sales_data&) = default;
    Sales_data& operator=(const Sales_data &);
    ~Sales_data() = default;
    // other members as before
};
Sales_data&
Sales_data::operator=(const Sales_data&) = default;
当我们在类内用=default修饰成员的声明时合成的函数将隐式地声明为内联的
就像任何其他类内声明的成员函数一样)。
如果我们不希望合成的成员是内联函数应该只对成员的类外定义使用=default
就像对拷贝赋值运算符所做的那样

我们只能对具有合成版本的成员函数使用=default默认构造函数或拷贝控制成员

阻止拷贝

大多数类应该定义默认构造函数拷贝构造函数和拷贝赋值运算符
无论是隐式地还是显式地

虽然大多数类应该定义而且通常也的确定义了拷贝构造函数和拷贝赋值运算符
但对某些类来说这些操作没有合理的意义
在此情况下定义类时必须采用某种机制阻止拷贝或赋值
- 例如iostream类阻止了拷贝以避免多个对象写入或读取相同的IO缓冲
为了阻止拷贝看起来可能应该不定义拷贝控制成员
但是这种策略是无效的如果我们的类未定义这些操作编译器为它生成合成的版本

定义删除的函数

在新标准下我们可以通过将拷贝构造函数和拷贝赋值运算符定义为删除的函数
deleted function来阻止拷贝
删除的函数是这样一种函数我们虽然声明了它们但不能以任何方式使用它们
在函数的参数列表后面加上=delete来指出我们希望将它定义为删除的
struct NoCopy {
    NoCopy() = default;    // use the synthesized default constructor
    NoCopy(const NoCopy&) = delete;            // no copy
    NoCopy &operator=(const NoCopy&) = delete; // no assignment
    ~NoCopy() = default;   // use the synthesized destructor
    // other members
};
=delete通知编译器以及我们代码的读者),我们不希望定义这些成员

定义删除的函数

=default不同=delete必须出现在函数第一次声明的时候
这个差异与这些声明的含义在逻辑上是吻合的

一个默认的成员只影响为这个成员而生成的代码
因此=default直到编译器生成代码时才需要

而另一方面编译器需要知道一个函数是删除的以便禁止试图使用它的操作

=default的另一个不同之处是我们可以对任何函数指定=delete
我们只能对编译器可以合成的默认构造函数或拷贝控制成员使用=default)。

虽然删除函数的主要用途是禁止拷贝控制成员
但当我们希望引导函数匹配过程时删除函数有时也是有用的

析构函数不能是删除的成员

值得注意的是我们不能删除析构函数如果析构函数被删除就无法销毁此类型的对象了
对于一个删除了析构函数的类型编译器将不允许定义该类型的变量或创建该类的临时对象
而且如果一个类有某个成员的类型删除了析构函数我们也不能定义该类的变量或临时对象
因为如果一个成员的析构函数是删除的则该成员无法被销毁
而如果一个成员无法被销毁则对象整体也就无法被销毁了

对于删除了析构函数的类型虽然我们不能定义这种类型的变量或成员
但可以动态分配这种类型的对象但是不能释放这些对象
struct NoDtor {
    NoDtor() =  default;  // use the synthesized default constructor
    ~NoDtor() = delete;  // we can't destroy objects of type NoDtor
};
NoDtor nd;  // error: NoDtor destructor is deleted
NoDtor *p = new NoDtor();   // ok: but we can't delete p
delete p; // error: NoDtor destructor is deleted
对于析构函数已删除的类型不能定义该类型的变量或释放指向该类型动态分配对象的指针

合成的拷贝控制成员可能是删除的

如前所述如果我们未定义拷贝控制成员编译器会为我们定义合成的版本
类似的如果一个类未定义构造函数编译器会为其合成一个默认构造函数
对某些类来说编译器将这些合成的成员定义为删除的函数
- 如果类的某个成员的析构函数是删除的或不可访问的例如是private的),
  则类的合成析构函数被定义为删除的。· 
- 如果类的某个成员的拷贝构造函数是删除的或不可访问的
  则类的合成拷贝构造函数被定义为删除的
- 如果类的某个成员的析构函数是删除的或不可访问的
  则类合成的拷贝构造函数也被定义为删除的。· 
- 如果类的某个成员的拷贝赋值运算符是删除的或不可访问的
  或是类有一个const的或引用成员则类的合成拷贝赋值运算符被定义为删除的。· 
- 如果类的某个成员的析构函数是删除的或不可访问的或是类有一个引用成员
  它没有类内初始化器或是类有一个const成员它没有类内初始化器且其类型未
  显式定义默认构造函数则该类的默认构造函数被定义为删除的

这些规则的含义是如果一个类有数据成员不能默认构造拷贝复制或销毁
则对应的成员函数将被定义为删除的

合成的拷贝控制成员可能是删除的

一个成员有删除的或不可访问的析构函数会导致合成的默认和拷贝构造函数被定义
为删除的这看起来可能有些奇怪其原因是如果没有这条规则我们可能会创
建出无法销毁的对象

对于具有引用成员或无法默认构造的const成员的类编译器不会为其合成默认构造函数
如果一个类有const成员则它不能使用合成的拷贝赋值运算符
- 毕竟此运算符试图赋值所有成员而将一个新值赋予一个const对象是不可能的

虽然我们可以将一个新值赋予一个引用成员但这样做改变的是引用指向的对象的值
而不是引用本身如果为这样的类合成拷贝赋值运算符则赋值后左侧运算对象仍然
指向与赋值前一样的对象而不会与右侧运算对象指向相同的对象由于这种行为看起
来并不是我们所期望的因此对于有引用成员的类合成拷贝赋值运算符被定义为删除的

本质上当不可能拷贝赋值或销毁类的成员时类的合成拷贝控制成员就被定义为删除的

private拷贝控制

在新标准发布之前类是通过将其拷贝构造函数和拷贝赋值运算符声明为private的
来阻止拷贝
class PrivateCopy {
    // no access specifier; following members are private by default; 
    // copy control is private and so is inaccessible to ordinary user code
    PrivateCopy(const PrivateCopy&);
    PrivateCopy &operator=(const PrivateCopy&);
    // other members
public:
    PrivateCopy() = default; // use the synthesized default constructor
    ~PrivateCopy(); // users can define objects of this type but not copy them
};
由于析构函数是public的用户可以定义PrivateCopy类型的对象
由于拷贝构造函数和拷贝赋值运算符是private的用户代码将不能拷贝这个类型的对象
但是友元和成员函数仍旧可以拷贝对象
为了阻止友元和成员函数进行拷贝
我们将这些拷贝控制成员声明为private的但并不定义它们

声明但不定义一个成员函数是合法的对此只有一个例外后续介绍
试图访问一个未定义的成员将导致一个链接时错误
通过声明但不定义private的拷贝构造函数可以预先阻止任何拷贝该类型对象的企图
- 试图拷贝对象的用户代码将在编译阶段被标记为错误
- 成员函数或友元函数中的拷贝操作将会导致链接时错误

希望阻止拷贝的类应该使用=delete来定义它们自己的拷贝构造函数和拷贝赋值运算符
而不应该将它们声明为private的

练习

定义一个 Employee 它包含雇员的姓名和唯一的雇员证号
为这个类定义默认构造函数以及接受一个表示雇员姓名的 string 的构造函数
每个构造函数应该通过递增一个 static 数据成员来生成一个唯一的证号
#include <string>
using std::string;
class Employee{
public:
    Employee();
    Employee(const string& name);
    const int id() const { return id_; }
private:
    string name_;
    int id_;
    static int s_increment;
};
int Employee::s_increment = 0;
Employee::Employee(){id_ = s_increment++;}
Employee::Employee(const string& name){
    id_ = s_increment++;
    name_ = name;
}

练习

你的 Employee 类需要定义它自己的拷贝控制成员吗如果需要为什么
如果不需要为什么实现你认为 Employee 需要的拷贝控制成员

可以显式地阻止拷贝
#include <string>
using std::string;
class Employee {
public:
    Employee();
    Employee(const string &name);
    Employee(const Employee&) = delete;
    Employee& operator=(const Employee&) = delete;
    const int id() const { return id_; }
private:
    string name_;
    int id_;
    static int s_increment;
};

练习

解释当我们拷贝赋值或销毁 TextQuery  QueryResult 类对象时会发生什么

成员会被复制

练习

你认为 TextQuery  QueryResult 类需要定义它们自己版本的拷贝控制成员吗
如果需要为什么实现你认为这两个类需要的拷贝控制操作

合成的版本满足所有的需求因此不需要自定义拷贝控制成员

拷贝控制和资源管理

类的行为可以像一个值也可以像一个指针
- 像值对象有自己的状态副本和原对象是完全独立的
    - 标准库容器和string类
- 像指针共享状态拷贝一个这种类的对象时副本和原对象使用相同的底层数据
    - shared_ptr类

练习

假定我们希望HasPtr的行为像一个值对于对象所指向的string成员
每个对象都有一份自己的拷贝为HasPtr编写拷贝构造函数和拷贝赋值运算符
#include <string>
class HasPtr {
public:
    HasPtr(const std::string &s = std::string()) :
                            ps(new std::string(s)), i(0) { }
    HasPtr(const HasPtr &hp) : ps(new std::string(*hp.ps)), i(hp.i) { }
    HasPtr& operator=(const HasPtr &hp) {
        auto new_p = new std::string(*hp.ps);
        delete ps;
        ps = new_p;
        i = hp.i;
        return *this;
    }
    ~HasPtr() {
        delete ps;
    } 
private:
    std::string *ps;
    int i;
};

行为像值的类

//为了提供类值的行为,对于类管理的资源,每个对象都应该拥有一份自己的拷贝。
class HasPtr {
public:
    HasPtr(const std::string &s = std::string()):
        ps(new std::string(s)), i(0) { }
    // each HasPtr has its own copy of the string to which ps points
    HasPtr(const HasPtr &p):
        ps(new std::string(*p.ps)), i(p.i) { }
    HasPtr& operator=(const HasPtr &);
    ~HasPtr() { delete ps; }
private:
    std::string *ps;
    int    i;
};
//类值拷贝赋值运算符
HasPtr& HasPtr::operator=(const HasPtr &rhs){
    auto newp = new string(*rhs.ps);   // copy the underlying string
    delete ps;       // free the old memory
    ps = newp;       // copy data from rhs into this object
    i = rhs.i;
    return *this;    // return this object
}

关键概念:赋值运算符

如果将一个对象赋予它自身赋值运算符必须能正确工作。· 
大多数赋值运算符组合了析构函数和拷贝构造函数的工作
//防范自赋值操作的重要性,考虑如果赋值运算符如下编写将会发生什么
// WRONG way to write an assignment operator!
HasPtr& HasPtr::operator=(const HasPtr &rhs)
{
    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;
}
//对于一个赋值运算符来说,正确工作是非常重要的,即使是将一个对象赋予它自身,
//也要能正确工作。一个好的方法是在销毁左侧运算对象资源之前拷贝右侧运算对象。

练习

比较上一节练习中你编写的拷贝控制成员和这一节中的代码
确定你理解了你的代码和我们的代码之间的差异如果有的话)。

练习

如果本节中的HasPtr版本未定义析构函数将会发生什么
如果未定义拷贝构造函数将会发生什么


如果未定义析构函数将会发生内存泄漏
如果未定义拷贝构造函数将会拷贝指针的值指向同一个地址

练习

假定希望定义 StrBlob 的类值版本而且希望继续使用 shared_ptr
这样我们的 StrBlobPtr 类就仍能使用指向vector的 weak_ptr 
你修改后的类将需要一个拷贝的构造函数和一个拷贝赋值运算符但不需要析构函数
解释拷贝构造函数和拷贝赋值运算符必须要做什么解释为什么不需要析构函数


拷贝构造函数和拷贝赋值运算符要重新动态分配内存
因为 StrBlob 使用的是智能指针当引用计数为0时会自动释放对象
因此不需要析构函数

练习

对上一题中描述的 strBlob 编写你自己的版本
头文件
#include <vector>
#include <string>
#include <initializer_list>
#include <memory>
#include <exception>
using std::vector; using std::string;
class ConstStrBlobPtr;
class StrBlob {
public:
    using size_type = vector<string>::size_type;
    friend class ConstStrBlobPtr;
    ConstStrBlobPtr begin() const;
    ConstStrBlobPtr end() const;

练习

    StrBlob():data(std::make_shared<vector<string>>()) { }
    StrBlob(std::initializer_list<string> il):
                data(std::make_shared<vector<string>>(il)) { }
    // copy constructor
    StrBlob(const StrBlob& sb) : 
                data(std::make_shared<vector<string>>(*sb.data)) { }
    // copy-assignment operators
    StrBlob& operator=(const StrBlob& sb);
    size_type size() const { return data->size(); }
    bool empty() const { return data->empty(); }
    void push_back(const string &t) { data->push_back(t); }
    void pop_back() {
        check(0, "pop_back on empty StrBlob");
        data->pop_back();
    }
    string& front() {
        check(0, "front on empty StrBlob");
        return data->front();
    }

练习

    string& back() {
        check(0, "back on empty StrBlob");
        return data->back();
    }
    const string& front() const {
        check(0, "front on empty StrBlob");
        return data->front();
    }
    const string& back() const {
        check(0, "back on empty StrBlob");
        return data->back();
    }
private:
    void check(size_type i, const string &msg) const {
        if (i >= data->size()) throw std::out_of_range(msg);
    }
private:
    std::shared_ptr<vector<string>> data;
};

练习

class ConstStrBlobPtr {
public:
    ConstStrBlobPtr():curr(0) { }
    ConstStrBlobPtr(const StrBlob &a, size_t sz = 0):wptr(a.data), curr(sz) { } // should add const
    bool operator!=(ConstStrBlobPtr& p) { return p.curr != curr; }
    const string& deref() const { // return value should add const
        auto p = check(curr, "dereference past end");
        return (*p)[curr];
    }
    ConstStrBlobPtr& incr() {
        check(curr, "increment past end of StrBlobPtr");
        ++curr;
        return *this;
    }
private:
    std::shared_ptr<vector<string>> check(size_t i, const string &msg) const {
        auto ret = wptr.lock();
        if (!ret) throw std::runtime_error("unbound StrBlobPtr");
        if (i >= ret->size()) throw std::out_of_range(msg);
        return ret;
    }
    std::weak_ptr<vector<string>> wptr;
    size_t curr;
};

练习

主函数
#include "ex_13_26.h"
ConstStrBlobPtr StrBlob::begin() const // should add const
{
    return ConstStrBlobPtr(*this);
}
ConstStrBlobPtr StrBlob::end() const // should add const
{
    return ConstStrBlobPtr(*this, data->size());
}
StrBlob& StrBlob::operator=(const StrBlob& sb)
{
    data = std::make_shared<vector<string>>(*sb.data);
    return *this;
}
int main()
{
    return 0;
}

定义行为像指针的类

有时我们希望直接管理资源在这种情况下使用引用计数referencecount
- 除了初始化对象外每个构造函数拷贝构造函数除外还要创建一个引用计数
  用来记录有多少对象与正在创建的对象共享状态
  当我们创建一个对象时只有一个对象共享状态因此将计数器初始化为1。· 
- 拷贝构造函数不分配新的计数器而是拷贝给定对象的数据成员包括计数器
  拷贝构造函数递增共享的计数器指出给定对象的状态又被一个新用户所共享。· 
- 析构函数递减计数器指出共享状态的用户少了一个
  如果计数器变为0则析构函数释放状态。·
- 拷贝赋值运算符递增右侧运算对象的计数器递减左侧运算对象的计数器
  如果左侧运算对象的计数器变为0意味着它的共享状态没有用户了
  拷贝赋值运算符就必须销毁状态

引用计数

计数器不能直接作为HasPtr对象的成员下面的例子说明了原因
HasPtr p1("Hiya!");
HasPtr p2(p1);  // p1 and p2 point to the same string
HasPtr p3(p1);  // p1, p2, and p3 all point to the same string
解决此问题的一种方法是将计数器保存在动态内存中
当创建一个对象时我们也分配一个新的计数器
当拷贝或赋值对象时我们拷贝指向计数器的指针
使用这种方法副本和原对象都会指向相同的计数器

定义一个使用引用计数的类

class HasPtr {
public:
// constructor allocates a new string and a new counter, which it sets to 1
    HasPtr(const std::string &s = std::string()):
      ps(new std::string(s)), i(0), use(new std::size_t(1)){}
// 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
};
//在此,我们添加了一个名为use的数据成员,它记录有多少对象共享相同的string。
//接受string参数的构造函数分配新的计数器,并将其初始化为1,
//指出当前有一个用户使用本对象的string成员。

类指针的拷贝成员“篡改”引用计数

HasPtr::~HasPtr()
{
    if (--*use == 0) {    // if the reference count goes to 0
        delete ps;        // delete the string
        delete use;       // and the counter
    }
}
HasPtr& HasPtr::operator=(const HasPtr &rhs)
{
    ++*rhs.use;  // increment the use count of the right-hand operand
    if (--*use == 0) {  // then decrement this object's counter
        delete ps;      // if no other users
        delete use;     // free this object's allocated members
    }
    ps = rhs.ps;        // copy data from rhs into this object
    i = rhs.i;
    use = rhs.use;
    return *this;       // return this object
}

练习

定义你自己的使用引用计数版本的 HasPtr
#include <string>
class HasPtr {
public:
    HasPtr(const std::string &s = std::string()) : ps(new std::string(s)), i(0), use(new size_t(1)) { }
    HasPtr(const HasPtr &hp) : ps(hp.ps), i(hp.i), use(hp.use) { ++*use; }
    HasPtr& operator=(const HasPtr &rhs) {
        ++*rhs.use;
        if (--*use == 0) {
            delete ps;
            delete use;
        }
        ps = rhs.ps;
        i = rhs.i;
        use = rhs.use;
        return *this;
    }

练习

    ~HasPtr() {
        if (--*use == 0) {
            delete ps;
            delete use;
        }
    } 
private:
    std::string *ps;
    int i;
    size_t *use;
};

练习

给定下面的类为其实现一个默认构造函数和必要的拷贝控制成员
(a) 
class TreeNode {
pravite:
    std::string value;
    int count;
    TreeNode *left;
    TreeNode *right;    
};
(b)
class BinStrTree{
pravite:
    TreeNode *root;    
};

练习

头文件
#include <string>
using std::string;
class TreeNode {
public:
    TreeNode() : value(string()), count(new int(1)), 
                            left(nullptr), right(nullptr) { }
    TreeNode(const TreeNode &rhs) : value(rhs.value), count(rhs.count), 
                        left(rhs.left), right(rhs.right) { ++*count; }
    TreeNode& operator=(const TreeNode &rhs);
    ~TreeNode() {
        if (--*count == 0) {
            delete left;
            delete right;
            delete count;
        }
    }
private:
    std::string value;
    int         *count;
    TreeNode    *left;
    TreeNode    *right;
};

练习

class BinStrTree {
public:
    BinStrTree() : root(new TreeNode()) { }
    BinStrTree(const BinStrTree &bst) : root(new TreeNode(*bst.root)) { }
    BinStrTree& operator=(const BinStrTree &bst);
    ~BinStrTree() { delete root; }
private:
    TreeNode *root;
};

练习

实现和主函数
#include "ex_13_28.h"
TreeNode& TreeNode::operator=(const TreeNode &rhs)
{
    ++*rhs.count;
    if (--*count == 0) {
        delete left;
        delete right;
        delete count;
    }
    value = rhs.value;
    left = rhs.left;
    right = rhs.right;
    count = rhs.count;
    return *this;
}

练习

BinStrTree& BinStrTree::operator=(const BinStrTree &bst)
{
    TreeNode *new_root = new TreeNode(*bst.root);
    delete root;
    root = new_root;
    return *this;
}

int main()
{
    return 0;
}

交换操作

管理资源的类通常还定义一个名为swap的函数
- 重排元素顺序的算法,在需要交换两个元素时会调用swap
    - 如果一个类定义了自己的swap那么算法将使用类自定义版本
      否则算法将使用标准库定义的swap
//为了交换两个对象我们需要进行一次拷贝和两次赋值
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
//理论上,这些内存分配都是不必要的。
//我们更希望swap交换指针,而不是分配string的新副本
string *temp = v1.ps; // make a temporary copy of the pointer in v1.ps
v1.ps = v2.ps;        // assign the pointer in v2.ps to v1.ps
v2.ps = temp;         // assign the saved pointer in v1.ps to v2.ps

编写我们自己的swap函数

//在我们的类上定义一个自己版本的swap来重载swap的默认行为
class HasPtr {
    friend void swap(HasPtr&, HasPtr&);
    // ... other members as in § 13.2.1 
};
inline void swap(HasPtr &lhs, HasPtr &rhs)
{
    using std::swap;
    swap(lhs.ps, rhs.ps); // swap the pointers, not the string data
    swap(lhs.i, rhs.i);   // swap the int members
}
//与拷贝控制成员不同,swap并不是必要的。
//但是,对于分配了资源的类,定义swap可能是一种很重要的优化手段。

swap函数应该调用swap,而不是std::swap

void swap(Foo &lhs, Foo &rhs)
{
// WRONG: this function uses the library version of swap, 
// not the HasPtr version
    std::swap(lhs.h, rhs.h);
    // swap other members of type Foo
}
//希望调用为HasPtr对象定义的版本
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
}
//用swap而不是std::swap。
//如果存在类型特定的swap版本,其匹配程度会优于std中定义的版本

在赋值运算符中使用swap

拷贝并交换copy and swap
- 将左侧运算对象与右侧运算对象的一个副本进行交换
// note rhs is passed by value, which means the HasPtr copy constructor
// copies the string in the right-hand operand into rhs
HasPtr& HasPtr::operator=(HasPtr rhs)
{
    // 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;     // rhs is destroyed, which deletes the pointer in rhs
}
//自动处理了自赋值情况且天然就是异常安全的。
// - 改变左侧运算对象之前拷贝右侧运算对象,保证了自赋值的正确
// - 唯一可能抛出异常的是拷贝构造函数中的new表达式
//      - 如果真发生了异常,它也会在我们改变左侧运算对象之前发生。
//使用拷贝和交换的赋值运算符自动就是异常安全的,且能正确处理自赋值。

练习

解释 swap(HasPtr&, HasPtr&)中对 swap 的调用不会导致递归循环

这其实是3个不同的函数参数类型不一样所以不会导致递归循环

练习

为你的类值版本的 HasPtr 编写 swap 函数并测试它
为你的 swap 函数添加一个打印语句指出函数什么时候执行

#include <string>
#include <iostream>

class HasPtr {
public:
    friend void swap(HasPtr&, HasPtr&);
    HasPtr(const std::string &s = std::string()) : 
                ps(new std::string(s)), i(0) { }
    HasPtr(const HasPtr &hp) : ps(new std::string(*hp.ps)), i(hp.i) { }
    HasPtr& operator=(const HasPtr &hp) {
        auto new_p = new std::string(*hp.ps);
        delete ps;
        ps = new_p;
        i = hp.i;
        return *this;
    }

练习

    ~HasPtr() {
        delete ps;
    } 

    void show() { std::cout << *ps << std::endl; }
private:
    std::string *ps;
    int i;
};

inline
void swap(HasPtr& lhs, HasPtr& rhs)
{
    using std::swap;
    swap(lhs.ps, rhs.ps);
    swap(lhs.i, rhs.i);
    std::cout << "call swap(HasPtr& lhs, HasPtr& rhs)" << std::endl;
}

练习

为你的 HasPtr 类定义一个 < 运算符并定义一个 HasPtr  vector
为这个 vector 添加一些元素并对它执行 sort注意何时会调用 swap

#include <string>
#include <iostream>

class HasPtr 
{
public:
    friend void swap(HasPtr&, HasPtr&);
    friend bool operator<(const HasPtr &lhs, const HasPtr &rhs);

    HasPtr(const std::string &s = std::string()) 
        : ps(new std::string(s)), i(0) 
    { }

    HasPtr(const HasPtr &hp) 
        : ps(new std::string(*hp.ps)), i(hp.i) 
    { }

练习

    HasPtr& operator=(HasPtr tmp) 
    {
        this->swap(tmp);
        return *this;
    }

    ~HasPtr() 
    {
        delete ps;
    }

    void swap(HasPtr &rhs) 
    {
        using std::swap;
        swap(ps, rhs.ps);
        swap(i, rhs.i);
        std::cout << "call swap(HasPtr &rhs)" << std::endl;
    }

练习

    void show() const
    { 
        std::cout << *ps << std::endl; 
    }
private:
    std::string *ps;
    int i;
};

void swap(HasPtr& lhs, HasPtr& rhs)
{
    lhs.swap(rhs);
}

bool operator<(const HasPtr &lhs, const HasPtr &rhs)
{
    return *lhs.ps < *rhs.ps;
}

练习

int main(){
    std::vector<HasPtr> v={HasPtr("abc"),HasPtr("def"),HasPtr("bcd")};
    for(auto &a:v){
        a.show();
    }
    std::cout<<std::endl;
    sort(v.begin(),v.end());

    for(auto &a:v){
        a.show();
    }
    return 0;

}

练习

类指针的 HasPtr 版本会从 swap 函数收益吗
如果会得到了什么益处如果不是为什么

不会类值的版本利用swap交换指针不用进行内存分配因此得到了性能上的提升
类指针的版本本来就不用进行内存分配所以不会得到性能提升

拷贝控制示例

定义自己的拷贝控制成员的原因
- 分配资源
- 簿记工作或其他操作

拷贝控制示例

两个类命名为Message和Folder分别表示电子邮件消息和消息目录
- 每个Message都会保存一个它所在Folder的指针的set
- 每个Folder都保存一个它包含的Message的指针的set

析构函数和拷贝赋值运算符都必须从包含一条Message的所有Folder中删除它
拷贝构造函数和拷贝赋值运算符都要将一个Message添加到给定的一组Folder中
类设计

拷贝控制示例

拷贝赋值运算符通常执行拷贝构造函数和析构函数中也要做的工作
这种情况下公共的工作应该放在private的工具函数中完成

Message类

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&);            // copy constructor
    Message& operator=(const Message&); // copy assignment
    ~Message();                         // destructor
    // add/remove this Message from the specified Folder's set of messages
    void save(Folder&);
    void remove(Folder&);
private:
    std::string contents;      // actual message text
    std::set<Folder*> folders; // Folders that have this Message
    // utility functions used by copy constructor, assignment, and destructor
    // 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();
};

save和remove成员

void Message::save(Folder &f)
{
    folders.insert(&f); // add the given Folder to our list of Folders
    f.addMsg(this);     // add this Message to f's set of Messages
}
void Message::remove(Folder &f)
{
    folders.erase(&f); // take the given Folder out of our list of Folders
    f.remMsg(this);    // remove this Message to f's set of Messages
}

Message类的拷贝控制成员

// add this Message to Folders that point to m
void Message::add_to_Folders(const Message &m)
{
    for (auto f : m.folders) // for each Folder that holds m
        f->addMsg(this); // add a pointer to this Message to that Folder
}
//The Message copy constructor copies the data members of the given object
Message::Message(const Message &m):
    contents(m.contents), folders(m.folders)
{
    add_to_Folders(m); // add this Message to the Folders that point to m
}

Message的析构函数

// remove this Message from the corresponding Folders
void Message::remove_from_Folders()
{
    for (auto f : folders) // for each pointer in folders
        f->remMsg(this);   // remove this Message from that Folder
}
Message::~Message()
{
    remove_from_Folders();
}

Message的拷贝赋值运算符

//先从左侧运算对象的folders中删除此Message的指针,
//然后再将指针添加到右侧运算对象的folders中,从而实现了自赋值的正确处理
Message& Message::operator=(const Message &rhs)
{
    // handle self-assignment by removing pointers before inserting them
    remove_from_Folders();   // update existing Folders
    contents = rhs.contents; // copy message contents from rhs
    folders = rhs.folders;   // copy Folder pointers from rhs
    add_to_Folders(rhs);     // add this Message to those Folders
    return *this;
}

Message的swap函数

//定义一个Message特定版本的swap,
//我们可以避免对contents和folders成员进行不必要的拷贝
//swap函数必须管理指向被交换Message的Folder指针
//通过两遍扫描folders中每个成员来正确处理Folder指针
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->remMsg(&lhs);
    for (auto f: rhs.folders)
        f->remMsg(&rhs);
    // swap the contents and Folder pointer sets
    swap(lhs.folders, rhs.folders);     // uses swap(set&, set&)
    swap(lhs.contents, rhs.contents);   // swap(string&, string&)
    // add pointers to each Message to their (new) respective Folders
    for (auto f: lhs.folders)
        f->addMsg(&lhs);
    for (auto f: rhs.folders)
        f->addMsg(&rhs);
}

练习

为什么Message的成员save和remove的参数是一个 Folder&
为什么我们不能将参数定义为 Folder 或是 const Folder

因为 save  remove 操作需要更新指定 Folder

练习

编写本节所描述的 Message

头文件
#include <string>
#include <set>

class Folder;

class Message {
    friend void swap(Message &, Message &);
    friend class Folder;
public:
    explicit Message(const std::string &str = ""):contents(str) { }
    Message(const Message&);
    Message& operator=(const Message&);
    ~Message();
    void save(Folder&);
    void remove(Folder&);

    void print_debug();

练习

private:
    std::string contents;
    std::set<Folder*> folders;

    void add_to_Folders(const Message&);
    void remove_from_Folders();

    void addFldr(Folder *f) { folders.insert(f); }
    void remFldr(Folder *f) { folders.erase(f); }
};

void swap(Message&, Message&);

练习

class Folder {
    friend void swap(Folder &, Folder &);
    friend class Message;
public:
    Folder() = default;
    Folder(const Folder &);
    Folder& operator=(const Folder &);
    ~Folder();

    void print_debug();

private:
    std::set<Message*> msgs;

    void add_to_Message(const Folder&);
    void remove_from_Message();

    void addMsg(Message *m) { msgs.insert(m); }
    void remMsg(Message *m) { msgs.erase(m); }
};

void swap(Folder &, Folder &);

练习

实现和主函数

#include "ex13_34_36_37.h"
#include <iostream>

void swap(Message &lhs, Message &rhs) 
{
    using std::swap;
    lhs.remove_from_Folders(); // Use existing member function to avoid duplicate code.
    rhs.remove_from_Folders(); // Use existing member function to avoid duplicate code.

    swap(lhs.folders, rhs.folders);
    swap(lhs.contents, rhs.contents);

    lhs.add_to_Folders(lhs); // Use existing member function to avoid duplicate code.
    rhs.add_to_Folders(rhs); // Use existing member function to avoid duplicate code.
}

练习

// Message Implementation

void Message::save(Folder &f) 
{
    addFldr(&f); // Use existing member function to avoid duplicate code.
    f.addMsg(this);
}

void Message::remove(Folder &f) 
{
    remFldr(&f); // Use existing member function to avoid duplicate code.
    f.remMsg(this);
}

void Message::add_to_Folders(const Message &m) 
{
    for (auto f : m.folders)
        f->addMsg(this);
}

练习

Message::Message(const Message &m) 
    : contents(m.contents), folders(m.folders) 
{
    add_to_Folders(m);
}

void Message::remove_from_Folders() 
{
    for (auto f : folders)
        f->remMsg(this);
}

练习

Message::~Message() 
{ 
    remove_from_Folders(); 
}

Message &Message::operator=(const Message &rhs) 
{
    remove_from_Folders();
    contents = rhs.contents;
    folders = rhs.folders;
    add_to_Folders(rhs);
    return *this;
}

void Message::print_debug() 
{ 
    std::cout << contents << std::endl; 
}

练习

// Folder Implementation

void swap(Folder &lhs, Folder &rhs) 
{
    using std::swap;
    lhs.remove_from_Message();
    rhs.remove_from_Message();

    swap(lhs.msgs, rhs.msgs);

    lhs.add_to_Message(lhs);
    rhs.add_to_Message(rhs);
}

void Folder::add_to_Message(const Folder &f) 
{
    for (auto m : f.msgs)
        m->addFldr(this);
}

练习

Folder::Folder(const Folder &f) 
    : msgs(f.msgs) 
{ 
    add_to_Message(f); 
}

void Folder::remove_from_Message() 
{
    for (auto m : msgs)
        m->remFldr(this);
}

Folder::~Folder() 
{ 
    remove_from_Message(); 
}

Folder &Folder::operator=(const Folder &rhs) 
{
    remove_from_Message();
    msgs = rhs.msgs;
    add_to_Message(rhs);
    return *this;
}

练习

void Folder::print_debug() 
{
    for (auto m : msgs)
        std::cout << m->contents << " ";
    std::cout << std::endl;
}

int main() 
{ 
    return 0;
}

练习

 如果Message 使用合成的拷贝控制成员将会发生什么

在赋值后一些已存在的 Folders 将会与 Message 不同步

练习

设计并实现对应的 Folder 
此类应该保存一个指向 Folder 中包含  Message  set

参考之前

练习

 Message类添加成员实现向 folders添加和删除一个给定的 Folder*
这两个成员类似Folder 类的 addMsg  remMsg 操作

参考之前

练习

我们并未使用拷贝交换方式来设计 Message 的赋值运算符你认为其原因是什么

对于动态分配内存的例子来说拷贝交换方式是一种简洁的设计
而这里的 Message 类并不需要动态分配内存用拷贝交换方式只会增加实现的复杂度

动态内存管理类

某些类需要在运行时分配可变大小的内存空间
- 这种类通常可以使用标准库容器来保存它们的数据
而某些类需要自己进行内存分配
- 这些类一般来说必须定义自己的拷贝控制成员来管理所分配的内存

StrVec类的设计

//实现标准库vector类的一个简化版本StrVec
//使用一个allocator来获得原始内存
//- 在需要添加新元素时用allocator的construct成员在原始内存中创建对象。
//- 当我们需要删除一个元素时,我们将使用destroy成员来销毁元素。
每个StrVec有三个指针成员指向其元素所使用的内存:· 
- elements指向分配的内存中的首元素· 
- first_free指向最后一个实际元素之后的位置· 
- cap指向分配的内存末尾之后的位置

0   1   2   3   4   未构造元素
^                ^              ^
elements         first_free     cap

StrVec类的设计

StrVec还有一个名为alloc的静态成员其类型为allocator<string>
- alloc成员会分配StrVec使用的内存
我们的类还有4个工具函数:· 
- alloc_n_copy会分配内存并拷贝一个给定范围中的元素。· 
- free会销毁构造的元素并释放内存。· 
- chk_n_alloc保证StrVec至少有容纳一个新元素的空间
  如果没有空间添加新元素chk_n_alloc会调用reallocate来分配更多内存。· 
- reallocate在内存用完时为StrVec分配新内存

StrVec类定义

// simplified implementation of the memory allocation strategy for a vector-like class
class StrVec {
public:
    StrVec(): // the allocator member is default initialized
      elements(nullptr), first_free(nullptr), cap(nullptr) {}
    StrVec(const StrVec&);            // copy constructor
    StrVec &operator=(const StrVec&); // copy assignment
    ~StrVec();                        // destructor
    void push_back(const std::string&);  // copy the element
    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; }
    // ...

StrVec类定义

private:
    std::allocator<std::string> alloc; // allocates the elements
    // used by the functions that add elements to the StrVec
    void chk_n_alloc()
        { if (size() == capacity()) reallocate(); }
    // utilities used by the copy constructor, assignment operator, and destructor
    std::pair<std::string*, std::string*> alloc_n_copy
        (const std::string*, const std::string*);
    void free();             // destroy the elements and free the space
    void reallocate();       // get more space and copy the existing elements
    std::string *elements;   // pointer to the first element in the array
    std::string *first_free; // pointer to the first free element in the array
    std::string *cap;        // pointer to one past the end of the array
};

使用construct

函数push_back调用chk_n_alloc确保有空间容纳新元素
void StrVec::push_back(const string& s)
{
    chk_n_alloc(); // ensure that there is room for another element
    // construct a copy of s in the element to which first_free points
    alloc.construct(first_free++, s);
}

alloc_n_copy成员

//alloc_n_copy成员会分配足够的内存来保存给定范围的元素,
//并将这些元素拷贝到新分配的内存中
pair<string*, string*>
StrVec::alloc_n_copy(const string *b, const string *e)
{
    // 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, e, data)};
}

free成员

//首先destroy元素,然后释放StrVec自己分配的内存空间。
void StrVec::free()
{
// may not pass deallocate a 0 pointer; if elements is 0, there's no work to do
    if (elements) {
        // destroy the old elements in reverse order
        for (auto p = first_free; p != elements; /* empty */)
            alloc.destroy(--p);
        alloc.deallocate(elements, cap - elements);
    }
}
//destroy函数会运行string的析构函数
//deallocate来释放本StrVec对象分配的内存空间

拷贝控制成员

//拷贝构造函数调用alloc_n_copy:
StrVec::StrVec(const StrVec &s)
{
    // call alloc_n_copy to allocate exactly as many elements as in s
    auto newdata = alloc_n_copy(s.begin(), s.end());
    elements = newdata.first;
    first_free = cap = newdata.second;
}
//析构函数调用free
StrVec::~StrVec() { free(); }
//拷贝赋值运算符在释放已有元素之前调用alloc_n_copy
StrVec &StrVec::operator=(const StrVec &rhs)
{
    // call alloc_n_copy to allocate exactly as many elements as in rhs
    auto data = alloc_n_copy(rhs.begin(), rhs.end());
    free();
    elements = data.first;
    first_free = cap = data.second;
    return *this;
}

在重新分配内存的过程中移动而不是拷贝元素

reallocate成员函数应该做什么
- 为一个新的更大的string数组分配内存· 
- 在内存空间的前一部分构造对象保存现有元素· 
- 销毁原内存空间中的元素并释放这块内存
在重新分配内存空间时如果我们能避免分配和释放string的额外开销
StrVec的性能会好得多

移动构造函数和std::move

通过使用新标准库引入的两种机制我们就可以避免string的拷贝
- 有一些标准库类包括string都定义了所谓的移动构造函数”。
  移动构造函数通常是将资源从给定对象移动而不是拷贝到正在创建的对象
  标准库保证移后源”(moved-fromstring仍然保持一个有效的可析构的状态
  可以想象每个string都有一个指向char数组的指针可以假定string的移动构造函数
  进行了指针的拷贝而不是为字符分配内存空间然后拷贝字符
- 第二个机制是一个名为move的标准库函数它定义在utility头文件中
    - 当reallocate在新内存中构造string时
      它必须调用move来表示希望使用string的移动构造函数
      如果它漏掉了move调用将会使用string的拷贝构造函数
    - 使用move时直接调用std::move而不是move

reallocate成员

//每次重新分配内存时都会将StrVec的容量加倍。
//如果StrVec为空,我们将分配容纳一个元素的空间:
void StrVec::reallocate()
{
     // 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;  // points to the next free position in the new array
     auto elem = elements; // points to the next element in the old array
     for (size_t i = 0; i != size(); ++i)
         alloc.construct(dest++, std::move(*elem++));
     free();  // free the old space once we've moved the elements
     // update our data structure to point to the new elements
     elements = newdata;
     first_free = dest;
     cap = elements + newcapacity;
}
//由于我们使用了移动构造函数,这些string管理的内存将不会被拷贝。
//相反,我们构造的每个string都会从elem指向的string那里接管内存的所有权。

练习

编写你自己版本的 StrVec包括自己版本的 reservecapacity 和resize
头文件
#include <memory>
#include <string>
// 类vector类内存分配策略的简化实现
class StrVec
{
public:
    StrVec() : elements(nullptr), first_free(nullptr), cap(nullptr) { }
    StrVec(const StrVec&);  // 拷贝构造函数
    StrVec& operator=(const StrVec&);  // 拷贝赋值运算符
    ~StrVec();  // 析构函数

    void push_back(const std::string&);  // 添加元素时拷贝元素
    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; }

    void reserve(size_t new_cap);
    void resize(size_t count);
    void resize(size_t count, const std::string&);

练习

private:
    // 工具函数,被拷贝构造函数、赋值运算符和析构函数所使用
    std::pair<std::string*, std::string*> alloc_n_copy(const std::string*, const std::string*);
    // 销毁元素并释放内存
    void free();
    // 工具函数,被添加元素的函数使用
    void chk_n_alloc() { if (size() == capacity()) reallocate(); }
    //获得更多内存并拷贝已有元素
    void reallocate();
    void alloc_n_move(size_t new_cap);

private:
    std::string *elements;  // 指向数组首元素的指针
    std::string *first_free;  // 指向数组第一个空闲元素的指针
    std::string *cap;  // 指向数组第一个空闲元素的指针
    std::allocator<std::string> alloc;  // 分配元素
};

练习

实现和主函数
#include "ex_13_39.h"

void StrVec::push_back(const std::string &s)
{
    chk_n_alloc();
    alloc.construct(first_free++, s);
}

// 分配足够的内存来保存给定范围的元素,并将这些元素拷贝到新分配的内存中
std::pair<std::string*, std::string*>
StrVec::alloc_n_copy(const std::string *b, const std::string *e)
{
    // 分配空间保存给定范围中的元素
    auto data = alloc.allocate(e - b);
    // 初始化并返回一个pair,该pair由data和uninitialized_copy的返回值构成
    return{ data, std::uninitialized_copy(b, e, data) };
}

练习

void StrVec::free()
{
    // 不能传递给deallocate一个空指针,如果elements为0,函数什么也不做
    if (elements) {
        // 逆序销毁元素
        for (auto p = first_free; p != elements;)
            alloc.destroy(--p);
        alloc.deallocate(elements, cap - elements);
    }
}

StrVec::StrVec(const StrVec &rhs)
{
    // 调用alloc_n_copy分配空间以容纳与rhs中一样多的元素
    auto newdata = alloc_n_copy(rhs.begin(), rhs.end());
    elements = newdata.first;
    first_free = cap = newdata.second;
}

StrVec::~StrVec()
{
    free();
}

练习

StrVec& StrVec::operator = (const StrVec &rhs)
{
    // 调用alloc_n_copy分配空间以容纳与rhs中一样多的元素
    auto data = alloc_n_copy(rhs.begin(), rhs.end());
    free();
    elements = data.first;
    first_free = cap = data.second;
    return *this;
}

void StrVec::alloc_n_move(size_t new_cap)
{
    auto newdata = alloc.allocate(new_cap);
    auto dest = newdata;
    auto elem = elements;
    for (size_t i = 0; i != size(); ++i)
        alloc.construct(dest++, std::move(*elem++));
    free();
    elements = newdata;
    first_free = dest;
    cap = elements + new_cap;
}

练习

void StrVec::reallocate()
{
    auto newcapacity = size() ? 2 * size() : 1;
    alloc_n_move(newcapacity);
}

void StrVec::reserve(size_t new_cap)
{
    if (new_cap <= capacity()) return;
    alloc_n_move(new_cap);
}

void StrVec::resize(size_t count)
{
    resize(count, std::string());
}

练习

void StrVec::resize(size_t count, const std::string &s)
{
    if (count > size()) {
        if (count > capacity()) reserve(count * 2);
        for (size_t i = size(); i != count; ++i)
            alloc.construct(first_free++, s);
    }
    else if (count < size()) {
        while (first_free != elements + count)
            alloc.destroy(--first_free);
    }
}

int main()
{
    return 0;
}

练习

为你的 StrVec 类添加一个构造函数它接受一个 initializer_list<string> 参数
头文件
StrVec(std::initializer_list<std::string>);
实现
void StrVec::range_initialize(const std::string *first, 
                                            const std::string *last)
{
    auto newdata = alloc_n_copy(first, last);
    elements = newdata.first;
    first_free = cap = newdata.second;
}

StrVec::StrVec(std::initializer_list<std::string> il)
{
    range_initialize(il.begin(), il.end());
}

练习

 push_back 我们为什么在 construct 调用中使用后置递增运算
如果使用前置递增运算的话会发生什么

会出现 unconstructed

练习

在你的 TextQuery  QueryResult 类中用你的 StrVec 类代替vector<string>
以此来测试你的 StrVec 


练习

重写 free 成员 for_each  lambda 来代替 for 循环 destroy 元素
你更倾向于哪种实现为什么

for_each(elements, first_free,
            [this](std::string &rhs){ alloc.destroy(&rhs); });


更倾向于函数式写法

练习

编写标准库 string 类的简化版本命名为 String
你的类应该至少有一个默认构造函数和一个接受 C 风格字符串指针参数的构造函数
使用 allocator 为你的 String类分配所需内存
头文件
#include <memory>
class String{
public:
    String() : String("") { }
    String(const char *);
    String(const String&);
    String& operator=(const String&);
    ~String();
    const char *c_str() const { return elements; }
    size_t size() const { return end - elements; }
    size_t length() const { return end - elements - 1; }
private:
    std::pair<char*, char*> alloc_n_copy(const char*, const char*);
    void range_initializer(const char*, const char*);
    void free();
private:
    char *elements;
    char *end;
    std::allocator<char> alloc;
};

练习

实现
#include "ex_13_44_47.h"
#include <algorithm>
#include <iostream>
std::pair<char*, char*>
String::alloc_n_copy(const char *b, const char *e)
{
    auto str = alloc.allocate(e - b);
    return{ str, std::uninitialized_copy(b, e, str) };
}

void String::range_initializer(const char *first, const char *last)
{
    auto newstr = alloc_n_copy(first, last);
    elements = newstr.first;
    end = newstr.second;
}

String::String(const char *s)
{
    char *sl = const_cast<char*>(s);
    while (*sl)
        ++sl;
    range_initializer(s, ++sl);
}

练习

String::String(const String& rhs)
{
    range_initializer(rhs.elements, rhs.end);
    std::cout << "copy constructor" << std::endl;
}

void String::free()
{
    if (elements) {
        std::for_each(elements, end, [this](char &c){ alloc.destroy(&c); });
        alloc.deallocate(elements, end - elements);
    }
}

String::~String()
{
    free();
}

练习

String& String::operator = (const String &rhs)
{
    auto newstr = alloc_n_copy(rhs.elements, rhs.end);
    free();
    elements = newstr.first;
    end = newstr.second;
    std::cout << "copy-assignment" << std::endl;
    return *this;
}

练习

测试
#include "ex13_44_47.h"
#include <vector>
#include <iostream>
void foo(String x)
{
    std::cout << x.c_str() << std::endl;
}

void bar(const String& x)
{
    std::cout << x.c_str() << std::endl;
}

String baz()
{
    String ret("world");
    return ret;
}

练习

int main()
{
    char text[] = "world";

    String s0;
    String s1("hello");
    String s2(s0);
    String s3 = s1;
    String s4(text);
    s2 = s1;

    foo(s1);
    bar(s1);
    foo("temporary");
    bar("temporary");
    String s5 = baz();

练习

    std::vector<String> svec;
    svec.reserve(8);
    svec.push_back(s0);
    svec.push_back(s1);
    svec.push_back(s2);
    svec.push_back(s3);
    svec.push_back(s4);
    svec.push_back(s5);
    svec.push_back(baz());
    svec.push_back("good job");

    for (const auto &s : svec) {
        std::cout << s.c_str() << std::endl;
    }
}

对象移动

移动而非拷贝对象会大幅度提升性能
- 某些情况下对象拷贝后就立即被销毁了
某些类型的对象不能拷贝但可以移动
- IO类或unique_ptr这样的类包含不能被共享的资源如指针或IO缓冲

在新标准中我们可以用容器保存不可拷贝的类型只要它们可以被移动即可

标准库容器string和shared_ptr类既支持移动也支持拷贝
IO类和unique_ptr类可以移动但不能拷贝

右值引用

为了支持移动操作新标准引入了一种新的引用类型——右值引用rvalue reference)。
- 所谓右值引用就是必须绑定到右值的引用
- 通过&&而不是&来获得右值引用
- 右值引用有一个重要的性质——只能绑定到一个将要销毁的对象
  因此可以自由地将一个右值引用的资源移动到另一个对象中

左值和右值是表达式的属性
- 一个左值表达式表示的是一个对象的身份
- 一个右值表达式表示的是对象的值

右值引用

对于常规引用为了与右值引用区分开来我们可以称之为左值引用lvalue reference))
- 我们不能将其绑定到要求转换的表达式字面常量或是返回右值的表达式
右值引用有着完全相反的绑定特性
- 我们可以将一个右值引用绑定到这类表达式上
  但不能将一个右值引用直接绑定到一个左值上

int i = 42;
int &r = i;             // ok: r refers to i
int &&rr = i;           // error: cannot bind an rvalue reference to an lvalue
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 &&rr2 = i * 42;     // ok: bind rr2 to the result of the multiplication

右值引用

我们可以将一个左值引用绑定到返回左值表达式的结果上
- 返回左值引用的函数连同赋值下标解引用和前置递增/递减运算符
  都是返回左值的表达式的例子

我们不能将一个左值引用绑定到生成右值的表达式上
- 返回非引用类型的函数连同算术关系位以及后置递增/递减运算符都生成右值
但我们可以将一个const的左值引用或者一个右值引用绑定到这类表达式上

左值持久;右值短暂

左值有持久的状态
而右值要么是字面常量要么是在表达式求值过程中创建的临时对象

右值引用只能绑定到临时对象我们得知· 
- 所引用的对象将要被销毁· 
- 该对象没有其他用户
这两个特性意味着使用右值引用的代码可以自由地接管所引用的对象的资源

右值引用指向将要被销毁的对象我们可以从绑定到右值引用的对象窃取状态

变量是左值

变量可以看作只有一个运算对象而没有运算符的表达式
变量表达式都是左值

不能将一个右值引用绑定到一个右值引用类型的变量上
int &&rr1 = 42;    // ok: literals are rvalues
int &&rr2 = rr1;   // error: the expression rr1 is an lvalue!
//右值表示临时对象
//变量是持久的,直至离开作用域时才被销毁。
变量是左值因此我们不能将一个右值引用直接绑定到一个变量上
即使这个变量是右值引用类型也不行

标准库move函数

虽然不能将一个右值引用直接绑定到一个左值上
但我们可以显式地将一个左值转换为对应的右值引用类型
- 还可以通过调用一个名为move的新标准库函数来获得绑定到左值上的右值引用
  此函数定义在头文件utility中
- move函数返回给定对象的右值引用
int &&rr3 = std::move(rr1);   // ok
//move调用告诉编译器:我们有一个左值,但我们希望像一个右值一样处理它
//调用move就意味着承诺:除了对rr1赋值或销毁它外,我们将不再使用它。
//在调用move之后,我们不能对移后源对象的值做任何假设。
我们可以销毁一个移后源对象也可以赋予它新值但不能使用一个移后源对象的值

使用move的代码应该使用std::move而不是move这样做可以避免潜在的名字冲突

练习

解释左值引用和右值引用的区别

* 常规引用被称为左值引用
* 绑定到右值的引用被称为右值引用

练习

什么类型的引用可以绑定到下面的初始化器上

int f();
vector<int> vi(100);
int? r1 = f();
int? r2 = vi[0];
int? r3 = r1;
int? r4 = vi[0] * f();


int f();
vector<int> vi(100);
int&& r1 = f();
int& r2 = vi[0];
int& r3 = r1;
int&& r4 = vi[0] * f();

练习

对你在练习中定义的 String类
为它的拷贝构造函数和拷贝赋值运算符添加一条语句在每次函数执行时打印一条信息

参考之前

练习

定义一个vector<String> 并在其上多次调用 push_back运行你的程序
并观察 String 被拷贝了多少次
#include "ex_13_44_47.h"
#include <vector>
#include <iostream>

void foo(String x)
{
    std::cout << x.c_str() << std::endl;
}

void bar(const String& x)
{
    std::cout << x.c_str() << std::endl;
}

String baz()
{
    String ret("world");
    return ret;
}

练习

int main()
{
    char text[] = "world";

    String s0;
    String s1("hello");
    String s2(s0);
    String s3 = s1;
    String s4(text);
    s2 = s1;

    foo(s1);
    bar(s1);
    foo("temporary");
    bar("temporary");
    String s5 = baz();

练习

    std::vector<String> svec;
    svec.reserve(8);
    svec.push_back(s0);
    svec.push_back(s1);
    svec.push_back(s2);
    svec.push_back(s3);
    svec.push_back(s4);
    svec.push_back(s5);
    svec.push_back(baz());
    svec.push_back("good job");

    for (const auto &s : svec) {
        std::cout << s.c_str() << std::endl;
    }
}

移动构造函数和移动赋值运算符

为了让我们自己的类型支持移动操作需要为其定义移动构造函数和移动赋值运算符
这两个成员类似对应的拷贝操作但它们从给定对象窃取资源而不是拷贝资源

类似拷贝构造函数移动构造函数的第一个参数是该类类型的一个引用
不同于拷贝构造函数的是这个引用参数在移动构造函数中是一个右值引用
与拷贝构造函数一样任何额外的参数都必须有默认实参

移动构造函数
- 完成资源移动
- 确保移后销毁源对象是无害的
- 资源完成移动源对象必须不再指向被移动的资源,所有权已经归属新创建的对象

移动构造函数和移动赋值运算符

StrVec::StrVec(StrVec &&s) noexcept   // move won't throw any exceptions
  // member initializers take over the resources in s
  : elements(s.elements),first_free(s.first_free), cap(s.cap)
{
    // leave s in a state in which it is safe to run the destructor
    s.elements = s.first_free = s.cap = nullptr;
}
//noexcept通知标准库我们的构造函数不抛出任何异常
与拷贝构造函数不同移动构造函数不分配任何新内存
它接管给定的StrVec中的内存在接管内存之后它将给定对象中的指针都置为nullptr
这样就完成了从给定对象的移动操作此对象将继续存在
最终移后源对象会被销毁意味着将在其上运行析构函数

移动操作、标准库容器和异常

由于移动操作窃取资源它通常不分配任何资源移动操作通常不会抛出任何异常
当编写一个不抛出异常的移动操作时我们应该将此事通知标准库

noexcept是我们承诺一个函数不抛出异常的一种方法
我们在一个函数的参数列表后指定noexcept
在一个构造函数中noexcept出现在参数列表和初始化列表开始的冒号之间
class StrVec {
public:
    StrVec(StrVec&&) noexcept;     // move constructor
    // other members as before
};
StrVec::StrVec(StrVec &&s) noexcept : /* member initializers */
{ /* constructor body   */ }
//不抛出异常的移动构造函数和移动赋值运算符必须标记为noexcept。

移动操作、标准库容器和异常

除非vector知道元素类型的移动构造函数不会抛出异常否则在重新分配内存的过程中
- 它就必须使用拷贝构造函数而不是移动构造函数
    - 移动构造函数在移动了部分而不是全部元素后抛出了一个异常
      旧空间中的移动源元素已经被改变了而新空间中未构造的元素可能尚不存在
      vector将不能满足自身保持不变的要求
    - 使用了拷贝构造函数且发生了异常
      在新内存中构造元素时旧元素保持不变
      vector可以释放新分配的但还未成功构造的内存并返回
      vector原有的元素仍然存在

如果希望在vector重新分配内存这类情况下对我们自定义类型的对象进行移动而不是拷贝
就必须显式地告诉标准库我们的移动构造函数可以安全使用
我们通过将移动构造函数及移动赋值运算符标记为noexcept来做到这一点

移动赋值运算符

移动赋值运算符执行与析构函数和移动构造函数相同的工作
StrVec &StrVec::operator=(StrVec &&rhs) noexcept
{
    // direct test for self-assignment 正确处理自赋值:
    if (this != &rhs) {
        free();                  // free existing elements
        elements = rhs.elements; // take over resources from rhs
        first_free = rhs.first_free;
        cap = rhs.cap;
        // leave rhs in a destructible state
        rhs.elements = rhs.first_free = rhs.cap = nullptr;
    }
    return *this;
}
//此右值可能是move调用的返回结果。
//与其他任何赋值运算符一样,关键点是我们不能在使用右侧运算对象的资源之前
//就释放左侧运算对象的资源(可能是相同的资源)。

移后源对象必须可析构

从一个对象移动数据并不会销毁此对象但有时在移动操作完成后源对象会被销毁
当我们编写一个移动操作时必须确保移后源对象进入一个可析构的状态
- StrVec的移动操作满足这一要求
  这是通过将移后源对象的指针成员置为nullptr来实现的
除了将移后源对象置为析构安全的状态之外移动操作还必须保证对象仍然是有效的
- 指可以安全地为其赋予新值或者可以安全地使用而不依赖其当前值
- 移动操作对移后源对象中留下的值没有任何要求
  我们的程序不应该依赖于移后源对象中的数据

在移动操作之后移后源对象必须保持有效的可析构的状态
但是用户不能对其值进行任何假设

合成的移动操作

编译器也会合成移动构造函数和移动赋值运算符
- 与拷贝操作不同编译器根本不会为某些类合成移动操作
    - 如果一个类定义了自己的拷贝构造函数拷贝赋值运算符或者析构函数
      编译器就不会为它合成移动构造函数和移动赋值运算符了
    - 因此某些类就没有移动构造函数或移动赋值运算符
    - 如果一个类没有移动操作通过正常的函数匹配
      类会使用对应的拷贝操作来代替移动操作

合成的移动操作

只有当一个类没有定义任何自己版本的拷贝控制成员
且类的每个非static数据成员都可以移动时
编译器才会为它合成移动构造函数或移动赋值运算符
- 编译器可以移动内置类型的成员
- 如果一个成员是类类型且该类有对应的移动操作编译器也能移动这个成员

// 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
};
只有当一个类没有定义任何自己版本的拷贝控制成员
且它的所有数据成员都能移动构造或移动赋值时
编译器才会为它合成移动构造函数或移动赋值运算符

合成的移动操作

与拷贝操作不同移动操作永远不会隐式定义为删除的函数
- 但是如果我们显式地要求编译器生成=default的移动操作
  且编译器不能移动所有成员则编译器会将移动操作定义为删除的函数
除了一个重要例外什么时候将合成的移动操作定义为删除的函数遵循与定义删除
的合成拷贝操作类似的原则:
- 与拷贝构造函数不同移动构造函数被定义为删除的函数的条件是
  有类成员定义了自己的拷贝构造函数且未定义移动构造函数
  或者是有类成员未定义自己的拷贝构造函数且编译器不能为其合成移动构造函数
  移动赋值运算符的情况类似
- 如果有类成员的移动构造函数或移动赋值运算符被定义为删除的或是不可访问的
  则类的移动构造函数或移动赋值运算符被定义为删除的
- 类似拷贝构造函数如果类的析构函数被定义为删除的或不可访问的
  则类的移动构造函数被定义为删除的
- 类似拷贝赋值运算符如果有类成员是const的或是引用
  则类的移动赋值运算符被定义为删除的

合成的移动操作

例如假定Y是一个类它定义了自己的拷贝构造函数但未定义自己的移动构造函数
struct hasY {
    hasY() = default;
    hasY(hasY&&) = default;
    Y mem; // hasY will have a deleted move constructor
};
hasY hy, hy2 = std::move(hy); // error: move constructor is deleted
//编译器可以拷贝类型为Y的对象,但不能移动它们。
//类hasY显式地要求一个移动构造函数,但编译器无法为其生成。
//因此,hasY会有一个删除的移动构造函数。
//如果hasY忽略了移动构造函数的声明,则编译器根本不能为它合成一个。
//如果移动操作可能被定义为删除的函数,编译器就不会合成它们。

如果类定义了一个移动构造函数和/或一个移动赋值运算符
则该类的合成拷贝构造函数和拷贝赋值运算符会被定义为删除的

定义了一个移动构造函数或移动赋值运算符的类必须也定义自己的拷贝操作
否则这些成员默认地被定义为删除的

移动右值,拷贝左值……

如果一个类既有移动构造函数也有拷贝构造函数
编译器使用普通的函数匹配规则来确定使用哪个构造函数
赋值操作的情况类似
//在我们的StrVec类中,拷贝构造函数接受一个const StrVec的引用。
//因此,它可以用于任何可以转换为StrVec的类型。
//而移动构造函数接受一个StrVec&&,因此只能用于实参是(非static)右值的情形:
StrVec v1, v2;
v1 = v2;                  // v2 is an lvalue; copy assignment
StrVec getVec(istream &); // getVec returns an rvalue
v2 = getVec(cin);         // getVec(cin) is an rvalue; move assignment
//在第一个赋值中,我们将v2传递给赋值运算符。v2的类型是StrVec,表达式v2是一
//个左值。因此移动版本的赋值运算符是不可行的因为我们不能隐式地将一个右值引
//用绑定到一个左值。因此,这个赋值语句使用拷贝赋值运算符。

//在第二个赋值中,我们赋予v2的是getVec调用的结果。此表达式是一个右值。
//在此情况下,两个赋值运算符都是可行的——将getVec的结果绑定到两个运算符的
//参数都是允许的。调用拷贝赋值运算符需要进行一次到const的转换,而StrVec&&则
//是精确匹配。因此,第二个赋值会使用移动赋值运算符。

……但如果没有移动构造函数,右值也被拷贝

如果一个类有一个拷贝构造函数但未定义移动构造函数,编译器不会合成移动构造函数
如果一个类没有移动构造函数函数匹配规则保证该类型的对象会被拷贝
- 即使我们试图通过调用move来移动它们时也是如此
class Foo {
public:
    Foo() = default;
    Foo(const Foo&);  // copy constructor
    // other members, but Foo does not define a move constructor
};
Foo x;
Foo y(x);            // copy constructor; x is an lvalue
Foo z(std::move(x)); // copy constructor, because there is no move constructor
//在对z进行初始化时,我们调用了move(x),它返回一个绑定到x的Foo&&。
//Foo的拷贝构造函数是可行的,因为我们可以将一个Foo&&转换为一个const Foo&。
//因此,z的初始化将使用Foo的拷贝构造函数。
用拷贝构造函数代替移动构造函数几乎肯定是安全的赋值运算符的情况类似)。
一般情况下拷贝构造函数满足对应的移动构造函数的要求
- 它会拷贝给定对象并将原对象置于有效状态
  实际上拷贝构造函数甚至都不会改变原对象的值

如果一个类有一个可用的拷贝构造函数而没有移动构造函数
则其对象是通过拷贝构造函数来移动拷贝赋值运算符和移动赋值运算符的情况类似

拷贝并交换赋值运算符和移动操作

HasPtr版本定义了一个拷贝并交换赋值运算符,
为此类添加一个移动构造函数它实际上也会获得一个移动赋值运算符
class HasPtr {
public:
    // added move constructor
    HasPtr(HasPtr &&p) noexcept : ps(p.ps), i(p.i) {p.ps =0;}
    // assignment 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 
};
//让我们观察赋值运算符。此运算符有一个非引用参数,这意味着此参数要进行
//拷贝初始化。依赖于实参的类型,拷贝初始化要么使用拷贝构造函数,要么使用移
//动构造函数——左值被拷贝,右值被移动。因此,单一的赋值运算符就实现了
//拷贝赋值运算符和移动赋值运算符两种功能。

拷贝并交换赋值运算符和移动操作

假定hp和hp2都是HasPtr对象
hp = hp2; //  hp2 is an lvalue; copy constructor used to copy hp2
hp = std::move(hp2); // move constructor moves hp2
//第二个赋值中,我们调用std::move将一个右值引用绑定到hp2上。
//在此情况下,拷贝构造函数和移动构造函数都是可行的。
//但是,由于实参是一个右值引用,移动构造函数是精确匹配的。
//移动构造函数从hp2拷贝指针,而不会分配任何内存。

不管使用的是拷贝构造函数还是移动构造函数赋值运算符的函数体都swap两个运算
对象的状态交换HasPtr会交换两个对象的指针及int成员
在swap之后rhs中的指针将指向原来左侧运算对象所拥有的string
当rhs离开其作用域时这个string将被销毁

建议:更新三/五法则

所有五个拷贝控制成员应该看作一个整体
一般来说如果一个类定义了任何一个拷贝操作它就应该定义所有五个操作

如前所述某些类必须定义拷贝构造函数拷贝赋值运算符和析构函数才能正确工作
这些类通常拥有一个资源而拷贝成员必须拷贝此资源
一般来说拷贝一个资源会导致一些额外开销在这种拷贝并非必要的情况下
定义了移动构造函数和移动赋值运算符的类就可以避免此问题

Message类的移动操作

定义了自己的拷贝构造函数和拷贝赋值运算符的类通常也会从移动操作受益
- 通过定义移动操作Message类可以使用string和set的移动操作来避免
  拷贝contents和folders成员的额外开销
  除了移动folders成员我们还必须更新每个指向原Message的Folder
  我们必须删除指向旧Message的指针并添加一个指向新Message的指针
移动构造函数和移动赋值运算符都需要更新Folder指针
因此我们首先定义一个操作来完成这一共同的工作
// move the Folder pointers from m to this Message
void Message::move_Folders(Message *m)
{
    folders = std::move(m->folders); // uses set move assignment
    for (auto f : folders) {  // for each Folder
        f->remMsg(m);    // remove the old Message from the Folder
        f->addMsg(this); // add this Message to that Folder
    }
    m->folders.clear();  // ensure that destroying m is harmless
}
//此函数首先移动folders集合。通过调用move,我们使用了set的移动赋值运算符
//而不是它的拷贝赋值运算符。如果我们忽略了move调用,代码仍能正常工作,但带
//来了不必要的拷贝。函数然后遍历所有Folder,从其中删除指向原Message的指针
//并添加指向新Message的指针。
值得注意的是向set插入一个元素可能会抛出一个异常
因此Message的移动构造函数和移动赋值运算符可能会抛出异常
因此我们未将它们标记为noexcept

Message类的移动操作

Message的移动构造函数调用move来移动contents并默认初始化自己的folders成员
Message::Message(Message &&m): contents(std::move(m.contents))
{
    move_Folders(&m); // moves folders and updates the Folder pointers
}
移动赋值运算符直接检查自赋值情况
Message& Message::operator=(Message &&rhs)
{
    if (this != &rhs) {       // direct check for self-assignment
        remove_from_Folders();
        contents = std::move(rhs.contents); // move assignment
        move_Folders(&rhs); // reset the Folders to point to this Message
    }
    return *this;
}
//移动赋值运算符必须销毁左侧运算对象的旧状态

移动迭代器

StrVec的reallocate使用了一个for循环来调用construct从旧内存将元素拷贝到新内存中
如果我们能调用uninitialized_copy来构造新分配的内存将比循环更为简单
uninitialized_copy恰如其名
- 它对元素进行拷贝操作标准库中并没有类似的函数将对象移动到未构造的内存中
新标准库中定义了一种移动迭代器move iterator适配器
一个移动迭代器通过改变给定迭代器的解引用运算符的行为来适配此迭代器
一般来说一个迭代器的解引用运算符返回一个指向元素的左值
与其他迭代器不同移动迭代器的解引用运算符生成一个右值引用

调用标准库的make_move_iterator函数将一个普通迭代器转换为一个移动迭代器
- 此函数接受一个迭代器参数返回一个移动迭代器

移动迭代器支持正常的迭代器操作
- 可以将移动迭代器传递给uninitialized_copy
void StrVec::reallocate()
{
    // 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()),
                                   make_move_iterator(end()),
                                   first);
    free();             // free the old space
    elements = first;   // update the pointers
    first_free = last;
    cap = elements + newcapacity;
}
//uninitialized_copy对输入序列中的每个元素调用construct来将元素“拷贝”到目的位置
//由于我们传递给它的是移动迭代器,因此解引用运算符生成的是一个右值引用,
//这意味着construct将使用移动构造函数来构造元素。
由于移动一个对象可能销毁掉原对象因此你只有在确信算法在为一个元素赋值或
将其传递给一个用户定义的函数后不再访问它时才能将移动迭代器传递给算法

建议:不要随意使用移动操作

由于一个移后源对象具有不确定的状态对其调用std::move是危险的
当我们调用move时必须绝对确认移后源对象没有其他用户

通过在类代码中小心地使用move可以大幅度提升性能
而如果随意在普通用户代码与类实现代码相对中使用移动操作
很可能导致莫名其妙的难以查找的错误而难以提升应用程序性能
在移动构造函数和移动赋值运算符这些类实现代码之外的地方
只有当你确信需要进行移动操作且移动操作是安全的才可以使用std::move

练习

为你的 StrVecString  Message 类添加一个移动构造函数和一个移动赋值运算符

自行设计

练习

在你的 String 类的移动操作中添加打印语句
并重新运行13.6.1节的练习13.48中的程序
它使用了一个vector<String>观察什么时候会避免拷贝

String baz()
{
    String ret("world");
    return ret; // first avoided
}

String s5 = baz(); // second avoided

练习

虽然 unique_ptr 不能拷贝但我们在12.1.5节中编写了一个 clone 函数
它以值的方式返回一个 unique_ptr
解释为什么函数是合法的以及为什么它能正确工作

在这里是移动的操作而不是拷贝操作因此是合法的

练习

详细解释 HasPtr 对象的赋值发生了什么
特别是一步一步描述 hphp2 以及 HasPtr 的赋值运算符
中的参数 rhs 的值发生了什么变化


左值被拷贝右值被移动

练习

从底层效率的角度看HasPtr 的赋值运算符并不理想解释为什么
 HasPtr 实现一个拷贝赋值运算符和一个移动赋值运算符
并比较你的新的移动赋值运算符中执行的操作和拷贝并交换版本中的执行的操作

当使用HasPtr& operator=(HasPtr rhs)比如hp = std::move(hp2)
ps成员被拷贝了两次一次从hp2到rhs,另一次 rhs到*this

而使用HasPtr& operator=(HasPtr&& rhs), ps只被拷贝一次
HasPtr& operator=(const HasPtr& rhs){  // copy assignment operator
    auto newps = new string(*rhs.ps);
    delete ps;
    ps = newps;
    i = rhs.i;
    return *this;
}

inline HasPtr& operator=(HasPtr&& rhs) noexcept{       // move assignment operator
    cout<<" move assignment operator "<<endl;
    if(this!=rhs){
        delete ps;
        ps = rhs.ps;
        rhs.ps = nullptr;
        rhs.i = 0;
    }
    return *this;
}

练习

如果我们为 HasPtr 定义了移动赋值运算符
但未改变拷贝并交换运算符会发生什么编写代码验证你的答案

error: ambiguous overload for 'operator=' (operand types are 'HasPtr' and
'std::remove_reference<HasPtr&>::type { aka HasPtr }')

hp1 = std::move(*pH);
^

右值引用和成员函数

除了构造函数和赋值运算符之外如果一个成员函数同时提供拷贝和移动版本
- 它也能从中受益
这种允许移动的成员函数通常使用与拷贝/移动构造函数和赋值运算符相同的参数模式
- 一个版本接受一个指向const的左值引用
- 第二个版本接受一个指向非const的右值引用

定义了push_back的标准库容器提供两个版本
void push_back(const X&); // copy: binds to any kind of X
void push_back(X&&);      // move: binds only to modifiable rvalues of type X
可以将能转换为类型X的任何对象传递给第一个版本的push_back此版本从其参数拷贝数据
对于第二个版本我们只可以传递给它非const的右值从此版本会从其参数窃取数据

区分移动和拷贝的重载函数通常有一个版本接受一个constT&而另一个版本接受一个T&&

右值引用和成员函数

为StrVec类定义另一个版本的push_back
class StrVec {
public:
    void push_back(const std::string&);  // copy the element
    void push_back(std::string&&);       // move the element
    // other members as before
};
// unchanged from the original version in § 13.5 (p. 527)
void StrVec::push_back(const string& s)
{
    chk_n_alloc(); // ensure that there is room for another element
    // construct a copy of s in the element to which first_free points
    alloc.construct(first_free++, s);
}
void StrVec::push_back(string &&s)
{
    chk_n_alloc(); // reallocates the StrVec if necessary
    alloc.construct(first_free++, std::move(s));
}

右值引用和成员函数

当我们调用push_back时实参类型决定了新元素是拷贝还是移动到容器中
StrVec vec;  // empty StrVec
string s = "some string or another";
vec.push_back(s);      // calls push_back(const string&)
vec.push_back("done"); // calls push_back(string&&)
//差别在于实参是一个左值还是一个右值(从"done"创建的临时string)

右值和左值引用成员函数

通常我们在一个对象上调用成员函数而不管该对象是一个左值还是一个右值例如
string s1 = "a value", s2 = "another";
auto n = (s1 + s2).find('a');
有时右值的使用方式可能令人惊讶
s1 + s2 = "wow!";
//此处我们对两个string的连接结果——一个右值,进行了赋值。

//在旧标准中,我们没有办法阻止这种使用方式
//为了维持向后兼容性,新标准库类仍然允许向右值赋值

右值和左值引用成员函数

但是我们可能希望在自己的类中阻止这种用法
- 在此情况下我们希望强制左侧运算对象this指向的对象是一个左值
- 我们指出this的左值/右值属性的方式与定义const成员函数相同
  在参数列表后放置一个引用限定符reference qualifier
class Foo {
public:
    Foo &operator=(const Foo&) &; // may assign only to modifiable lvalues
    // other members of Foo
};
Foo &Foo::operator=(const Foo &rhs) &
{
    // do whatever is needed to assign rhs to this object
    return *this;
}
引用限定符可以是&&&分别指出this可以指向一个左值或右值
类似const限定符引用限定符只能用于非static成员函数
且必须同时出现在函数的声明和定义中

对于&限定的函数我们只能将它用于左值对于&&限定的函数只能用于右值
Foo &retFoo();  // returns a reference; a call to retFoo is an lvalue
Foo retVal();   // returns by value; a call to retVal is an rvalue
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();   // ok: we can pass an rvalue as the right-hand operand to assignment
一个函数可以同时用const和引用限定
- 在此情况下引用限定符必须跟随在const限定符之后
class Foo {
public:
    Foo someMem() & const;    // error: const qualifier must come first
    Foo anotherMem() const &; // ok: const qualifier comes first
};

重载和引用函数

引用限定符也可以区分重载版本
- 而且我们可以综合引用限定符和const来区分一个成员函数的重载版本
class Foo {
public:
    Foo sorted() &&;         // may run on modifiable rvalues
    Foo sorted() const &;    // may run on any kind of Foo
    // other members of Foo
private:
    vector<int> data;
};
// this object is an rvalue, so we can sort in place
Foo Foo::sorted() &&
{
    sort(data.begin(), data.end());
    return *this;
}
// this object is either const or it is an lvalue; either way we can't sort in place
Foo Foo::sorted() const & {
    Foo ret(*this);                         // make a copy
    sort(ret.data.begin(), ret.data.end()); // sort the copy
    return ret;                             // return the copy
}

当我们对一个右值执行sorted时它可以安全地直接对data成员进行排序
对象是一个右值意味着没有其他用户因此我们可以改变对象
当对一个const右值或一个左值执行sorted时我们不能改变对象
因此就需要在排序前拷贝data
编译器会根据调用sorted的对象的左值/右值属性来确定使用哪个sorted版本
retVal().sorted(); // retVal() is an rvalue, calls Foo::sorted() &&
retFoo().sorted(); // retFoo() is an lvalue, calls Foo::sorted() const &

当我们定义const成员函数时
- 可以定义两个版本 唯一的差别是一个版本有const限定而另一个没有
引用限定的函数则不一样
- 如果我们定义两个或两个以上具有相同名字和相同参数列表的成员函数
  就必须对所有函数都加上引用限定符或者所有都不加
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*);        // ok: different parameter list
    Foo sorted(Comp*) const;  // ok: neither version is reference qualified
};
//本例中声明了一个没有参数的const版本的sorted,此声明是错误的。
//因为Foo类中还有一个无参的sorted版本,它有一个引用限定符,
//因此const版本也必须有引用限定符。
//另一方面,接受一个比较操作指针的sorted版本是没问题的,
//因为两个函数都没有引用限定符。
如果一个成员函数有引用限定符则具有相同参数列表的所有版本都必须有引用限定符

练习

为你的 StrBlob 添加一个右值引用版本的 push_back

void push_back(string &&s) { data->push_back(std::move(s)); }

练习

如果 sorted 定义如下会发生什么
Foo Foo::sorted() const & {
    Foo ret(*this);
    return ret.sorted();
}


会产生递归并且最终溢出

练习

如果 sorted定义如下会发生什么

Foo Foo::sorted() const & { return Foo(*this).sorted(); }

没问题会调用移动版本

练习

编写新版本的 Foo  sorted 函数中有打印语句
测试这个类来验证你对前两题的答案是否正确
#include <vector>
#include <iostream>
#include <algorithm>
using std::vector; using std::sort;
class Foo {
public:
    Foo sorted() &&;
    Foo sorted() const &;
private:
    vector<int> data;
};

练习

Foo Foo::sorted() && {
    sort(data.begin(), data.end());
    std::cout << "&&" << std::endl; // debug
    return *this;
}

Foo Foo::sorted() const & {
    std::cout << "const &" << std::endl; // debug
    return Foo(*this).sorted(); // Exercise 13.57
}

int main()
{
    Foo().sorted(); // call "&&"
    Foo f;
    f.sorted(); // call "const &"
}

小结

类的五个成员函数定义了该类型对象拷贝移动赋值以及销毁时发生什么
- 拷贝构造函数移动构造函数拷贝赋值运算符移动赋值运算符和析构函数
移动构造函数和移动赋值运算符接受一个通常是非const的右值引用
拷贝版本则接受一个通常是const的普通左值引用

如果一个类未声明这些操作编译器会自动为其生成
如果这些操作未定义成删除的它们会逐成员初始化移动赋值或销毁对象
- 合成的操作依次处理每个非static数据成员
  根据成员类型确定如何移动拷贝赋值或销毁它

分配了内存或其他资源的类几乎总是需要定义拷贝控制成员来管理分配的资源
如果一个类需要析构函数则它几乎肯定也需要定义
移动和拷贝构造函数及移动和拷贝赋值运算符

实践课

  • 从课程主页 cpp.njuer.org 打开 《面向对象编程基础》实验课实验课 十二 c++新特性 https://developer.aliyun.com/adc/scenario/14de95385d684af3acc3cbd2865b90ee
  • 使用g++编译代码
  • 编辑一个 readme.md 文档,键入本次实验心得.
  • 使用git进行版本控制 可使用之前的gitee代码仓库
      - 云服务器elastic compute service,简称ecs
      - aliyun linux 2是阿里云推出的 linux 发行版
      - vim是从vi发展出来的一个文本编辑器.
      - g++ 是c++编译器
    
习题1
给定下面的类框架编写它的
1)拷贝构造函数拷贝所有成员你的构造函数应该动态分配一个新的string
  并将对象拷贝到ps所指向的位置而不是拷贝ps本身
2)赋值运算符类似拷贝构造函数你的赋值运算符应该将对象拷贝到ps指向的位置
3)一个析构函数
4)移动赋值运算符
5)移动构造函数
并编写main函数并观察何时会调用这些函数

class HasPtr {
public:
    HasPtr(const std::string& s = std::string()):
        ps(new std::string(s)), i(0) { }
private:
    std::string *ps;
    int i;
}
编辑c++代码和markdown文档,使用git进行版本控制
yum install -y git gcc-c++
使用git工具进行版本控制
git clone你之前的网络git仓库test(或其它名字)
cd test 进入文件夹test
(clone的仓库,可移动旧文件到目录weekN:  mkdir -p weekN ; mv 文件名 weekN;)

vim test1.cpp
g++ ./test1.cpp 编译
./a.out 执行程序
git add . 加入当前文件夹下所有文件到暂存区
git config --global user.email "you@example.com"
git config --global user.name "Your Name"
vim readme.md 键入新内容(实验感想),按ESC 再按:wq退出
git add .
git commit –m "weekN" 表示提交到本地,备注weekN

git push 到你的git仓库

git log --oneline --graph 可看git记录
键入命令并截图或复制文字,并提交到群作业.
cat test* readme.md

提交

  • 截图或复制文字,提交到群作业.
  • 填写阿里云平台(本实验)的网页实验报告栏,发布保存.本次报告不需要分享提交
  • 填写问卷调查 https://rnk6jc.aliwork.com/o/cppinfo

关于使用tmux

sudo yum install -y tmux
cd ~ && wget https://cpp.njuer.org/tmux && mv tmux .tmux.conf
tmux 进入会话 .
前缀按键prefix= ctrl+a, 
prefix+c创建新面板,
prefix+"分屏,
prefix+k选上面,prefix+j选下面,
prefix+1选择第一,prefix+n选择第n,
prefix+d脱离会话
tmux attach-session -t 0 回到会话0

vim 共分为三种模式

图片1

- 命令模式
  - 刚启动 vim,便进入了命令模式.其它模式下按ESC,可切换回命令模式
    - i 切换到输入模式,以输入字符.
    - x 删除当前光标所在处的字符.
    - : 切换到底线命令模式,可输入命令.
- 输入模式
  - 命令模式下按下i就进入了输入模式.
    - ESC,退出输入模式,切换到命令模式
- 底线命令模式
  - 命令模式下按下:英文冒号就进入了底线命令模式.
    - wq 保存退出

vim 常用按键说明

除了 i, Esc, :wq 之外,其实 vim 还有非常多的按键可以使用.命令模式下
- 光标移动
  - j下 k上 h左 l右
  - w前进一个词 b后退一个词
  - Ctrl+d 向下半屏  ctrl+u 向上半屏
  - G 移动到最后一行 gg 第一行 ngg 第n行
- 复制粘贴
  - dd 删一行 ndd 删n行
  - yy 复制一行 nyy复制n行
  - p将复制的数据粘贴在下一行 P粘贴到上一行
  - u恢复到前一个动作 ctrl+r重做上一个动作
- 搜索替换
  - /word 向下找word     word 向上找
  - n重复搜索 N反向搜索
  - :1,$s/word1/word2/g从第一行到最后一行寻找 word1 字符串,并将该字符串
    取代为 word2

vim 常用按键说明

底线命令模式下
- :set nu   显示行号
- :set nonu 取消行号
- :set paste    粘贴代码不乱序
把caps lock按键映射为ctrl,能提高编辑效率.

Markdown 文档语法

# 一级标题
## 二级标题
*斜体* **粗体**
- 列表项
  - 子列表项
> 引用
[超链接](http://asdf.com)
![图片名](http://asdf.com/a.jpg)

|表格标题1|表格标题2|
 |-|-|
|内容1|内容2|

谢谢