C ++ Primer(第五版)第十三章练习答案
13.1.1 节练习
练习 13.1
拷贝构造函数是什么?什么时候使用它?
如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数时拷贝构造函数。
1.一个对象作为函数参数,以值传递的方式传入函数体;
2.一个对象作为函数返回值,以值传递的方式从函数返回;
3.一个对象用于给另外一个对象进行初始化(常称为赋值初始化);
4.用花括号列表初始化一个数组中的元素或一个聚合类成员。
练习 13.2
解释为什么下面的声明是非法的:
Sales_data::Sales_data(Sales_data rhs);
函数的参数不是引用类型,调用永远不会成功——为了调用拷贝构造函数,必须拷贝它的实参,但为了拷贝实参,又需要调用拷贝构造函数,如此无限循环。
练习 13.3
当我们拷贝一个 StrBlob 时,会发生什么?拷贝一个 StrBlobPtr 呢?
它们都没有拷贝构造函数,StrBlob 中的 data 是 shared_ptr,StrBlobPtr 中 data 的 weak_ptr,则拷贝一个 StrBlob 时,shared_ptr+1,StrBlobPtr 没有。
练习 13.4
假定 Point 是一个类类型,它有一个 public 的拷贝构造函数,指出下面程序片段中哪些地方使用了拷贝构造函数:
Point global;
Point foo_bar(Point arg)
{
Point local = arg, *heap = new Point(global);
*heap = local;
Point pa[4] = { local, *heap };
return *heap;
}
一共 6 次。可以写一个简单的拷贝构造函数验证。
#include<iostream>
#include<string>
using namespace std;
class Point
{
public:
Point(){}
Point(const Point &);
};
Point::Point(const Point&p)
{
cout << "使用拷贝构造函数!" << endl;
}
Point global;
Point foo_bar(Point arg) // 1.函数参数
{
Point local = arg, *heap = new Point(global); // 2.3.赋值初始化
*heap = local;
Point pa[4] = { local, *heap }; // 4、5.列表初始化
return *heap; // 6.返回值
}
int main()
{
Point p;
foo_bar(p);
return 0;
}
练习 13.5
给定下面的类框架,编写一个拷贝构造函数,拷贝所有成员。你的构造函数应该动态分配一个新的 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;
}
HasPtr.h
#ifndef HASPTR_H_
#define HASPTR_H_
#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;
};
#endif
13.1.2 节练习
练习 13.6
拷贝赋值运算符是什么?什么时候使用它?合成拷贝赋值运算符完成什么工作?什么时候会生成合成拷贝赋值运算符?
拷贝赋值运算符是一个名为 operator= 的函数,它接受与类相同类型的参数;
当赋值发生时使用该运算符;
将右侧运算对象的每个非 static 成员赋予左侧运算对象的对应成员,对于数组类型的成员,逐个赋值数组元素,合成拷贝赋值运算符返回一个指向其左侧运算对象的引用;
如果一个类未定义自己的拷贝赋值运算符,会生成合成拷贝赋值运算符。
练习 13.7
当我们将一个 StrBlob 赋值给另一个 StrBlob 时,会发生什么?赋值 StrBlobPtr 呢?
同 13.3
练习 13.8
为 13.1.1 节练习 13.5 中的 HasPtr 类编写赋值运算符。类似拷贝构造函数,你的赋值运算符应该将对象拷贝到 ps 指向的位置。
#ifndef HASPTR_H_
#define HASPTR_H_
#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 &);
private:
std::string *ps;
int i;
};
HasPtr &HasPtr::operator=(const HasPtr &rhs)
{
ps = new std::string(*rhs.ps);
i = rhs.i;
return *this;
}
#endif
13.1.3 节练习
练习 13.9
析构函数是什么?合成析构函数完成什么工作?什么时候会生成合成析构函数?
析构函数是类的一个成员函数,名字由波浪号接类名构成,它没有返回值,也不接受参数,用于释放对象所使用的资源,并销毁对象的非 static 数据成员;
类似拷贝构造函数和拷贝赋值运算符,对于某些类,合成析构函数被用来阻止该类型的对象被销毁,如果不是这种情况,合成析构函数的函数体就为空;
当一个类未定义自己的析构函数时,编译器会为它定义一个合成析构函数。
练习 13.10
当一个 StrBlob 对象销毁时会发生什么?一个 StrBlobPtr 对象销毁时呢?
销毁 StrBlob 时,shared_ptr 引用计数减一,当计数为零时,对象销毁;销毁 StrBlobPtr 时,对象不会被销毁。
练习 13.11
为前面练习中的 HasPtr 类添加一个析构函数。
#ifndef HASPTR_H_
#define HASPTR_H_
#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 &);
~HasPtr() { delete ps; }
private:
std::string *ps;
int i;
};
HasPtr &HasPtr::operator=(const HasPtr &rhs)
{
ps = new std::string(*rhs.ps);
i = rhs.i;
return *this;
}
#endif
练习 13.12
在下面的代码片段中会发生几次析构函数调用?
bool fcn(const Sales_data *trans, Sales_data accum)
{
Sales_data item1(*trans), item2(accum);
return item1.isbn() != item2.isbn();
}
3 次:离开作用域后 accum、item1 和 item2 被销毁。trans 离开作用域不会执行析构函数(指向一个对象的引用或指针离开作用域)。
练习 13.13
理解拷贝控制成员和构造函数的一个好方法是定义一个简单的类,为该类定义这些成员,每个成员都打印自己的名字:
struct X {
X() { std::cout << "X()" << std::endl; }
X(const X&) { std::cout << "X(const X&)" << std::endl; }
};
给 X 添加拷贝赋值运算符和析构函数,并编写一个程序以不同的方式使用 X 的对象:将它们作为非引用参数传递;动态分配它们;将它们存放于容器中;诸如此类。观察程序的输出,直到你确认理解了什么时候会使用拷贝控制成员,以及为什么会使用它们。当你观察程序输出时,记住编译器可以略过对拷贝构造函数的调用。
#include <iostream>
#include <string>
#include <vector>
struct X
{
X() { std::cout << "X()" << std::endl; }
X(const X &) { std::cout << "X(const X&)" << std::endl; }
X &operator=(const X &x)
{
std::cout << "X &operator=(const X &x)" << std::endl;
return *this;
}
~X() { std::cout << "~X()" << std::endl; }
};
void func1(X x)
{
std::cout << "void func1(X x)" << std::endl;
}
void func2(X &x)
{
std::cout << "void func2(X &x)" << std::endl;
}
int main()
{
X x1;
X x2 = x1;
X x3(x1);
X x4{x1};
func1(x1);
func2(x1);
X *x_ptr = new X();
delete x_ptr;
std::vector<X> xv;
xv.push_back(x1);
x3 = x1;
return 0;
}
13.1.4 节练习
练习 13.14
假定 numbered 是一个类,它有一个默认构造函数,能为每个对象生成一个唯一的序号,保存在名为 mysn 的数据成员中。假定 numbered 使用合成的拷贝控制成员,并给定如下函数:
void f (numbered s) { cout << s.mysn < endl; }
则下面代码输出什么内容?
numbered a, b = a, c = b;
f(a); f(b); f(c);
3 次输出同一个 mysn。
练习 13.15
假定 numbered 定义了一个拷贝构造函数,能生成一个新的序列号。这会改变上一题中调用的输出结果吗?如果会改变,为什么?新的输出结果是什么?
会,在拷贝初始化时会调用拷贝构造函数,能生成一个新的序号,同时调用 f 函数时,由于是传递给一个非引用的形参又会调用拷贝构造函数。
3 次输出不一样的 mysn,但不是 a、b、c 的 mysn。
练习 13.16
如果 f 中的参数是 const numbered&,将会怎样?这会改变输出结果吗?如果会改变,为什么?新的输出结果是什么?
会,在拷贝初始化时会调用拷贝构造函数,能生成一个新的序号,同时调用 f 函数时,由于是传递给一个引用的形参,则不会再调用拷贝函数,f 函数输出的就是实参本身的成员。
3 次输出不一样的 mysn,而且是 a、b、c 本身的 mysn。
练习 13.17
分别编写前三题中所描述的 numbered 和 f,验证你是否正确预测了输出结果。
#include <iostream>
using namespace std;
static int i = 7;
struct numbered
{
numbered() { ++i; }
numbered(const numbered &) { ++i; } //新添加的拷贝构造函数
int mysn = i;
};
void f(numbered s)
{
cout << s.mysn << endl;
}
void f2(numbered &s)
{
cout << s.mysn << endl;
}
int main()
{
numbered a, b = a, c = b;
// a、b、c 本身的 mysn 应该是 7、8、9
f(a);f(b);f(c);
f2(a);f2(b);f2(c);
return 0;
}
13.1.6 节练习
练习 13.18
定义一个 Employee 类,它包含雇员的姓名和唯一的雇员证号。为这个类定义默认构造函数,以及接受一个表示雇员姓名的 string 的构造函数。每个构造函数应该通过递增一个 static 数据成员来生成一个唯一的证号。
#ifndef EMPLOYEE_H_
#define EMPLOYEE_H_
#include <string>
class Employee
{
private:
std::string name;
int id;
static int n;
public:
Employee()
{
id = n;
n++;
}
Employee(const std::string &s)
{
id = n;
name = s;
}
~Employee();
};
#endif
练习 13.19
你的 Employee 类需要定义它自己的拷贝控制成员吗?如果需要,为什么?如果不需要,为什么?实现你认为 Employee 需要的拷贝控制成员。
不需要,员工在现实中不能复制。
#ifndef EMPLOYEE_H_
#define EMPLOYEE_H_
#include <string>
class Employee
{
private:
std::string name;
int id;
static int n;
public:
Employee()
{
id = n;
n++;
}
Employee(const std::string &s)
{
id = n;
name = s;
}
Employee(const Employee &) = delete;
Employee &operator=(const Employee &) = delete;
~Employee();
};
#endif
练习 13.20
解释当我们拷贝、赋值或销毁 TextQuery 和 QueryResult 类对象时会发生什么?
因为这两个类中使用的是智能指针,因此在拷贝时,类的所有成员都将被拷贝,在销毁时所有成员也将被销毁。
练习 13.21
你认为 TextQuery 和 QueryResult 类需要定义它们自己版本的拷贝控制成员吗?如果需要,为什么?实现你认为这两个类需要的拷贝控制操作。
判断一个类是否需要自己版本的拷贝控制成员,一个基本原则是首先确定这个类是否需要一个析构函数。
TextQuery 和 QueryResult 类使用智能指针,可以自动控制释放内存,因为其不需要自己版本的析构函数,也就不需要自己版本的拷贝控制函数了。
13.2 节练习
练习 13.22
假定我们希望 HasPtr 的行为像一个值。即,对于对象所指向的 string 成员,每个对象都有一份自己的拷贝。我们将在下一节介绍拷贝控制成员的定义。但是,你已经学习了定义这些成员所需的所有知识。在继续学习下一节之前,为 HasPtr 编写拷贝构造函数和拷贝赋值运算符。
同练习 13.11
13.2.1 节练习
练习 13.23
比较上一节练习中你编写的拷贝控制成员和这一节中的代码。确定你理解了你的代码和我们的代码之间的差异。
缺少释放对象指向的 string;
缺少局部的临时对象。
练习 13.24
如果本节的 HasPtr 版本未定义析构函数,将会发生什么?如果未定义拷贝构造函数,将会发生什么?
如果未定义析构函数,将会发生内存泄漏,动态内存得不到释放,直到没有内存可以申请;如果未定义拷贝构造函数,指针将被复制,可能会多次释放同一个内存。
练习 13.25
假定希望定义 StrBlob 的类值版本,而且希望继续使用 shared_ptr,这样我们的 StrBlobPtr 类就仍能使用指向 vector的 weak_ptr 了。你修改后的类将需要一个拷贝的构造函数和一个拷贝赋值运算符,但不需要析构函数。解释拷贝构造函数和拷贝赋值运算符必须要做什么。解释为什么不需要析构函数。
拷贝构造函数和拷贝赋值运算符需要使用值新建一个shared_ptr;
当类销毁时,shared_ptr计数减1,当计数为0时,其指向的对象会自动销毁,不用析构函数。
练习 13.26
对上一题中描述的 StrBlob 类,编写你自己的版本。
#ifndef STRBLOB_H_
#define STRBLOB_H_
#include <initializer_list>
#include <memory>
#include <stdexcept>
#include <string>
#include <vector>
class ConstStrBlobPtr;
class StrBlob
{
public:
friend class ConstStrBlobPtr;
typedef std::vector<std::string>::size_type size_type;
StrBlob();
StrBlob(std::initializer_list<std::string> il);
StrBlob(const StrBlob &); // 拷贝构造函数
StrBlob &operator=(const StrBlob &); // 拷贝赋值运算符
size_type size() const { return data->size(); }
bool empty() const { return data->empty(); }
void push_back(const std::string &t) { data->push_back(t); }
void pop_back();
std::string &front();
std::string &back();
const std::string &front() const;
const std::string &back() const;
ConstStrBlobPtr begin() const;
ConstStrBlobPtr end() const;
private:
std::shared_ptr<std::vector<std::string>> data;
void check(size_type i, const std::string &msg) const;
};
StrBlob::StrBlob() : data(std::make_shared<std::vector<std::string>>()) {}
StrBlob::StrBlob(std::initializer_list<std::string> il) : data(std::make_shared<std::vector<std::string>>(il)) {}
StrBlob::StrBlob(const StrBlob &sb) { data = std::make_shared<std::vector<std::string>>(*(sb.data)); }
StrBlob &StrBlob::operator=(const StrBlob &sb)
{
data = std::make_shared<std::vector<std::string>>(*(sb.data));
return *this;
}
void StrBlob::check(size_type i, const std::string &msg) const
{
if (i >= data->size())
{
throw std::out_of_range(msg);
}
}
std::string &StrBlob::front()
{
check(0, "front on empty StrBlob");
return data->front();
}
std::string &StrBlob::back()
{
check(0, "back on empty StrBlob");
return data->back();
}
void StrBlob::pop_back()
{
check(0, "pop_back on empty StrBlob");
data->pop_back();
}
class ConstStrBlobPtr
{
public:
ConstStrBlobPtr() : curr(0) {}
ConstStrBlobPtr(const StrBlob &a, size_t sz = 0) : wptr(a.data), curr(sz) {}
std::string &deref() const;
ConstStrBlobPtr &incr();
private:
std::shared_ptr<std::vector<std::string>> check(std::size_t, const std::string &) const;
std::weak_ptr<std::vector<std::string>> wptr;
std::size_t curr;
};
std::shared_ptr<std::vector<std::string>> ConstStrBlobPtr::check(std::size_t i, const std::string &msg) const
{
auto ret = wptr.lock();
if (!ret)
{
throw std::runtime_error("unbound ConstStrBlobPtr");
}
if (i >= ret->size())
{
throw std::out_of_range(msg);
}
return ret;
}
std::string &ConstStrBlobPtr::deref() const
{
auto p = check(curr, "dereference past end");
return (*p)[curr];
}
ConstStrBlobPtr &ConstStrBlobPtr::incr()
{
check(curr, "increment past end if ConstStrBlobPtr");
++curr;
return *this;
}
ConstStrBlobPtr StrBlob::begin() const
{
return ConstStrBlobPtr(*this);
}
ConstStrBlobPtr StrBlob::end() const
{
auto ret = ConstStrBlobPtr(*this, data->size());
return ret;
}
#endif
13.2.2 节练习
练习 13.27
定义你自己的使用引用计数版本的 HasPtr。
#ifndef HASPTR_H_
#define HASPTR_H_
#include <string>
class HasPtr
{
public:
HasPtr(const std::string &s = std::string()) : ps(new std::string(s)), i(0), use(new std::size_t(1)) {}
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;
};
HasPtr &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::~HasPtr()
{
if (--*use == 0)
{
delete ps;
delete use;
}
}
#endif
练习 13.28
给定下面的类,为其实现一个默认构造函数和必要的拷贝控制成员。
(a)
class TreeNode {
pravite:
std::string value;
int count;
TreeNode *left;
TreeNode *right;
};
(b)
class BinStrTree{
pravite:
TreeNode *root;
};
默认构造函数
TreeNode::TreeNode() : value(""), count(1), left(nullptr), right(nullptr) {}
BinStrTree::BinStrTree() : root(nullptr) {}
拷贝构造函数
void TreeNode::CopyTree(void)
{ // 拷贝以此节点为根的子树,增加引用计数
if (left)
{
left->CopyTree(); //左子树不空,拷贝左子树
}
if (right)
{
right->CopyTree(); //右子树不空,拷贝右子树
}
count++;
}
TreeNode::TreeNode(const TreeNode &rhs) : value(rhs.value), count(1), left(rhs.left), right(rhs.right)
{ // 从某个节点开始拷贝子树
if (left)
{
left->CopyTree(); //左子树不空,拷贝左子树
}
if (right)
{
right->CopyTree(); //右子树不空,拷贝右子树
}
}
BinStrTree::BinStrTree(const BinStrTree &bst) : root(bst.root)
{ //拷贝整棵树
root->CopyTree(); //应拷贝整棵树,而不是根节点
}
析构函数
int TreeNode::ReleaseTree()
{ // 释放以此节点为根的子树
if (left)
{ // 如果存在左孩子,遍历释放
if (!left->ReleaseTree())
{ // 左孩子计数为 0,释放其空间
delete left;
}
}
if (right)
{
if (!right->ReleaseTree())
{
delete right;
}
}
--count;
return count;
}
TreeNode::~TreeNode()
{
if (count)
ReleaseTree();
}
BinStrTree::~BinStrTree()
{ // 释放整棵树
if (!root->ReleaseTree())
{ // 释放整棵树,而非仅仅根节点
delete root; // 引用计数为0,释放节点空间
}
}
13.3 节练习
练习 13.29
解释 swap(HasPtr&, HasPtr&) 中对 swap 的调用不会导致递归循环。
参数类型不同,不是同一个函数。
练习 13.30
为你的类值版本的 HasPtr 编写 swap 函数,并测试它。为你的 swap 函数添加一个打印语句,指出函数什么时候执行。
#ifndef HASPTR_EX11_H
#define HASPTR_EX11_H
#include <iostream>
#include <string>
class HasPtr
{
friend void swap(HasPtr &, HasPtr &);
public:
HasPtr(const std::string &s = std::string()) : ps(new std::string(s)), i(0) {}
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_hp)
{
auto newp = new std::string(*rhs_hp.ps);
delete ps;
ps = newp;
i = rhs_hp.i;
return *this;
}
inline void swap(HasPtr &lhs, HasPtr &rhs)
{
using std::swap;
swap(lhs.ps, rhs.ps);
swap(lhs.i, rhs.i);
std::cout << "swap" << std::endl;
}
#endif
练习 13.31
为你的 HasPtr 类定义一个 < 运算符,并定义一个 HasPtr 的 vector。为这个 vector 添加一些元素,并对它执行 sort。注意何时会调用 swap。
#ifndef HASPTR_EX11_H
#define HASPTR_EX11_H
#include <iostream>
#include <string>
class HasPtr
{
friend void swap(HasPtr &, HasPtr &);
friend bool operator<(const HasPtr &, const HasPtr &);
public:
HasPtr(const std::string &s = std::string()) : ps(new std::string(s)), i(0) {}
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_hp)
{
auto newp = new std::string(*rhs_hp.ps);
delete ps;
ps = newp;
i = rhs_hp.i;
return *this;
}
inline void swap(HasPtr &lhs, HasPtr &rhs)
{
using std::swap;
swap(lhs.ps, rhs.ps);
swap(lhs.i, rhs.i);
std::cout << "swap" << std::endl;
}
inline bool operator<(const HasPtr &lhs, const HasPtr &rhs)
{
std::cout << "<" << std::endl;
return *lhs.ps < *rhs.ps;
}
#endif
#include<algorithm>
#include<vector>
#include"HasPtrV.h"
int main()
{
HasPtr p1("a"),p2("b");
std::vector<HasPtr> vp{p1, p2};
std::sort(vp.begin(), vp.end());
return 0;
}
练习 13.32
类指针的 HasPtr 版本会从 swap 函数收益吗?如果会,得到了什么益处?如果不是,为什么?
不会,类指针就是指针的交换,没有涉及内存分配。
13.4 节练习
练习 13.33
为什么 Message 的成员 save 和 remove 的参数是一个 Folder&?为什么我们不能将参数定义为 Folder 或是 const Folder?
因为这两个函数需要更改对应 Folder 的值。
练习 13.34
编写本节所描述的 Message。
#ifndef MESSAGE_H_
#define MESSAGE_H_
#include <set>
#include <string>
class Folder;
class Message
{
friend class Folder;
friend void swap(Message &, Message &);
private:
std::string contents; // 实际包含的文本
std::set<Folder *> folders; // 包含本 Message 的 Folder
// 拷贝构造函数、拷贝赋值运算符和析构函数所使用的工具函数
// 将本 Message 添加到指向参数的 Folder 中
void add_to_Folders(const Message &);
// 从 folders 中的每个 Folder 中删除本 Message
void remove_from_Folders();
public:
explicit Message(const std::string &str = "") : contents(str) {}
// 拷贝控制成员,用来管理指向本 Message 的指针
Message::Message(const Message &m) : contents(m.contents), folders(m.folders) { add_to_Folders(m); }
Message &operator=(const Message &);
~Message() { remove_from_Folders(); }
// 从给定 Folder 集合中添加/删除本 Message
void save(Folder &);
void remove(Folder &);
};
void Message::save(Folder &f)
{
folders.insert(&f); // 将给定 Folder 添加到我们的 Folder 列表中
f.addMsg(this); // 将本 Message 添加到 f 的Message 集合中
}
void Message::remove(Folder &f)
{
folders.erase(&f); // 将给定 Folder 从我们的 Folder 列表中删除
f.remMsg(this); // 将本 Message 从 f 的 Message 集合中删除
}
// 将本 Message 添加到指向 m 的 Folder 中
void Message::add_to_Folders(const Message &m)
{
for (auto f : m.folders) // 对每个包含 m 的 Folder
{
f->addMsg(this); // 向该 Folder 添加一个指向本 Message 的指针
}
}
// 从对应的 Folder 中删除本 Message
void Message::remove_from_Folders()
{
for (auto f : folders) // 对 folders 中每个指针
{
f->remMsg(this); // 才该 Folder 中删除本 Message
}
}
Message &Message::operator=(const Message &rhs)
{
// 通过先删除指针再插入题目来处理自赋值情况
remove_from_Folders(); // 更新已有 Folder
contents = rhs.contents; // 从 rhs 拷贝消息内容
folders = rhs.folders; //从 rhs 拷贝 Folder 指针
add_to_Folders(rhs); // 将本 Message 添加到那些 Folder 中
return *this;
}
void swap(Message &lhs, Message &rhs)
{
using std::swap; // 在本例中严格来说并不需要,但这是个好习惯
// 将每个消息的指针从它(原来)所在 Folder 中删除
for (auto f : lhs.folders)
{
f->remMsg(&lhs);
}
for (auto f : rhs.folders)
{
f->remMsg(&rhs);
}
// 交换 contents 和 Folder 指针 set
swap(lhs.folders, rhs.folders); // 所以 swap(set&, set&)
swap(lhs.contents, rhs.contents); // swap(string&, string&)
// 将每个 Message 的指针添加到它的(新)Folder 中
for (auto f : lhs.folders)
{
f->addMsg(&lhs);
}
for (auto f : rhs.folders)
{
f->addMsg(&rhs);
}
}
#endif
练习 13.35
如果 Message 使用合成的拷贝控制成员,将会发生什么?
Message 本身可以拷贝,但 Message 中保存 Folder 信息的 folders 列表与 Folder 中保存的 Message 信息不统一。
练习 13.36
设计并实现对应的 Folder 类。此类应该保存一个指向 Folder 中包含 Message 的 set。
#ifndef MESSAGE_H_
#define MESSAGE_H_
#include <set>
#include <string>
class Folder;
class Message
{
friend class Folder;
friend void swap(Message &, Message &);
private:
std::string contents;
std::set<Folder *> folders;
void add_to_Folders(const Message &);
void remove_from_Folders();
public:
explicit Message(const std::string &str = "") : contents(str) {}
Message::Message(const Message &m) : contents(m.contents), folders(m.folders) { add_to_Folders(m); }
Message &operator=(const Message &);
~Message() { remove_from_Folders(); }
void save(Folder &);
void remove(Folder &);
};
class Folder
{
friend class Message;
private:
std::set<Message *> messages;
void add_to_Message(const Folder &);
void remove_from_Message();
public:
Folder();
Folder(const Folder &);
Folder &operator=(const Folder &);
~Folder();
void addMsg(Message *m) { messages.insert(m); }
void remMsg(Message *m) { messages.erase(m); }
};
void Message::save(Folder &f)
{
folders.insert(&f);
f.addMsg(this);
}
void Message::remove(Folder &f)
{
folders.erase(&f);
f.remMsg(this);
}
void Message::add_to_Folders(const Message &m)
{
for (auto f : m.folders)
{
f->addMsg(this);
}
}
void Message::remove_from_Folders()
{
for (auto f : folders)
{
f->remMsg(this);
}
}
Message &Message::operator=(const Message &rhs)
{
remove_from_Folders();
contents = rhs.contents;
folders = rhs.folders;
add_to_Folders(rhs);
return *this;
}
void swap(Message &lhs, Message &rhs)
{
using std::swap;
for (auto f : lhs.folders)
{
f->remMsg(&lhs);
}
for (auto f : rhs.folders)
{
f->remMsg(&rhs);
}
swap(lhs.folders, rhs.folders);
swap(lhs.contents, rhs.contents);
for (auto f : lhs.folders)
{
f->addMsg(&lhs);
}
for (auto f : rhs.folders)
{
f->addMsg(&rhs);
}
}
void Folder::add_to_Message(const Folder &f)
{
for(auto m : f.messages)
m->save(*this);
}
void Folder::remove_from_Message()
{
for(auto m : messages)
m->remove(*this);
}
Folder &Folder::operator=(const Folder &rhs)
{
remove_from_Message();
messages = rhs.messages;
add_to_Message(rhs);
return *this;
}
#endif
练习 13.37
为 Message 类添加成员,实现向 folders 添加和删除一个给定的 Folder*。这两个成员类似 Folder 类的 addMsg 和 remMsg 操作。
见练习 13.37
练习 13.38
我们并未使用拷贝交换方式来设计 Message 的赋值运算符。你认为其原因是什么?
当涉及到动态分配时,使用拷贝和交换方式来实现赋值运算符是一个很好的方式(因为有共同的 delete 操作)。
但 Message 类并未涉及到动态分配,此时如果使用拷贝和交换的方式就没有意义。
13.5 节练习
练习 13.39
编写你自己版本的 StrVec,包括自己版本的 reserve、capacity 和 resize。
#ifndef STRVEC_H_
#define STRVEC_H_
#include <memory>
#include <string>
class StrVec
{
public:
StrVec() : elements(nullptr), first_free(nullptr), cap(nullptr) {}
StrVec(const StrVec &);
StrVec &operator=(const StrVec &);
~StrVec() { free(); }
void push_back(const std::string &);
size_t size() const { return first_free - elements; }
size_t capacity() const { return cap - elements; }
std::string *begain() const { return elements; }
std::string *end() const { return first_free; }
void reserve(size_t);
void resize(size_t);
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();
std::string *elements;
std::string *first_free;
std::string *cap;
};
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);
return {data, std::uninitialized_copy(b, e, data)};
}
void StrVec::free()
{
if (elements)
{
for (auto p = first_free; p != elements; /* */)
alloc.destroy(--p); // 销毁内存的对象,自动调用对象类型的析构函数,即 ~string()
alloc.deallocate(elements, cap - elements); // 销毁内存本身
}
}
StrVec::StrVec(const StrVec &s)
{
auto newdata = alloc_n_copy(s.begain(), s.end());
elements = newdata.first;
first_free = cap = newdata.second;
}
StrVec &StrVec::operator=(const StrVec &rhs)
{
auto data = alloc_n_copy(rhs.begain(), rhs.end()); // 在释放前先拷贝,处理自赋值
free();
elements = data.first;
first_free = cap = data.second;
return *this;
}
void StrVec::reallocate()
{
auto newcapacity = size() ? 2 * size() : 1;
auto newdata = alloc.allocate(newcapacity); // 返回新空间开始的位置
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 + newcapacity;
}
void StrVec::reserve(size_t n)
{
if (n <= capacity()) // 如果需求小于或者等于当前容量,什么都不做
return;
auto newdata = alloc.allocate(n);
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 + n;
}
void StrVec::resize(size_t n)
{
if (n < size())
{ // 如果要求的大小比原来小,要删除多余的元素
while (n < size())
alloc.destroy(--first_free);
}
else if (n > size())
{ // 如果要求的大小比原来小,则增加元素(用 string 的默认构造)
while (n > size())
push_back(std::string());
}
}
#endif
练习 13.40
为你的 StrVec 类添加一个构造函数,它接受一个 initializer_list< string > 参数。
class StrVec
{
public:
StrVec() : elements(nullptr), first_free(nullptr), cap(nullptr) {}
StrVec(std::initializer_list<std::string> &);
// 其他不变
};
StrVec::StrVec(std::initializer_list<std::string> &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 指向未构造内存的开始,后置递增运算是保证,从当前逐个构造内存空间;如果改为前置递增,构造后会空出一个未构造内存空间。
练习 13.42
在你的 TextQuery 和 QueryResult 类中用你的 StrVec 类代替 vector< string >,以此来测试你的 StrVec 类。
直接替换即可!
// using line_no = vector<string>::size_type;
using line_no = size_t;
// shared_ptr<vector<string>> file;
shared_ptr<StrVec> file;
测试同 12.29
练习 13.43
重写 free 成员,用 for_each 和 lambda 来代替 for 循环 destroy 元素。你更倾向于哪种实现,为什么?
void StrVec::free()
{
if (elements)
{
/* for (auto p = first_free; p != elements;)
alloc.destroy(--p); */
std::for_each(elements, first_free, [this](string &p) { alloc.destroy(&p); });
alloc.deallocate(elements, cap - elements);
}
}
原来的实现容易读懂。
练习 13.44
编写标准库 string 类的简化版本,命名为 String。你的类应该至少有一个默认构造函数和一个接受 C 风格字符串指针参数的构造函数。使用 allocator 为你的 String类分配所需内存。
#ifndef STRING_H_
#define STRING_H_
#include <cstring>
#include <memory>
class String
{
public:
String();
String(const char *);
String(const String &);
String &operator=(const String &);
size_t size() const { return first_free - elements; }
char *begin() const { return elements; }
char *end() const { return first_free; }
~String() { free(); }
private:
static std::allocator<char> alloc;
std::pair<char *, char *> alloc_n_copy(const char *, const char *);
void free();
char *elements;
char *first_free;
};
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::free()
{
if (elements)
{
for (auto p = first_free; p != elements;)
alloc.destroy(--p);
alloc.deallocate(elements, size());
}
}
String::String(const char *c)
{
size_t n = strlen(c);
auto newStr = alloc_n_copy(c, c + n);
elements = newStr.first;
first_free = newStr.second;
}
String::String(const String &s)
{
auto newStr = alloc_n_copy(s.begin(), s.end());
elements = newStr.first;
first_free = newStr.second;
}
String &String::operator=(const String &rhs)
{
auto str = alloc_n_copy(rhs.begin(), rhs.end());
free();
elements = str.first;
first_free = str.second;
return *this;
}
#endif
13.6.1 节练习
练习 13.45
解释左值引用和右值引用的区别?
左值引用是绑定到左值上的引用,左值持久;
右值引用是绑定到右值上的引用,右值短暂,右值引用可以绑定到要求转换的表达式、字面值常量或是返回右值的表达式上。
练习 13.46
什么类型的引用可以绑定到下面的初始化器上?
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();
练习 13.47
对你在练习 13.44 中定义的 String 类,为它的拷贝构造函数和拷贝赋值运算符添加一条语句,在每次函数执行时打印一条信息。
String::String(const char *c)
{
size_t n = strlen(c);
auto newStr = alloc_n_copy(c, c + n);
elements = newStr.first;
first_free = newStr.second;
std::cout << "String(const char *)" << std::endl;
}
String::String(const String &s)
{
auto newStr = alloc_n_copy(s.begin(), s.end());
elements = newStr.first;
first_free = newStr.second;
std::cout << "String(const String &)" << std::endl;
}
String &String::operator=(const String &rhs)
{
auto str = alloc_n_copy(rhs.begin(), rhs.end());
free();
elements = str.first;
first_free = str.second;
std::cout << "String &operator=(const String &)" << std::endl;
return *this;
}
练习 13.48
定义一个vector < String >并在其上多次调用 push_back。运行你的程序,并观察 String 被拷贝了多少次。
#include"String.h"
#include<vector>
int main()
{
std::vector<String> v;
v.push_back("aaaa");
v.push_back("aaaa");
v.push_back("aaaa");
return 0;
}
运行结果
String(const char *)
String(const String &)
String(const char *)
String(const String &)
String(const String &)
String(const char *)
String(const String &)
String(const String &)
String(const String &)
13.6.2 节练习
练习 13.49
为你的 StrVec、String 和 Message 类添加一个移动构造函数和一个移动赋值运算符。
StrVec 类
StrVec(StrVec &&s) noexcept : elements(std::move(s.elements)), first_free(std::move(s.first_free)), cap(std::move(s.cap)) { s.elements = s.first_free = s.cap; }
StrVec &operator=(StrVec &&) noexcept;
StrVec &StrVec::operator=(StrVec &&rhs) noexcept
{
if (this != &rhs)
{
free();
elements = rhs.elements;
first_free = rhs.first_free;
cap = rhs.cap;
rhs.elements = rhs.first_free = rhs.cap = nullptr;
}
return *this;
}
String 类
String(String &&s) noexcept : elements(std::move(s.elements)), first_free(std::move(s.first_free)) { s.elements = s.first_free = nullptr; }
String &operator=(String &&) noexcept;
String &String::operator=(String&&rhs)noexcept
{
if (this!=&rhs)
{
free();
elements = rhs.elements;
first_free = rhs.first_free;
rhs.elements = rhs.first_free = nullptr;
}
return *this;
}
Message 类
Message(Message &&m) noexcept : contents(std::move(m.contents)) { move_Folders(&m); }
Message operator=(Message &&);
void Message::move_Folders(Message*m)
{
folders = std::move(m->folders);
for(auto f:folders)
{
f->remMsg(m);
f->addMsg(this);
}
m->folders.clear();
}
Message &Message::operator=(Message &&rhs)
{
if (this!=&rhs)
{
remove_from_Folders();
contents = std::move(rhs.contents);
move_Folders(&rhs);
}
return *this;
}
练习 13.50
在你的 String 类的移动操作中添加打印语句,并重新运行 13.6.1 节的练习 13.48 中的程序,它使用了一个 vector< String >,观察什么时候会避免拷贝。
#include "String.h"
#include <vector>
int main()
{
std::vector<String> v;
v.push_back("acac");
v.push_back("121");
v.push_back("@@@@@");
return 0;
}
运行结果
String(const char *)
String(String &&) noexcept
String(const char *)
String(String &&) noexcept
String(String &&) noexcept
String(const char *)
String(String &&) noexcept
String(String &&) noexcept
String(String &&) noexcept
练习 13.51
虽然 unique_ptr 不能拷贝,但我们在 12.1.5 节中编写了一个 clone 函数,它以值的方式返回一个 unique_ptr。解释为什么函数是合法的,以及为什么它能正确工作。
不能拷贝 unique_ptr 的规则有一个例外:可以拷贝或赋值一个将要销毁的 unique_ptr ,即移动操作。
练习 13.52
详细解释第 478 页中的 HasPtr 对象的赋值发生了什么?特别是,一步一步描述 hp、hp2 以及 HasPtr 的赋值运算符中的参数 rhs 的值发生了什么变化。
hp = hp2;
hp2 是一个左值,因此移动构造函数是不可行的。赋值运算符的参数 rhs 将使用拷贝构造函数来初始化。拷贝构造函数将分配一个新的 string,并拷贝 hp2 指向的 string;
hp = std::move(hp2);
调用 std::move 将一个右值引用绑定到 hp2 上。在此情况下,拷贝构造函数和移动构造函数都是可行的。但是,由于实参是一个右值引用,移动构造函数是精确匹配的。移动构造函数从 hp2 拷贝指针,而不会分配任何内存。
练习 13.53
从底层效率的角度看,HasPtr 的赋值运算符并不理想,解释为什么?为 HasPtr 实现一个拷贝赋值运算符和一个移动赋值运算符,并比较你的新的移动赋值运算符中执行的操作和拷贝并交换版本中的执行的操作。
使用交换 swap 函数,会再次给两个对象赋值,但是赋值运算符本意只需要给左侧对象赋值。
HasPtr(HasPtr &&p) noexcept : ps(p.ps), i(p.i) { p.ps = 0; }
HasPtr &HasPtr::operator=(HasPtr &&rhs)
{
if (this != &rhs)
{
delete ps;
ps = std::move(rhs.ps);
i = rhs.i;
}
return *this;
}
练习 13.54
如果我们为 HasPtr 定义了移动赋值运算符,但未改变拷贝并交换运算符,会发生什么?编写代码验证你的答案。
HasPtrV.h
#ifndef HASPTR_EX11_H
#define HASPTR_EX11_H
#include <iostream>
#include <string>
class HasPtr
{
friend void swap(HasPtr &, HasPtr &);
friend bool operator<(const HasPtr &, const HasPtr &);
public:
HasPtr(const std::string &s = std::string()) : ps(new std::string(s)), i(0) {}
HasPtr(const HasPtr &p) : ps(new std::string(*p.ps)), i(p.i) {}
// HasPtr &operator=(const HasPtr &);
HasPtr &operator=(HasPtr &rhs_hp)
{
swap(*this, rhs_hp);
std::cout << "HasPtr &operator=(HasPtr &rhs_hp)" << std::endl;
return *this;
}
HasPtr(HasPtr &&p) noexcept : ps(p.ps), i(p.i) { p.ps = 0; }
HasPtr &operator=(HasPtr &&);
~HasPtr() { delete ps; }
private:
std::string *ps;
int i;
};
/* HasPtr &HasPtr::operator=(const HasPtr &rhs_hp)
{
auto newp = new std::string(*rhs_hp.ps);
delete ps;
ps = newp;
i = rhs_hp.i;
return *this;
} */
inline void swap(HasPtr &lhs, HasPtr &rhs)
{
using std::swap;
swap(lhs.ps, rhs.ps);
swap(lhs.i, rhs.i);
std::cout << "swap" << std::endl;
}
inline bool operator<(const HasPtr &lhs, const HasPtr &rhs)
{
std::cout << "<" << std::endl;
return *lhs.ps < *rhs.ps;
}
HasPtr &HasPtr::operator=(HasPtr &&rhs_hp)
{
if (this != &rhs_hp)
{
delete ps;
ps = std::move(rhs_hp.ps);
i = rhs_hp.i;
}
std::cout << "HasPtr &operator=(HasPtr &&rhs_hp)" << std::endl;
return *this;
}
#endif
13.54.cpp
#include "HasPtrV.h"
int main()
{
HasPtr hp1("aaa"), hp2("bbb");
hp1 = hp2;
hp1 = std::move(hp2);
return 0;
}
运行结果
swap
HasPtr &operator=(HasPtr &rhs_hp)
HasPtr &operator=(HasPtr &&rhs_hp)
赋值时会使用拷贝操作,调用 move 时会使用移动操作!
13.6.3 节练习
练习 13.55
为你的 StrBlob 添加一个右值引用版本的 push_back。
void push_back(std::string &&t) { data->push_back(t); }
练习 13.56
如果 sorted 定义如下,会发生什么?
Foo Foo::sorted() const & {
Foo ret(*this);
return ret.sorted();
}
无线递归。
练习 13.57
如果 sorted 定义如下,会发生什么?
Foo Foo::sorted() const & { return Foo(*this).sorted(); }
强制将 *this 类型转换会产生一个右值,右值调用右值版本的 sorted,结果是正常运行。
练习 13.58
编写新版本的 Foo 类,其 sorted 函数中有打印语句,测试这个类,来验证你对前两题的答案是否正确。
Foo.h
#ifndef FOO_H_
#define FOO_H_
#include <algorithm>
#include <iostream>
#include <vector>
class Foo
{
public:
Foo sorted() &&;
Foo sorted() const &;
private:
std::vector<int> data;
};
Foo Foo::sorted() &&
{
sort(data.begin(), data.end());
std::cout << "Foo sorted() &&" << std::endl;
return *this;
}
Foo Foo::sorted() const &
{
/* Foo ret(*this);
sort(ret.data.begin(), ret.data.end());
return ret; */
std::cout << "Foo sorted() const &;" << std::endl;
Foo ret(*this);
return ret.sorted();
}
#endif
#include "Foo.h"
int main()
{
Foo foo;
foo.sorted();
return 0;
}
运行结果
Foo sorted() const &;
Foo sorted() const &;
Foo sorted() const &;
Foo sorted() const &;
Foo sorted() const &;
...(无限循环)
Foo.h
#ifndef FOO_H_
#define FOO_H_
#include <algorithm>
#include <iostream>
#include <vector>
class Foo
{
public:
Foo sorted() &&;
Foo sorted() const &;
private:
std::vector<int> data;
};
Foo Foo::sorted() &&
{
sort(data.begin(), data.end());
std::cout << "Foo sorted() &&" << std::endl;
return *this;
}
Foo Foo::sorted() const &
{
std::cout << "Foo sorted() const &;" << std::endl;
/* Foo ret(*this);
sort(ret.data.begin(), ret.data.end());
return ret; */
/* Foo ret(*this);
return ret.sorted(); */
return Foo(*this).sorted();
}
#endif
运行结果
Foo sorted() const &;
Foo sorted() &&