C++primer学习笔记及作业答案之第十三章

    xiaoxiao2022-07-07  204

    笔记:

    1.拷贝、赋值和销毁

    重载运算符本质是函数。

    析构函数是类的一个成员函数,没有返回值,也不接受参数,因此它不能被重载,对于一个给定类,只有唯一一个析构函数。

    内置类型没有析构函数。

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

    对于析构函数已删除的类型,不能定义该类型的变量或释放指向该类型动态分配对象的指针。

    拷贝构造函数和拷贝赋值运算符是分开的。

    2.拷贝控制和资源管理

    赋值运算符通常组合了析构函数和构造函数的操作。

    拷贝赋值运算符要保证即使是将一个对象赋予它自身,也要能正常工作。一个好方法是在销毁左侧运算对象资源之前拷贝右侧运算对象。

    3.交换操作

    与拷贝控制成员不同,swap并不是必要的。但是,对于分配了资源的类,定义swap可能是一种很重要的优化手段。

    定义swap的类通常用swap来定义它们的赋值运算符。

    使用拷贝和交换的赋值运算符自动就是异常安全的,且能正确处理自赋值。

    4.拷贝控制示例

     

    5.动态内存管理类

     

    6.对象移动

    IO类或unique_ptr这些类都包含不能被共享的资源(如指针或IO缓冲)。因此,这些类型的对象不能拷贝但可以移动。

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

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

    一个左值表达式表示的是一个对象的身份,而一个右值表达式表示的是对象的值。

    左值持久,右值短暂。使用右值引用的代码可以自由地接管所引用的对象的资源。

    我们在一个函数的参数列表后指定noexpect。在一个构造函数中,noexpect出现在参数列表和初始化列表开始的冒号之间。

    不抛出异常的移动构造函数和移动赋值运算符必须标记为noexpect。

    只有当一个类没有定义任何自己版本的拷贝控制成员,且类的每个非static数据成员都可以移动时,编译器才会为它合成移动构造函数或移动移动赋值运算符。编译器可以移动内置类型的成员。

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

    引用限定符&,&&;&是左值限定符,&&是右值限定符。一个函数可以同时用const和引用限定。在此情况下,引用限定符必须跟随在const限定符之后,即const &。

    如果一个成员函数有引用限定符,则具有相同参数列表的所有版本都必须具有引用限定符。

    课后习题:

    练习13.1:拷贝构造函数是什么?什么时候使用它?

    答:如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数。

    拷贝构造函数在以下几种情况下会被使用:

    1、拷贝初始化

    2、将一个对象作为实参传递给非引用类型的形参

    3、一个返回类型为非引用类型的函数返回一个对象

    4、用花括号列表初始化一个数组中的元素或一个聚合类中的成员

    5、初始化标准库容器或调用其insert/push操作时,容器会对其元素进行拷贝初始化。

    练习13.2:解释为什么下面的声明是非法的:

    Sales_data::Sales_data(Sales_data rhs);

    答:这一声明是非法的, 因为对于上一题所述的情况,我们需要调用拷贝构造函数,但调用永远也不会成功。因为其自身的参数也是非引用类型,为了调用它,必须拷贝其实参,而为了拷贝实参,又需要调用拷贝构造函数,也就是其自身,从而造成死循环。

    练习13.3:当我们拷贝一个StrBlob 时,会发生什么?拷贝一个StrBlobPtr 呢?

    答:这两个类都没定义拷贝构造函数,因此编译器为它们定义了合成的拷贝构造函数。合成的拷贝构造函数逐个拷贝非const成员,对内置类型的成员,直接进行内存拷贝,对类类型的成员,调用其拷贝构造函数进行拷贝。

    因此,拷贝一个StrBlob时,拷贝其唯一的成员data,使用shared_ptr的拷贝构造函数来进行拷贝,因此其引用计数增加1。

    拷贝一个StrBlobPtr时,拷贝成员wptr,用weak_ptr的拷贝构造函数进行拷贝,引用计数不变,然后拷贝curr,直接进行内存复制。

    练习13.4:假定Point 是一个类类型,它有一个public 的拷贝构造函数,指出下面程序片段中哪些地方使用了拷贝构造函数:

    Point global; Point foo_bar(Point arg) { Point local = arg, //将arg拷贝给local Point *heap = new Point(global); *heap = local; //将local拷贝到heap指定的地址中 Point pa[4] = { local, *heap }; //将local和*heap拷贝给数组的前两个元素 return *heap; //也用到了拷贝构造函数 }

    练习13.5:给定下面的类框架,编写一个拷贝构造函数,拷贝所有成员。你的构造函数应该动态分配一个新的string(参见12.1. 2 节,第407 页),并将对象拷贝到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; };

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

    答:拷贝赋值运算符本身是一个重载的赋值运算符,定义为类的成员函数,左侧运算对象绑定到隐含的this参数,而右侧运算对象是所属类类型的,作为函数的参数,函数返回指向其左侧运算对象的引用。

    当对类对象进行赋值时,会使用拷贝赋值运算符。

    通常情况下,合成的拷贝赋值运算符会将右侧对象的非static成员逐个赋予左侧对象的对应成员,这些赋值操作是由成员类型的拷贝赋值运算符来完成的。

    若一个类未定义自己的拷贝赋值运算符,编译器就会为其合成拷贝赋值运算符,完成赋值操作,但对于某些类,还会起到禁止该类型对象赋值的效果。

    练习13.7:当我们将一个StrBlob 赋值给另一个StrBlob 时,会发生什么?赋值StrBlobPtr 呢?

    答:由于这两个类都没定义拷贝赋值运算符,因此编译器为它们定义了合成的拷贝赋值运算符。。

    与拷贝构造函数的行为类似,赋值一个StrBlob时,赋值其唯一的成员data,使用shared_ptr的拷贝赋值运算符来完成,因此其引用计数增加1。

    赋值一个StrBlobPtr时,赋值成员wptr,用weak_ptr的拷贝赋值运算符进行赋值,引用计数不变,然后赋值curr,直接进行内存复制。

    练习13.8:为13.1.1节(第443 页)练习13.5 中的HasPtr 类编写赋值运算符。类似拷贝构造函数,你的赋值运算符应该将对象拷贝到ps 指向的位置。

    HasPtr& HasPtr::operator=(const HasPtr& hp) { auto newps = new string(*hp.ps); //拷贝指针指向的对象 delete ps; //销毁原string ps = newps; //指向新的string i = hp.i; //使用内置的int赋值 return *this; //返回一个此对象的引用 }

    练习13.9:析构函数是什么?合成析构函数完成什么工作?什么时候会生成合成析构函数?

    答:析构函数完成与构造函数相反的工作:释放对象使用的资源,销毁非静态数据成员。从语法上看,它是类的一个成员函数,名字是波浪号接类名,没有返回值,也不接收参数。

    当一个类没有定义析构函数时,编译器会为它合成析构函数。

    合成的析构函数体为空,但这并不意味着它什么也不做,当空函数体执行完后,非静态数据成员会被逐个销毁。也就是说,成员是在析构函数之后隐含的析构阶段中进行销毁的。

    练习13.10:当一个StrBlob 对象销毁时会发生什么? 一个StrBlobPtr 对象销毁时呢?

    答:这两个类都没有定义析构函数,因此编译器会为它们合成析构函数。

    对StrBlob,合成析构函数的空函数体执行完毕后,会进行隐含的析构阶段,销毁非静态数据成员data。这会调用shared_ptr的析构函数,将引用计数减1,引用计数变为0,会销毁共享的vector对象。

    对StrBlobPtr,合成析构函数在隐含的析构阶段会销毁数据成员wptr和curr,销毁wptr会调用weak_ptr的析构函数,引用计数不变,而curr是内置类型,销毁它不会有特殊动作。

    练习13.11:为前面练习中的HasPtr 类添加一个析构函数。

    ~HasPtr() { delete ps; //只需释放string对象所占的空间即可 }

    练习13.12:在下面的代码片段中会发生几次析构函数调用?

    bool fcn(const Sales_data *trans, Sales_data accum) { Sales_data item1(*trans), item2(accum); return item1.isbn() != item2.isbn(); }

    答:这段代码中会发生三次析构函数调用:

    1、函数结束时,局部变量item1的生命期结束,被销毁,Sales_data的析构函数被调用。

    2、类似的,item2在函数结束时被销毁,Sales_data的析构函数被调用。

    3、函数结束时,参数accum的生命期结束,被销毁,Sales_data的析构函数被调用。

    在函数结束时,trans的生命期也结束了,但它是Sales_data的指针,并不是它指向的Sales_data对象的生命期结束(只有delete指针时,指向的动态对象的生命期才结束),所以不会引起析构函数的调用。

    练习13.13:理解拷贝控制成员和构造函数的一个好方法是定义一个简单的类, 为该类定义这些成员, 每个成员都打印出自己的名字:

    struct X { X() {std::cout << "X()" << std::endl; } X(const X&) {std::cout << "X(const X&)" << std::endl; } };

    给x 添加拷贝赋值运算符和析构函数, 并编写一个程序以不同方式使用x 的对象:将它们作为非引用和引用参数传递:动态分配它们;将它们存放于容器中; 诸如此类。观察程序的输出, 直到你确认理解了什么时候会使用拷贝控制成员, 以及为什么会使用它们。当你观察程序输出时,记住编译器可以略过对拷贝构造函数的调用。

    //练习 13.13 #include <iostream> #include <vector> using namespace std; struct X { X() { cout << "构造函数 X()" << endl; } X(const X&) { cout << "拷贝构造函数 X(const X&)" << endl; } //拷贝赋值运算符,必须是成员函数 X& operator=(const X& conx) { cout << "拷贝赋值运算符 =(const X&)" << endl; return *this; } //析构函数 ~X() { cout << "析构函数 ~X()" << endl; } }; void f1(X x) { } void f2(X &x) { } int main(int argc, char *argv[]) { cout << "局部变量:" << endl; X x; cout << endl; cout << "非引用参数传递:" << endl; f1(x); cout << endl; cout << "引用参数传递:" << endl; f2(x); cout << endl; cout << "动态分配:" << endl; X *px = new X; cout << endl; cout << "添加到容器中:" << endl; vector<X> vx; vx.push_back(x); cout << endl; cout << "释放动态分配对象:" << endl; delete px; cout << endl; cout << "间接初始化和赋值:" << endl; X y = x; y = x; cout << endl; cout << "程序结束:" << endl; system("pause"); return 0; }

    程序的输出结果如下:

    可以看到,当作为引用参数传递的时候,是什么也不做的,间接初始化的时候是调用拷贝构造函数,这两点自己在分析的时候都错了,要注意一下。另外,在函数结束时,有三次析构函数的调用,分别对x、y和vx中的第一个元素。

    练习13.14:假定numbered 是一个类,它有一个默认构造函数, 能为每个对象生成一个唯一的序号,保存在名为mysn 的数据成员中。假定numbered 使用合成的拷贝控制成员,并给定以下函数:

    void d (numbered s) { cout << s.mysn << endl; }

    则下面代码输出什么内容?

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

    答:该代码会输出三个相同的序号—合成拷贝构造函数被调用时简单复制序号,使得三个对象具有相同的序号。

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

    答:在此程序中,都是拷贝构造函数在起作用,因此定义能生成新的序号的拷贝构造函数会改变输出结果。

    但注意,新的输出结果不是0、1、2,而是3、4、5。

    因为在定义变量a时,默认构造函数起作用,将其序号设定为0。当定义b、c时,拷贝构造函数起作用,将他们的序号分别设定为1、2。

    但是,在每次调用函数f时,由于参数是numbered类型,又会触发拷贝构造函数,使得每一次都将形参s的序号设定为新值,从而导致三次的输出结果是3、4、5。

    练习13.16:如果f 中的参数是const numbere&,将会怎样?这会改变输出结果吗?如果会改变,为什么?新的输出结果是什么?

    答:会改变输出结果,新结果是0、1、2。

    原因是,将参数改为const numbered &。由于形参类型由类类型变为引用类型,传递的不是类对象而是类对象的引用。这意味着调用f时不再出发拷贝构造函数将实参拷贝给形参,而是传递实参的引用。因此,对每次调用,s都是指向实参的引用,序号自然就是实参的序号。而不是创建一个新的对象,获得一个新序号。

    练习13.17:分别编写前三题中所描述的numbered 和f , 验证你是否正确预测了输出结果。

    //练习 13.17 #include <iostream> using namespace std; class numbered { public: numbered(); //默认构造函数 numbered(const numbered &); //拷贝构造函数 int mysn; private: static int i; }; int numbered:: i = 0; numbered::numbered() { mysn = i++; } numbered::numbered(const numbered& s) { mysn = i++; } void f(numbered& s) { cout << s.mysn << endl; } int main() { numbered a, b = a, c = b; f(a), f(b), f(c); system("pause"); return 0; }

    三题的应用程序皆在上面。

    练习13.18:定义一个Employee 类,它包含雇员的姓名和唯一的雇员证号。为这个类定义默认构造函数,以及接受一个表示雇员姓名的string 的构造函数。每个构造函数应该通过递增一个static 数据成员来生成一个唯一的证号。

    //练习 13.18 #include <iostream> #include <string> using namespace std; class Employee { public: Employee(); //默认构造函数 Employee(string s); ~Employee() = default; //默认析构函数 string name; int digital; private: static int initial; //静态成员不应类内初始化 }; int Employee::initial = 0; Employee::Employee() { digital = initial++; } Employee::Employee(string s) { name = s; digital = initial++; } int main() { Employee employee1; Employee employee2("Lilei"); Employee employee3("Hanmeimei"); cout << employee1.name << " " << employee1.digital << endl; cout << employee2.name << " " << employee2.digital << endl; cout << employee3.name << " " << employee3.digital << endl; system("pause"); return 0; }

    练习13.19:你的Employee 类需要定义它自己的拷贝控制成员吗?如果需要,为什么?如果不需要,为什么?实现你认为Employee 需要的拷贝控制成员。

    //练习 13.19 #include <iostream> #include <string> using namespace std; class Employee { public: Employee() { digital = initial++; } //默认构造函数 Employee(const string s) { name = s; digital = initial++; } //拷贝构造函数 //Employee(const Employee& emp) { name = emp.name; digital = emp.digital; } //拷贝赋值运算符 //Employee& operator=(Employee& rhs) { name = rhs.name; return *this; } ~Employee() = default; //默认析构函数 const string &get_name() { return name; } int get_digital() { return digital; } private: static int initial; //静态成员不应类内初始化 string name; int digital; }; int Employee::initial = 0; void f(Employee &s) { cout << s.get_name() << ":" << s.get_digital() << endl; } int main() { Employee a("赵"), b = a, c; c = b; f(a); f(b), f(c); system("pause"); return 0; }

    练习13.20:解释当我们拷贝、赋值或销毁TextQuery 和QueryResult 类(参见12 .3节,第430 页)对象时会发生什么。

    答:两个类都未定义拷贝控制成员,因此都是编译器为它们定义合成版本。

    当TextQuery销毁时,合成版本会销毁其file和wm成员。对file成员,会将shared_ptr的引用计数减1,若变为0,则销毁所管理的动态vector对象(会调用vector和string的析构函数)。对于wm,调用map函数的析构函数(从而调用string、shared_ptr和set的析构函数),会正确释放资源。

    当QueryResult销毁时,合成版本会销毁其sought、lines和file成员。类似TextQuery,string、shared_ptr、set、vector的析构函数可能被调用,因为这些类都有设计良好的拷贝控制成员,会正确的释放资源。

    当拷贝一个TextQuery时,合成版本会拷贝file和wm成员。对file,shared_ptr的引用计数会加1。对wm,会调用map的拷贝构造函数(继而调用string、shared_ptr和set的拷贝构造函数),因此会正确进行拷贝操作。赋值操作类似,只不过会将原来的资源释放掉,例如,原有的file的引用计数会减1。

    QueryResult的拷贝和赋值类似。

    练习13.21:你认为TextQuery 和QueryResult 类需要定义它们自己版本的拷贝控制成员吗?如果需要,为什么?如果不需要,为什么? 实现你认为这两个类需要的拷贝控制操作。

    答:不需要。

    练习13.22:假定我们希望HasPtr 的行为像一个值。即,对于对象所指向的string成员,每个对象都有一份自己的拷贝。我们将在下一节介绍拷贝控制成员的定义。但是,你已经学习了定义这些成员所需的所有知识。在继续学习下一节之前,为HasPtr 编写拷贝构造函数和拷贝赋值运算符。

    // 练习13.22 #include <iostream> #include <string> using namespace std; class HasPtr { public: HasPtr(const string &s = string()): ps(new string(s)), i(0) { } HasPtr(const HasPtr &p): ps(new string(*p.ps)), i(p.i) { } //拷贝构造函数 HasPtr& operator=(const HasPtr&); //拷贝赋值运算符 HasPtr& operator=(const string&); //赋予新string string& operator*(); //解引用 ~HasPtr(); private: string *ps; int i; }; HasPtr::~HasPtr() { delete ps; } inline HasPtr& HasPtr::operator=(const HasPtr &rhs) { auto newps = new string(*rhs.ps); //拷贝指针指向的对象 delete ps; //销毁原string ps = newps; //指向新的string i = rhs.i; //使用内置的int数值 return *this; //返回一个此对象的引用 } HasPtr& HasPtr::operator=(const string &rhs) { *ps = rhs; return *this; } string& HasPtr::operator*() { return *ps; } int main() { HasPtr h("hi mom!"); HasPtr h2(h); HasPtr h3 = h; h2 = "hi dad!"; h3 = "hi son"; cout << "h: " << *h << endl; cout << "h2: " << *h2 << endl; cout << "h3: " << *h3 << endl; system("pause"); return 0; }

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

    答:没有。

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

    答:如果未定义析构函数,在销毁HasPtr对象时合成的析构函数不会释放指针ps指向的内存,造成内存泄露。

    如果未定义拷贝构造函数,在拷贝HasPtr对象时,合成的拷贝构造函数会简单复制ps成员,使得两个HasPtr指向相同的string。当其中一个HasPtr修改string内容时,另一个HasPtr也被改变,这并不符合我们的设想。如果同时定义了析构函数,情况会更糟,当销毁其中一个HasPtr时,ps指向的string被销毁,另一个HasPtr的ps成为空悬指针。

    练习13.25:假定希望定义StrBlob 的类值版本,而且希望继续使用shared_ptr,这样我们的StrBlobPtr 类就仍能使用指向vector 的weak_ptr 了。你修改后的类将需要一个拷贝构造函数和一个拷贝赋值运算符,但不需要析构函数。解释拷贝构造函数和拷贝赋值运算符必须要做什么。解释为什么不需要析构函数。 答:由于希望StrBlob的行为像值一样,因此在拷贝构造函数和拷贝赋值运算符中,我们应该将其数据——string的vector拷贝一份,使得两个StrBlob对象指向各自的数据,而不是简单拷贝shared_ptr使得两个StrBlob指向同一个vector。

    StrBlob不需要析构函数的原因是,它管理的全部资源就是string的vector,而这是有shared_ptr负责管理的。当一个StrBlob对象销毁时,会调用shared_ptr的析构函数,它会正确调整引用计数,当需要(引用计数变为0)释放vector。即,shared_ptr保证了资源分配、释放的正确性,StrBlob就不必进行相应的处理。

    练习13.26:对上一题中描述的StrBlob 类,编写你自己的版本。  

    //头文件 #ifndef MY_STRBLOB_H #define MY_STRBLOB_H #include <vector> #include <string> #include <initializer_list> #include <memory> #include <stdexcept> using namespace std; //对于StrBlob的友元声明来说,此前置声明是必要的 class StrBolbPtr; class StrBlob { //友元类 friend class StrBlobPtr; public: typedef vector<string>::size_type size_type; StrBlob(); StrBlob(initializer_list<string> il); StrBlob(vector<string> *p); StrBlob(StrBlob &s); //拷贝构造函数 StrBlob& operator= (StrBlob &rhs); 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(); //元素访问 string& front(); const string& front() const; string& back(); const string& back() const; //提供给StrBlobPtr的两个接口 StrBlobPtr begin(); //定义StrBlobPtr后才能定义这两个函数 StrBlobPtr end(); //const版本 StrBlobPtr begin() const; StrBlobPtr end() const; private: shared_ptr<vector<string>> data; //如果data[i]不合法,抛出一个异常。 void check(size_type i, const string &msg) const; }; inline StrBlob::StrBlob() : data(make_shared<vector<string>>()) { } inline StrBlob::StrBlob(initializer_list<string> il) : data(make_shared<vector<string>>(il)) { } inline StrBlob::StrBlob(vector<string> *p) : data(p) { } //因为希望StrBlob的行为更像值,因此需要拷贝shared_ptr指向的对象,而不是简单地拷贝shared_ptr inline StrBlob::StrBlob(StrBlob &s) : data(make_shared<vector<string>>(*s.data)) { } inline StrBlob& StrBlob::operator= (StrBlob &rhs) { auto newdata = *rhs.data; //不用释放内存,由shared_ptr来管理内存 data = make_shared<vector<string>>(newdata); return *this; } inline void StrBlob::check(size_type i, const string &msg) const { if (i >= data->size()) { throw out_of_range(msg); } } inline string& StrBlob::front() { //如果vector为空,check会抛出一个异常 check(0, "front on empty StrBolb"); return data->front(); } //const版本的front inline const string& StrBlob::front() const { check(0, "front on empty StrBlob"); return data->front(); } inline string& StrBlob::back() { check(0, "back on empty StrBlob"); return data->back(); } //const版本的back inline const string& StrBlob::back() const { check(0, "back on empty StrBlob"); return data->back(); } inline void StrBlob::pop_back() { check(0, "pop_back on empty StrBlob"); return data->pop_back(); } //当试图访问一个不存在的元素时,StrBlobPtr抛出一个异常 class StrBlobPtr { friend bool eq(const StrBlobPtr&, const StrBlobPtr&); public: StrBlobPtr() :curr(0) { } StrBlobPtr(StrBlob &a, size_t sz = 0) :wptr(a.data), curr(sz) { } StrBlobPtr(const StrBlob &a, size_t sz = 0) : wptr(a.data), curr(sz) { } string& deref() const; string& deref(int off) const; StrBlobPtr& incr(); //前缀递增 StrBlobPtr& decr(); //前缀递减 private: //若检查成功,check返回一个指向vector的shared_ptr shared_ptr<vector<string>> check(size_t, const string&) const; //保存一个weak_ptr,意味着底层vector可能会被销毁 weak_ptr<vector<string>> wptr; size_t curr; //在数组中的当前位置 }; inline shared_ptr<vector<string>> StrBlobPtr::check(size_t i, const string &msg) const { auto ret = wptr.lock(); //vector还存在吗? if (!ret) { throw runtime_error("unbond StrBlobPtr"); } if (i > ret->size()) { throw out_of_range(msg); } return ret; //否则,返回指向vector的shared_ptr } inline string& StrBlobPtr::deref() const { auto p = check(curr, "dereference past end"); return (*p)[curr]; //(*p)是对象所指向的vector } inline string& StrBlobPtr::deref(int off) const { auto p = check(curr + off, "dereference past end"); return (*p)[curr + off]; //(*p)是对象所指向的vector } //前缀递增:返回递增后的对象的引用 inline StrBlobPtr& StrBlobPtr::incr() { //如果curr已经指向容器的尾后位置,就不能递增它 check(curr, "increment past end of StrBlobPtr"); ++curr; //推进当前位置 return *this; } //前缀递减:返回递增后的对象的引用 inline StrBlobPtr& StrBlobPtr::decr() { //如果curr已经为0,递减它就会产生一个非法下标 --curr; //递减当前位置 check(-1, "decrement past begin of StrBlobPtr"); return *this; } //StrBlob的begin和end成员的定义 inline StrBlobPtr StrBlob::begin() { return StrBlobPtr(*this); } inline StrBlobPtr StrBlob::end() { auto ret = StrBlobPtr(*this, data->size()); return ret; } //const 版本 inline StrBlobPtr StrBlob::begin() const { return StrBlobPtr(*this); } inline StrBlobPtr StrBlob::end() const { auto ret = StrBlobPtr(*this, data->size()); return ret; } //StrBlobPtr的比较操作 inline bool eq(const StrBlobPtr &lhs, const StrBlobPtr &rhs) { auto l = lhs.wptr.lock(), r = rhs.wptr.lock(); //若底层的vector是同一个 if (l == r) { //则两个指针都是空,或者指向相同元素时,它们相等 return (!r || lhs.curr == rhs.curr); } else { return false; //若指向不同vector,则不可能相等 } } inline bool neq(const StrBlobPtr &lhs, const StrBlobPtr &rhs) { return !eq(lhs, rhs); } #endif // 练习13.26 #include <iostream> #include "myStrBlob.h" using namespace std; int main(int argc, char **argv) { StrBlob b1; { StrBlob b2 = { "a", "an", "the" }; b1 = b2; b2.push_back("about"); cout << "b2大小为" <<b2.size() << endl; cout << "b2首尾元素为" << b2.front() << " " << b2.back() << endl; } cout << "b1大小为" << b1.size() << endl; cout << "b1首尾元素为" << b1.front() << " " << b1.back() << endl; StrBlob b3 = b1; b3.push_back("next"); cout << "b3大小为" << b3.size() << endl; cout << "b3首尾元素为" << b3.front() << " " << b3.back() << endl; cout << "b1全部元素:" << endl; for (auto it = b1.begin(); neq(it, b1.end()); it.incr()) cout << it.deref() << endl; system("pause"); return 0; }

    练习13.27:定义你自己的使用引用计数版本的HasPtr 。

    // 练习 13.27 #include <iostream> #include <string> using namespace std; class HasPtr { public: //构造函数分配新的string和新的计数器,将计数器置为1 HasPtr(const string &s = string()): ps(new string(s)), i(0), use(new size_t(1)) { } //拷贝构造函数拷贝所有三个数据成员,并递增计数器 HasPtr(const HasPtr &p): ps(p.ps), i(p.i), use(p.use) { ++*use; } //拷贝构造函数 HasPtr& operator=(const HasPtr&); //拷贝赋值运算符 HasPtr& operator=(const string&); //赋予新string string& operator*(); //解引用 ~HasPtr(); private: string *ps; int i; size_t *use; //用来记录有多少个对象共享*ps成员 }; HasPtr::~HasPtr() { if (--*use == 0) //如果引用计数变为0 { delete ps; //释放string内存 delete use; //释放计数器内存 } } inline HasPtr& HasPtr::operator=(const HasPtr &rhs) { ++*rhs.use; //递增右侧运算对象的引用计数 if (--*use == 0) //然后递减本对象的引用计数 { delete ps; //如果没有其他用户 delete use; //释放本对象分配的成员 } ps = rhs.ps; //将数据从rhs拷贝到本对象 i = rhs.i; use = rhs.use; return *this; //返回本对象 } HasPtr& HasPtr::operator=(const string &rhs) { *ps = rhs; return *this; } string& HasPtr::operator*() { return *ps; } int main() { HasPtr h("hi mom!"); HasPtr h2 = h; //未分配新string,h2和h指向相同的string h = "hi dad!"; cout << "h: " << *h << endl; cout << "h2: " << *h2 << endl; system("pause"); return 0; }

    练习13.28:给定下面的类,为其实现一个默认构造函数和必要的拷贝控制成员。

    (a)class TreeNode { private: std::string value; int count; TreeNode *left; TreeNode *right; } (b)class BinStrTree { private: TreeNode *root; } //练习 13.28 #include <iostream> #include <string> using namespace std; class TreeNode { public: TreeNode(); TreeNode(const TreeNode &); //拷贝构造函数 void CopyTree(); //拷贝整棵树 int ReleaseTree(); ~TreeNode(); private: string value; int count; TreeNode *left; TreeNode *right; }; //构造函数 TreeNode::TreeNode() : value(""), count(1), left(nullptr), right(nullptr) { } //拷贝构造函数,从某个节点开始拷贝子树 TreeNode::TreeNode(const TreeNode &tn) : value(tn.value), count(1), left(tn.left), right(tn.right) { if (left) //左子树不空,拷贝左子树 { left->CopyTree(); } if (right) //右子树不空,拷贝右子树 { right->CopyTree(); } } void TreeNode::CopyTree(void) //拷贝以此节点为根的子树——增加引用计数 { if (left) //左子树不空,拷贝左子树 { left->CopyTree(); } if (right) //右子树不空,拷贝右子树 { right->CopyTree(); } count++; } int TreeNode::ReleaseTree(void) { if (left) { if (!left->CopyTree()) { delete left; } } if (right) { if (!right->CopyTree()) { delete right; } } count--; return count; } TreeNode::~TreeNode() { if (count) { ReleaseTree(); } } class BinStrTree { public: BinStrTree(); BinStrTree(const BinStrTree &); //拷贝构造函数 ~BinStrTree(); private: TreeNode *root; }; BinStrTree::BinStrTree() { } BinStrTree::BinStrTree(const BinStrTree &bst) : root(bst.root) //拷贝整棵树 { root->CopyTree(); //应拷贝整棵树,而非仅仅根节点 } BinStrTree::~BinStrTree() //释放整棵树 { if (!root->ReleaseTree()) //释放整棵树,而非仅仅根节点 { delete root; //引用计数为0,释放节点空间 } }

    上面程序是有一点问题的。

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

    答:在此函数中又调用了swap来交换HasPtr成员ps和i。但这两个成员的类型分别是指针和整型,都是内置类型,因此函数中的swap调用被解析为std::swap,而不是HasPtr的特定版本swap(也就是自身),所以不会导致递归循环。

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

    //练习13.30 #include <iostream> #include <string> using namespace std; class HasPtr { friend void swap(HasPtr&, HasPtr&); public: HasPtr(const string &s = string()): ps(new string(s)), i(0) { } HasPtr(const HasPtr &p): ps(new string(*p.ps)), i(p.i) { } //拷贝构造函数 HasPtr& operator=(const HasPtr&); //拷贝赋值运算符 HasPtr& operator=(const string&); //赋予新string string& operator*(); //解引用 ~HasPtr(); private: string *ps; int i; }; HasPtr::~HasPtr() { delete ps; } inline HasPtr& HasPtr::operator=(const HasPtr &rhs) { auto newps = new string(*rhs.ps); //拷贝指针指向的对象 delete ps; //销毁原string ps = newps; //指向新的string i = rhs.i; //使用内置的int数值 return *this; //返回一个此对象的引用 } HasPtr& HasPtr::operator=(const string &rhs) { *ps = rhs; return *this; } string& HasPtr::operator*() { return *ps; } void swap(HasPtr &lhs, HasPtr &rhs) { cout << "交换" << *lhs.ps << "和" << *rhs.ps << endl; swap(lhs.ps, rhs.ps); swap(lhs.i, rhs.i); } int main() { HasPtr h("hi mom!"); HasPtr h2(h); //行为类值,h2、h3和h指向不同string HasPtr h3 = h; h2 = "hi dad!"; h3 = "hi son"; swap(h2, h3); cout << "h: " << *h << endl; cout << "h2: " << *h2 << endl; cout << "h3: " << *h3 << endl; system("pause"); return 0; }

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

    //练习13.31 #include <iostream> #include <string> #include <vector> #include <algorithm> using namespace std; class HasPtr { friend void swap(HasPtr&, HasPtr&); public: HasPtr(const string &s = string()) : ps(new string(s)), i(0) { } HasPtr(const HasPtr &p) : ps(new string(*p.ps)), i(p.i) { } //拷贝构造函数 HasPtr& operator=(const HasPtr&); //拷贝赋值运算符 HasPtr& operator=(const string&); //赋予新string string& operator*(); //解引用 bool operator<(const HasPtr&) const; //比较运算 ~HasPtr(); private: string *ps; int i; }; HasPtr::~HasPtr() { delete ps; //释放string内存 } inline HasPtr& HasPtr::operator=(const HasPtr &rhs) { auto newps = new string(*rhs.ps); //拷贝指针指向的对象 delete ps; //销毁原string ps = newps; //指向新的string i = rhs.i; //使用内置的int数值 return *this; //返回一个此对象的引用 } HasPtr& HasPtr::operator=(const string &rhs) { *ps = rhs; return *this; } string& HasPtr::operator*() { return *ps; } void swap(HasPtr &lhs, HasPtr &rhs) { cout << "交换" << *lhs.ps << "和" << *rhs.ps << endl; swap(lhs.ps, rhs.ps); swap(lhs.i, rhs.i); } bool HasPtr::operator<(const HasPtr &rhs) const { return *ps < *rhs.ps; } int main(int argc, char *argv[]) { vector<HasPtr> vh; int n = atoi(argv[1]); for (int i = 0; i < n; i++) { vh.push_back(to_string(n - i)); } for (auto p : vh) cout << *p << " "; cout << endl; sort(vh.begin(), vh.end()); for (auto p : vh) cout << *p << " "; cout << endl; system("pause"); return 0; }

    练习13.32:类指针的HasPtr 版本会从swap 函数受益吗?如果会,得到了什么益处?如果不是,为什么?

    答:默认swap版本简单交换两个对象的非静态成员,对HasPtr而言,就是交换string指针ps、引用计数指针use和整型值i。可以看出,这种语义是符合期望的——两个HasPtr指向了原来对方的string,而两者互换string后,各自的引用计数应该属不变的(都是减1再加1)。因此,默认swap版本已经能正确处理类指针HasPtr的交换,专用swap版本不会带来更多收益。

    练习13.33:

    练习13.34:

    练习13.35:这些暂时空着。

    练习13.36:

    练习13.37:

    练习13.38:

    练习13.39:编写你自己版本的StrVec,包括自己版本的reserve、capacity(参见9.4 节,第318 页)和resize(参见9.3.5 节,第314 页)。

    #ifndef STRVEC_H #define STRVEC_H #include <iostream> #include <memory> #include <utility> #include <algorithm> //类vector类内存分配策略的简化实现 class StrVec { public: StrVec() : // allocator成员进行默认初始化 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; } void reverse(size_t n) { if (n > capacity()) reallocate(n); } void resize(size_t n); void resize(size_t n, const std::string &s); // 迭代器接口 std::string *begin() const { return elements; } std::string *end() const { return first_free; } // operator functions covered in chapter 14 std::string& operator[](std::size_t n) { return elements[n]; } const std::string& operator[](std::size_t n) const { return elements[n]; } private: static std::allocator<std::string> alloc; // 分配元素 // 被添加元素的函数所使用 void chk_n_alloc() { if (size() == capacity()) reallocate(); } // 工具函数,被拷贝构造函数、赋值运算符和析构函数所使用 std::pair<std::string*, std::string*> alloc_n_copy (const std::string*, const std::string*); void free(); // 销毁元素并释放内存 void reallocate(); // 获得更多内存并拷贝已有元素 void reallocate(size_t n); std::string *elements; // 指向数组首元素的指针 std::string *first_free; // 指向数组第一个空闲元素的指针 std::string *cap; // 指向数组尾后位置的指针 }; inline StrVec::~StrVec() { free(); } inline std::pair<std::string*, std::string*> StrVec::alloc_n_copy(const std::string *b, const std::string *e) { // 分配空间保存给定范围中的元素 std::string *data = alloc.allocate(e - b); // 初始化并返回一个pair, // 该pair由data和uninitialized_copy的返回值构成 return std::make_pair(data, uninitialized_copy(b, e, data)); } inline StrVec::StrVec(const StrVec &s) { // 调用alloc_n_copy分配空间以容纳与s中一样多的元素 std::pair<std::string*, std::string*> newdata = alloc_n_copy(s.begin(), s.end()); elements = newdata.first; first_free = cap = newdata.second; } inline void StrVec::free() { // 不能传递给deallocate一个空指针,如果elements为0,函数什么也不做 if (elements) { // 逆序销毁旧元素 for (std::string *p = first_free; p != elements; /* 空 */) alloc.destroy(--p); alloc.deallocate(elements, cap - elements); } } inline StrVec &StrVec::operator=(const StrVec &rhs) { // 调用alloc_n_copy分配内存,大小与rhs中元素占用空间一样多 std::pair<std::string*, std::string*> data = alloc_n_copy(rhs.begin(), rhs.end()); free(); elements = data.first; first_free = cap = data.second; return *this; } inline void StrVec::reallocate() { // 我们将分配当前大小两倍的内存空间 size_t newcapacity = size() ? 2 * size() : 1; // 分配新内存 std::string *newdata = alloc.allocate(newcapacity); // 将数据从旧内存移动到新内存 std::string *dest = newdata; // 指向新数组中下一个空闲位置 std::string *elem = elements; // 指向旧数组中下一个元素 for (size_t i = 0; i != size(); ++i) alloc.construct(dest++, std::move(*elem++)); free(); // 一旦我们移动完元素就释放旧内存空间 // 更新我们的数据结构,执行新元素 elements = newdata; first_free = dest; cap = elements + newcapacity; } inline void StrVec::reallocate(size_t newcapacity) { // 分配新内存 auto newdata = alloc.allocate(newcapacity); // 将数据从旧内存移动到新内存 std::string *dest = newdata; // 指向新数组中下一个空闲位置 std::string *elem = elements; // 指向旧数组中下一个元素 for (size_t i = 0; i != size(); ++i) alloc.construct(dest++, std::move(*elem++)); free(); // 一旦我们移动完元素就释放旧内存空间 // 更新我们的数据结构,执行新元素 elements = newdata; first_free = dest; cap = elements + newcapacity; } inline void StrVec::push_back(const std::string& s) { chk_n_alloc(); // 确保有空间容纳新元素 // 在first_free指向的元素中构造s的副本 alloc.construct(first_free++, s); } //添加对象 inline void StrVec::resize(size_t n) { if (n > size()) //添加空字符串 { while (size() < n) { push_back(""); //this指针隐式传递 } } else if (n < size()) //销毁字符串 { while (size() > n) { alloc.destroy(--first_free); } } } //添加对象 inline void StrVec::resize(size_t n, const std::string &s) { if (n > size()) { while (size() < n) { push_back(""); } } } #endif

    练习13.40:为你的StrVec 类添加一个构造函数,它接受一个initializer_list<string>参数。

    inline StrVec::StrVec(std::initializer_list<std::string> il) { //调用alloc_n_copy分配与列表il中元素数目一样多的空间 auto newdata = alloc_n_copy(il.begin(), il.end()); elements = newdata.first; first_free = cap = newdata.second; }

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

    答:因为first_free指向第一个空闲位置,也就是最后一个string的尾后位置。当添加新string时,应该保存在first_free指向的位置,然后将first_free推进一个位置,因此后置递增运算恰好符合要求。

    如果使用前置递增运算,则是先将first_free推进一个位置,然后将新string保存在新位置上。显然,这种方法意味着first_free指向最后一个string,而非尾后位置,与first_free的设定不吻合。

    练习13.42:在你的TextQuery 和QueryResult 类(参见12.3节,第431 页)中用你的StrVec 类代替vector < string >,以此来测试你的StrVec 类。

    答:只需将vector<string>改成StrVec就好了。

    练习13.43:重写free 成员,用for_each 和lambda (参见10.3.2节,第346 页)来代替for 循环destroy元素。你更倾向于哪种实现,为什么?

    for_each(elements, first_free, [](std::string &s) { alloc.destory(&s); })

    labmda版本更简洁。

    练习13.44:编写标准库string 类的简化版本,命名为String 。你的类应该至少有一个默认构造函数和一个接受C 风格字符串指针参数的构造函数。使用allocator 为你的String 类分配所需内存。

    #ifndef STRING_H #define STRING_H #include <cstring> #include <algorithm> #include <cstddef> #include <iostream> #include <iostream> #include <memory> class String { friend String operator+(const String&, const String&); friend String add(const String&, const String&); friend std::ostream &operator<<(std::ostream&, const String&); friend std::ostream &print(std::ostream&, const String&); public: String(): sz(0), p(0) { } // cp points to a null terminated array, // allocate new memory & copy the array String(const char *cp) : sz(std::strlen(cp)), p(a.allocate(sz)) { std::uninitialized_copy(cp, cp + sz, p); } // copy constructor: allocate a new copy of the characters in s String(const String &s):sz(s.sz), p(a.allocate(s.sz)) { std::uninitialized_copy(s.p, s.p + sz , p); } String(size_t n, char c) : sz(n), p(a.allocate(n)) { std::uninitialized_fill_n(p, sz, c); } // allocates a new copy of the data in the right-hand operand; // deletes the memory used by the left-hand operand String &operator=(const String &); // unconditionally delete the memory because each String has its own memory ~String() { if (p) a.deallocate(p, sz); } public: // additional assignment operators String &operator=(const char*); // car = "Studebaker" String &operator=(char); // model = 'T' const char *begin() { return p; } const char *begin() const { return p; } const char *end() { return p + sz; } const char *end() const { return p + sz; } size_t size() const { return sz; } void swap(String &s) { char *tmp = p; p = s.p; s.p = tmp; std::size_t cnt = sz; sz = s.sz; s.sz = cnt; } private: std::size_t sz; char *p ; static std::allocator<char> a; }; String make_plural(size_t ctr, const String &, const String &); inline void swap(String &s1, String &s2) { s1.swap(s2); } #endif

    练习13.45:解释右值引用和左值引用的区别。

    答:所谓右值引用就是必须绑定到右值的引用,通过&&获得。右值引用只能绑定到一个将要销毁的对象上,因此可以自由地移动其资源。

    左值引用,也就是“常规引用”,不能绑定到要转换的表达式、字面值常量或返回右值的表达式。而右值引用恰好相反,可以绑定到这类表达式,但不能绑定到一个左值上。

    返回左值的表达式包括返回左值引用的函数及赋值、下标、解引用和前置递增/递减运算符,返回右值的包括返回非引用类型的函数及算术、关系、位和后置递增/递减运算符。可以看到,左值的特点是有持久的状态,而右值则是短暂的。

    练习13.46:什么类型的引用可以绑定到下面的初始化器上?

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

    1、r1必须是右值引用,因为f是返回非引用类型的函数,返回值是一个右值。

    2、r2必须是左值引用,因为下标运算符返回的是左值。

    3、r3只能是左值引用,因为rl是一个变量,而变量是左值。

    4、r4只能是右值引用,因为vi[0] * f()是一个算数运算表达式,返回右值。

    练习13.47:对你在练习13.44 ( 13.5 节,第470 页)中定义的String 类, 为它的拷贝构造函数和拷贝赋值运算符添加一条语句, 在每次函数执行时打印一条信息。

    //练习 13.47 #include <iostream> #include <vector> #include "String.h" #define _SCL_SECURE_NO_WARNINGS using namespace std; int main() { String s1("One"), s2("Two"); cout << s1 << " " << s2 << endl << endl; String s3(s2); cout << s1 << " " << s2 << " " << s3 << endl << endl; s3 = s1; cout << s1 << " " << s2 << " " << s3 << endl << endl; s3 = String("Three"); cout << s1 << " " << s2 << " " << s3 << endl << endl; vector<String> vs; vs.push_back(s1); vs.push_back(std::move(s2)); vs.push_back(String("Three")); vs.push_back("Four"); for_each(vs.begin(), vs.end(), [](const String &s) { cout << s << " "; }); cout << endl; system("pause"); return 0; }

    练习13.48:定义一个vector<String>并在其上多次调用push_back 。运行你的程序,并观察String 被拷贝了多少次。

    答:如上题所示,String在push_back的过程中,由于扩容会被拷贝很多次,具体次数与vector的扩容策略有关,加倍扩容策略下拷贝函数共触发7次。

    练习13.49:为你的StrVec 、String 和Message 类添加一个移动构造函数和一个移动赋值运算符。

     

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

    答:在windows中,vector扩容时,采用的还是拷贝构造函数;而在VS2013中扩容时使用的是移动构造函数。

    练习13.51:虽然unique_ptr 不能拷贝,但我们在12.1.5 节(第418 页)中编写了一个clone 函数,它以值方式返回一个unique_ptr解释为什么函数是合法的,以及为什么它能正确工作。

    答:unique_ptr不能拷贝,但有一个例外——将要销毁的unique_ptr是可以拷贝或销毁的。因此,在418页的clone函数中返回局部unique_ptr对象ret是可以的,因为ret马上就要被销毁了。而此时的“拷贝”其实就是触发移动构造函数进行了移动。

    练习13.52:详细解释第478 页中的HasPtr 对象的赋值发生了什么?特别是, 一步一步描述hp, hp2 以及HasPtr 的赋值运算符中的参数rhs 的值发生了什么变化。

    答:对hp = hp2,因为hp2是一个变量,是一个左值,因此它传递给赋值运算符参数rhs的过程是拷贝构造过程,rhs获得hp2的一个副本,rhs.ps与hp2.ps指向不同的string,但两个string包含相同的内容。在赋值运算符中,交换hp和rhs,rhs指向hp原来的string,在赋值结束后被销毁。最终结果,hp和hp2指向两个独立的string,但内容相同。

    对hp = std::move(hp2),hp2传递给rhs的过程是移动构造过程,rhs.ps指向hp2.ps原来的string,hp2的ps被设置为空指针。然后赋值运算符交换hp和rhs,rhs指向hp原来的string,在赋值结束后,被销毁。最终结果hp指向hp2原来的string,而hp2则变为空。

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

    答:在进行拷贝赋值时,先通过拷贝构造创建了hp2的拷贝rhs,然后在交换hp和rhs,rhs作为一个中间媒介,只是起到了将值从hp2传递给hp的作用,是一个冗余的操作。

    类似的,在进行移动赋值时,先从hp2转移到rhs,在交换到hp,也是冗余的。

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

    答:会产生编译错误,因为对于hp = std::move(hp2)这样的赋值语句来说,两个运算符匹配得一样好,从而产生二义性。

    练习13.55:为你的StrBlob 添加一个右值引用版本的push_back 。

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

    练习13.56:如果sorted 定义如下,会发生什么:

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

    首先,局部变量ret拷贝了被调用对象的一个副本。然后,对ret调用sorted,由于并非是函数返回语句或函数结束(虽然写成一条语句,但执行过程是先调用sorted,然后将结果返回),因此编译器认为它是左值,仍然调用左值引用版本,产生递归循环。

    利用右值引用版本来完成排序的期望不能实现。

    练习13.57:如果sorted 定义如下,会发生什么:

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

    与上一题不同,本题的写法可以正确利用右值引用版本来完成排序。原因在于,编译器认为Foo(*this)是一个“无主”的右值,对它调用sorted会匹配右值引用版本。

    练习13.58:编写新版本的Foo 类,其sorted 函数中有打印语句,测试这个类, 来验证你对前两题的答案是否正确。

    // 练习 13.58 #include <iostream> #include <vector> #include <algorithm> using namespace std; class Foo { public: Foo sorted() &&; Foo sorted() const &; private: vector<int> data; }; Foo Foo::sorted() && { cout << "右值引用版本" << endl; sort(data.begin(), data.end()); return *this; } Foo Foo::sorted() const & { cout << "左值引用版本" << endl; Foo ret(*this); //return ret.sorted(); return Foo(*this).sorted(); } int main() { Foo f; f.sorted(); system("pause"); return 0; }

    vs2013不支持引用限定符,因此上题建议在windows下进行调试运行。  

     

     

     

    最新回复(0)