# Chapter 13 Copy Control
拷贝控制操作(copy control):
- 拷贝构造函数(copy constructor)
- 拷贝赋值运算符(copy-assignment operator)
- 移动构造函数(move constructor)
- 移动赋值函数(move-assignement operator)
- 析构函数(destructor)
# Copy, Assign, and Destroy
# 拷贝、赋值和销毁
# 拷贝构造函数
- 如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数。
class Foo{ public: Foo(const Foo&); }
- 合成的拷贝构造函数(synthesized copy constructor):会将参数的成员逐个拷贝到正在创建的对象中。
- 拷贝初始化:
- 将右侧运算对象拷贝到正在创建的对象中,如果需要,还需进行类型转换。
- 通常使用拷贝构造函数完成。
string book = "9-99";
- 出现场景:
- 用
=
定义变量时。 - 将一个对象作为实参传递给一个非引用类型的形参。
- 从一个返回类型为非引用类型的函数返回一个对象。
- 用花括号列表初始化一个数组中的元素或者一个聚合类中的成员。
- 用
# 拷贝赋值运算符
- 重载赋值运算符:
- 重写一个名为
operator=
的函数. - 通常返回一个指向其左侧运算对象的引用。
Foo& operator=(const Foo&);
- 重写一个名为
- 合成拷贝赋值运算符:
- 将右侧运算对象的每个非
static
成员赋予左侧运算对象的对应成员。
- 将右侧运算对象的每个非
# 析构函数
- 释放对象所使用的资源,并销毁对象的非
static
数据成员。 - 名字由波浪号接类名构成。没有返回值,也不接受参数。
~Foo();
- 调用时机:
- 变量在离开其作用域时。
- 当一个对象被销毁时,其成员被销毁。
- 容器被销毁时,其元素被销毁。
- 动态分配的对象,当对指向它的指针应用
delete
运算符时。 - 对于临时对象,当创建它的完整表达式结束时。
- 合成析构函数:
- 空函数体执行完后,成员会被自动销毁。
- 注意:析构函数体本身并不直接销毁成员。
# 三 / 五法则
- 需要析构函数的类也需要拷贝和赋值操作。
- 需要拷贝操作的类也需要赋值操作,反之亦然。
# 使用 = default
- 可以通过将拷贝控制成员定义为
=default
来显式地要求编译器生成合成的版本。 - 合成的函数将隐式地声明为内联的。
# 阻止拷贝
- 大多数类应该定义默认构造函数、拷贝构造函数和拷贝赋值运算符,无论是隐式地还是显式地。
- 定义删除的函数:
=delete
。 - 虽然声明了它们,但是不能以任何方式使用它们。
- 析构函数不能是删除的成员。
- 如果一个类有数据成员不能默认构造、拷贝、复制或者销毁,则对应的成员函数将被定义为删除的。
- 老版本使用
private
声明来阻止拷贝。
# Exercise 13.1
拷贝构造函数是什么?什么时候使用它?
解:
如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数。当使用拷贝初始化时,我们会用到拷贝构造函数。
# Exercise 13.2
解释为什么下面的声明是非法的:
Sales_data::Sales_data(Sales_data rhs); |
解:
参数类型应该是引用类型。
# Exercise 13.3
当我们拷贝一个
StrBlob
时,会发生什么?拷贝一个StrBlobPtr
呢?
解:
当我们拷贝 StrBlob
时,会使 shared_ptr
的引用计数加 1。当我们拷贝 StrBlobPtr
时,引用计数不会变化。
# Exercise 13.4
假定
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 处地方使用了拷贝构造函数。
# Exercise 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; | |
} |
解:
#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; | |
}; |
# Exercise 13.6
拷贝赋值运算符是什么?什么时候使用它?合成拷贝赋值运算符完成什么工作?什么时候会生成合成拷贝赋值运算符?
解:
拷贝赋值运算符是一个名为 operator=
的函数。当赋值运算发生时就会用到它。合成拷贝赋值运算符可以用来禁止该类型对象的赋值。如果一个类未定义自己的拷贝赋值运算符,编译器会为它生成一个合成拷贝赋值运算符。
# Exercise 13.7
当我们将一个
StrBlob
赋值给另一个StrBlob
时,会发生什么?赋值StrBlobPtr
呢?
解:
会发生浅层复制。
# Exercise 13.8
为 13.1.1 节练习 13.5 中的
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; | |
}; |
# Exercise 13.9
析构函数是什么?合成析构函数完成什么工作?什么时候会生成合成析构函数?
解:
析构函数是类的一个成员函数,名字由波浪号接类名构成。它没有返回值,也不接受参数。合成析构函数可被用来阻止该类型的对象被销毁。当一个类未定义自己的析构函数时,编译器会为它生成一个合成析构函数。
# Exercise 13.10
当一个
StrBlob
对象销毁时会发生什么?一个StrBlobPtr
对象销毁时呢?
解:
当一个 StrBlob
对象被销毁时, shared_ptr
的引用计数会减少。当 StrBlobPtr
对象被销毁时,不影响引用计数。
# Exercise 13.11
为前面练习中的
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; | |
}; |
# Exercise 13.12
在下面的代码片段中会发生几次析构函数调用?
bool fcn(const Sales_data *trans, Sales_data accum) | |
{ | |
Sales_data item1(*trans), item2(accum); | |
return item1.isbn() != item2.isbn(); | |
} |
解:
三次,分别是 accum
、 item1
和 item2
。
# Exercise 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 <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; | |
} |
# Exercise 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 个完全一样的数。
# Exercise 13.15
假定
numbered
定义了一个拷贝构造函数,能生成一个新的序列号。这会改变上一题中调用的输出结果吗?如果会改变,为什么?新的输出结果是什么?
解:
会输出 3 个不同的数。并且这 3 个数并不是 a、b、c 当中的数。
# Exercise 13.16
如果
f
中的参数是const numbered&
,将会怎样?这会改变输出结果吗?如果会改变,为什么?新的输出结果是什么?
解:
会输出 a、b、c 的数。
# Exercise 13.17
分别编写前三题中所描述的
numbered
和f
,验证你是否正确预测了输出结果。
解:
13.14:
#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); | |
} |
13.15:
#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); | |
} |
13.16:
#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); | |
} |
# Exercise 13.18
定义一个
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; | |
} |
# Exercise 13.19
你的
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; | |
}; |
# Exercise 13.20
解释当我们拷贝、赋值或销毁
TextQuery
和QueryResult
类对象时会发生什么?
解:
成员会被复制。
# Exercise 13.21
你认为
TextQuery
和QueryResult
类需要定义它们自己版本的拷贝控制成员吗?如果需要,为什么?实现你认为这两个类需要的拷贝控制操作。
解:
合成的版本满足所有的需求。因此不需要自定义拷贝控制成员。
# Copy Control and Resource Management
# 拷贝控制和资源管理
- 类的行为可以像一个值,也可以像一个指针。
- 行为像值:对象有自己的状态,副本和原对象是完全独立的。
- 行为像指针:共享状态,拷贝一个这种类的对象时,副本和原对象使用相同的底层数据。
# Exercise 13.22
假定我们希望
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; | |
}; |
# Exercise 13.23
比较上一节练习中你编写的拷贝控制成员和这一节中的代码。确定你理解了你的代码和我们的代码之间的差异。
解:
查看 13.22 代码。
# Exercise 13.24
如果本节的
HasPtr
版本未定义析构函数,将会发生什么?如果未定义拷贝构造函数,将会发生什么?
解:
如果未定义析构函数,将会发生内存泄漏。如果未定义拷贝构造函数,将会拷贝指针的值,指向同一个地址。
# Exercise 13.25
假定希望定义
StrBlob
的类值版本,而且希望继续使用shared_ptr
,这样我们的StrBlobPtr
类就仍能使用指向vector
的weak_ptr
了。你修改后的类将需要一个拷贝的构造函数和一个拷贝赋值运算符,但不需要析构函数。解释拷贝构造函数和拷贝赋值运算符必须要做什么。解释为什么不需要析构函数。
解:
拷贝构造函数和拷贝赋值运算符要重新动态分配内存。因为 StrBlob
使用的是智能指针,当引用计数为 0 时会自动释放对象,因此不需要析构函数。
# Exercise 13.26
对上一题中描述的
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; | |
} |
# Exercise 13.27
定义你自己的使用引用计数版本的
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; | |
}; |
# Exercise 13.28
给定下面的类,为其实现一个默认构造函数和必要的拷贝控制成员。
(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
而不是std::swap
。
# Exercise 13.29
解释
swap(HasPtr&, HasPtr&)
中对swap
的调用不会导致递归循环。
解:
这其实是 3 个不同的函数,参数类型不一样,所以不会导致递归循环。
# Exercise 13.30
为你的类值版本的
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; | |
} |
# Exercise 13.31
为你的
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; | |
} |
# Exercise 13.32
类指针的
HasPtr
版本会从swap
函数收益吗?如果会,得到了什么益处?如果不是,为什么?
解:
不会。类值的版本利用 swap
交换指针不用进行内存分配,因此得到了性能上的提升。类指针的版本本来就不用进行内存分配,所以不会得到性能提升。
# A Copy-Control Example
# Exercise 13.33
为什么
Message
的成员save
和remove
的参数是一个Folder&
?为什么我们不能将参数定义为Folder
或是const Folder
?
解:
因为 save
和 remove
操作需要更新指定 Folder
。
# Exercise 13.34
编写本节所描述的
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); | |
// The book added one line here: folders.clear(); but I think it is redundant and more importantly, it will cause a bug: | |
// - In Message::operator=, in the case of self-assignment, it first calls remove_from_Folders() and its folders.clear() | |
// clears the data member of lhs(rhs), and there is no way we can assign it back to lhs. | |
// Refer to: http://stackoverflow.com/questions/29308115/protection-again-self-assignment | |
// - Why is it redundant? As its analogous function Message::add_to_Folders(), Message::remove_from_Folders() should ONLY | |
// take care of the bookkeeping in Folders but not touch the Message's own data members - makes it much clearer and easier | |
// to use. As you can see in the 2 places where we call Message::remove_from_Folders(): in Message::operator=, folders.clear() | |
// introduces a bug as illustrated above; in the destructor ~Message(), the member "folders" will be destroyed anyways, why do | |
// we need to clear it first? | |
} | |
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; | |
} |
# Exercise 13.35
如果
Message
使用合成的拷贝控制成员,将会发生什么?
在赋值后一些已存在的 Folders
将会与 Message
不同步。
# Exercise 13.36
设计并实现对应的
Folder
类。此类应该保存一个指向Folder
中包含Message
的set
。
解:
参考 13.34。
# Exercise 13.37
为
Message
类添加成员,实现向folders
添加和删除一个给定的Folder*
。这两个成员类似Folder
类的addMsg
和remMsg
操作。
解:
参考 13.34。
# Exercise 13.38
我们并未使用拷贝交换方式来设计
Message
的赋值运算符。你认为其原因是什么?
对于动态分配内存的例子来说,拷贝交换方式是一种简洁的设计。而这里的 Message
类并不需要动态分配内存,用拷贝交换方式只会增加实现的复杂度。
# Classes That Manage Dynamic Memory
# Exercise 13.39
编写你自己版本的
StrVec
,包括自己版本的reserve
、capacity
和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; | |
} |
# Exercise 13.40
为你的
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()); | |
} |
# Exercise 13.41
在
push_back
中,我们为什么在construct
调用中使用后置递增运算?如果使用前置递增运算的话,会发生什么?
解:
会出现 unconstructed
。
# Exercise 13.42
在你的
TextQuery
和QueryResult
类中用你的StrVec
类代替vector<string>
,以此来测试你的StrVec
类。
解:
略
# Exercise 13.43
重写
free
成员,用for_each
和lambda
来代替for
循环destroy
元素。你更倾向于哪种实现,为什么?
解:
重写:
for_each(elements, first_free, [this](std::string &rhs){ alloc.destroy(&rhs); }); |
更倾向于函数式写法。
# Exercise 13.44
编写标准库
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> | |
// Test reference to http://coolshell.cn/articles/10478.html | |
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; | |
} | |
} |
参考:A trivial String class that designed for write-on-paper in an interview
# Moving Objects
# 对象移动
- 很多拷贝操作后,原对象会被销毁,因此引入移动操作可以大幅度提升性能。
- 在新标准中,我们可以用容器保存不可拷贝的类型,只要它们可以被移动即可。
- 标准库容器、
string
和shared_ptr
类既可以支持移动也支持拷贝。IO
类和unique_ptr
类可以移动但不能拷贝。
# 右值引用
- 新标准引入右值引用以支持移动操作。
- 通过
&&
获得右值引用。 - 只能绑定到一个将要销毁的对象。
- 常规引用可以称之为左值引用。
- 左值持久,右值短暂。
move 函数:
int &&rr2 = std::move(rr1);
move
告诉编译器,我们有一个左值,但我希望像右值一样处理它。- 调用
move
意味着:除了对rr1
赋值或者销毁它外,我们将不再使用它。
# 移动构造函数和移动赋值运算符
- 移动构造函数:
- 第一个参数是该类类型的一个引用,关键是,这个引用参数是一个右值引用。
StrVec::StrVec(StrVec &&s) noexcept{}
- 不分配任何新内存,只是接管给定的内存。
- 移动赋值运算符:
StrVec& StrVec::operator=(StrVec && rhs) noexcept{}
- 移动右值,拷贝左值。
- 如果没有移动构造函数,右值也被拷贝。
- 更新三 / 五法则:如果一个类定义了任何一个拷贝操作,它就应该定义所有五个操作。
- 移动迭代器:
make_move_iterator
函数讲一个普通迭代器转换为一个移动迭代器。
- 建议:小心地使用移动操作,以获得性能提升。
# 右值引用和成员函数
- 区分移动和拷贝的重载函数通常有一个版本接受一个
const T&
,而另一个版本接受一个T&&
。 - 引用限定符:
- 在参数列表后面防止一个
&
,限定只能向可修改的左值赋值而不能向右值赋值。
- 在参数列表后面防止一个
# Exercise 13.45
解释左值引用和右值引用的区别?
解:
定义:
- 常规引用被称为左值引用
- 绑定到右值的引用被称为右值引用。
# Exercise 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(); |
# Exercise 13.47
对你在练习 13.44 中定义的
String
类,为它的拷贝构造函数和拷贝赋值运算符添加一条语句,在每次函数执行时打印一条信息。
解:
参考 13.44。
# Exercise 13.48
定义一个
vector<String>
并在其上多次调用push_back
。运行你的程序,并观察String
被拷贝了多少次。
解:
#include "ex_13_44_47.h" | |
#include <vector> | |
#include <iostream> | |
// Test reference to http://coolshell.cn/articles/10478.html | |
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; | |
} | |
} |
# Exercise 13.49
为你的
StrVec
、String
和Message
类添加一个移动构造函数和一个移动赋值运算符。
解:
略
# Exercise 13.50
在你的
String
类的移动操作中添加打印语句,并重新运行 13.6.1 节的练习 13.48 中的程序,它使用了一个vector<String>
,观察什么时候会避免拷贝。
解:
String baz() | |
{ | |
String ret("world"); | |
return ret; // first avoided | |
} | |
String s5 = baz(); // second avoided |
# Exercise 13.51
虽然
unique_ptr
不能拷贝,但我们在 12.1.5 节中编写了一个clone
函数,它以值的方式返回一个unique_ptr
。解释为什么函数是合法的,以及为什么它能正确工作。
解:
在这里是移动的操作而不是拷贝操作,因此是合法的。
# Exercise 13.52
详细解释第 478 页中的
HasPtr
对象的赋值发生了什么?特别是,一步一步描述hp
、hp2
以及HasPtr
的赋值运算符中的参数rhs
的值发生了什么变化。
解:
左值被拷贝,右值被移动。
# Exercise 13.53
从底层效率的角度看,
HasPtr
的赋值运算符并不理想,解释为什么?为HasPtr
实现一个拷贝赋值运算符和一个移动赋值运算符,并比较你的新的移动赋值运算符中执行的操作和拷贝并交换版本中的执行的操作。
解:
参考:https://stackoverflow.com/questions/21010371/why-is-it-not-efficient-to-use-a-single-assignment-operator-handling-both-copy-a
# Exercise 13.54
如果我们为
HasPtr
定义了移动赋值运算符,但未改变拷贝并交换运算符,会发生什么?编写代码验证你的答案。
解:
error: ambiguous overload for 'operator=' (operand types are 'HasPtr' and 'std::remove_reference<HasPtr&>::type { aka HasPtr }') | |
hp1 = std::move(*pH); | |
^ |
# Exercise 13.55
为你的
StrBlob
添加一个右值引用版本的push_back
。
解:
void push_back(string &&s) { data->push_back(std::move(s)); } |
# Exercise 13.56
如果
sorted
定义如下,会发生什么?
Foo Foo::sorted() const & { | |
Foo ret(*this); | |
return ret.sorted(); | |
} |
解:
会产生递归并且最终溢出。
# Exercise 13.57
如果
sorted
定义如下,会发生什么:
Foo Foo::sorted() const & { return Foo(*this).sorted(); } |
解:
没问题。会调用移动版本。
# Exercise 13.58
编写新版本的
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 & { | |
// Foo ret(*this); | |
// sort(ret.data.begin(), ret.data.end()); | |
// return ret; | |
std::cout << "const &" << std::endl; // debug | |
// Foo ret(*this); | |
// ret.sorted(); // Exercise 13.56 | |
// return ret; | |
return Foo(*this).sorted(); // Exercise 13.57 | |
} | |
int main() | |
{ | |
Foo().sorted(); // call "&&" | |
Foo f; | |
f.sorted(); // call "const &" | |
} |
# Chapter Summary
🍓:)