第十三章 拷贝控制 exce
第十三章 拷贝控制
练习13.1
拷贝构造函数是什么?什么时候使用它?
解:
如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数。当使用拷贝初始化时,我们会用到拷贝构造函数。
练习13.2
解释为什么下面的声明是非法的:
Sales_data::Sales_data(Sales_data rhs);
解:
参数类型应该是引用类型。
练习13.3
当我们拷贝一个
StrBlob
时,会发生什么?拷贝一个StrBlobPtr
呢?
解:
当我们拷贝StrBlob
时,会使 shared_ptr
的引用计数加1。当我们拷贝 StrBlobPtr
时,引用计数不会变化。
练习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处地方使用了拷贝构造函数。
练习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;
};
练习13.6
拷贝赋值运算符是什么?什么时候使用它?合成拷贝赋值运算符完成什么工作?什么时候会生成合成拷贝赋值运算符?
解:
拷贝赋值运算符是一个名为 operator=
的函数。当赋值运算发生时就会用到它。合成拷贝赋值运算符可以用来禁止该类型对象的赋值。如果一个类未定义自己的拷贝赋值运算符,编译器会为它生成一个合成拷贝赋值运算符。
练习13.7
当我们将一个
StrBlob
赋值给另一个StrBlob
时,会发生什么?赋值StrBlobPtr
呢?
解:
会发生浅层复制。
练习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;
};
练习13.9
析构函数是什么?合成析构函数完成什么工作?什么时候会生成合成析构函数?
解:
析构函数是类的一个成员函数,名字由波浪号接类名构成。它没有返回值,也不接受参数。合成析构函数可被用来阻止该类型的对象被销毁。当一个类未定义自己的析构函数时,编译器会为它生成一个合成析构函数。
练习13.10
当一个
StrBlob
对象销毁时会发生什么?一个StrBlobPtr
对象销毁时呢?
解:
当一个 StrBlob
对象被销毁时,shared_ptr
的引用计数会减少。当 StrBlobPtr
对象被销毁时,不影响引用计数。
练习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;
};
练习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
。
练习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;
}
练习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个完全一样的数。
练习13.15
假定
numbered
定义了一个拷贝构造函数,能生成一个新的序列号。这会改变上一题中调用的输出结果吗?如果会改变,为什么?新的输出结果是什么?
解:
会输出3个不同的数。并且这3个数并不是a、b、c当中的数。
练习13.16
如果
f
中的参数是const numbered&
,将会怎样?这会改变输出结果吗?如果会改变,为什么?新的输出结果是什么?
解:
会输出 a、b、c的数。
练习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);
}
练习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;
}
练习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;
};
练习13.20
解释当我们拷贝、赋值或销毁
TextQuery
和QueryResult
类对象时会发生什么?
解:
成员会被复制。
练习13.21
你认为
TextQuery
和QueryResult
类需要定义它们自己版本的拷贝控制成员吗?如果需要,为什么?实现你认为这两个类需要的拷贝控制操作。
解:
合成的版本满足所有的需求。因此不需要自定义拷贝控制成员。
练习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;
};
练习13.23
比较上一节练习中你编写的拷贝控制成员和这一节中的代码。确定你理解了你的代码和我们的代码之间的差异。
解: 查看13.22代码。
练习13.24
如果本节的
HasPtr
版本未定义析构函数,将会发生什么?如果未定义拷贝构造函数,将会发生什么?
解:
如果未定义析构函数,将会发生内存泄漏。如果未定义拷贝构造函数,将会拷贝指针的值,指向同一个地址。
练习13.25
假定希望定义
StrBlob
的类值版本,而且希望继续使用shared_ptr
,这样我们的StrBlobPtr
类就仍能使用指向vector
的weak_ptr
了。你修改后的类将需要一个拷贝的构造函数和一个拷贝赋值运算符,但不需要析构函数。解释拷贝构造函数和拷贝赋值运算符必须要做什么。解释为什么不需要析构函数。
解:
拷贝构造函数和拷贝赋值运算符要重新动态分配内存。因为 StrBlob
使用的是智能指针,当引用计数为0时会自动释放对象,因此不需要析构函数。
练习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;
}
练习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;
};
练习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;
}
练习13.29
解释
swap(HasPtr&, HasPtr&)
中对swap
的调用不会导致递归循环。
解:
这其实是3个不同的函数,参数类型不一样,所以不会导致递归循环。
练习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;
}
练习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;
}
练习13.32
类指针的
HasPtr
版本会从swap
函数收益吗?如果会,得到了什么益处?如果不是,为什么?
解:
不会。类值的版本利用swap
交换指针不用进行内存分配,因此得到了性能上的提升。类指针的版本本来就不用进行内存分配,所以不会得到性能提升。
练习13.33
为什么
Message
的成员save
和remove
的参数是一个Folder&
?为什么我们不能将参数定义为Folder
或是const Folder
?
解:
因为 save
和 remove
操作需要更新指定 Folder
。
练习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;
}
练习13.35
如果
Message
使用合成的拷贝控制成员,将会发生什么?
在赋值后一些已存在的 Folders
将会与 Message
不同步。
练习13.36
设计并实现对应的
Folder
类。此类应该保存一个指向Folder
中包含Message
的set
。
解:
参考13.34。
练习13.37
为
Message
类添加成员,实现向folders
添加和删除一个给定的Folder*
。这两个成员类似Folder
类的addMsg
和remMsg
操作。
解:
参考13.34。
练习13.38
我们并未使用拷贝交换方式来设计
Message
的赋值运算符。你认为其原因是什么?
对于动态分配内存的例子来说,拷贝交换方式是一种简洁的设计。而这里的 Message
类并不需要动态分配内存,用拷贝交换方式只会增加实现的复杂度。
练习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;
}
练习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());
}
练习13.41
在
push_back
中,我们为什么在construct
调用中使用后置递增运算?如果使用前置递增运算的话,会发生什么?
解:
会出现 unconstructed
。
练习13.42
在你的
TextQuery
和QueryResult
类中用你的StrVec
类代替vector<string>
,以此来测试你的StrVec
类。
解:
略
练习13.43
重写
free
成员,用for_each
和lambda
来代替for
循环destroy
元素。你更倾向于哪种实现,为什么?
解:
重写:
for_each(elements, first_free, [this](std::string &rhs){ alloc.destroy(&rhs); });
更倾向于函数式写法。
练习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
练习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
类,为它的拷贝构造函数和拷贝赋值运算符添加一条语句,在每次函数执行时打印一条信息。
解:
参考13.44。
练习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;
}
}
练习13.49
为你的
StrVec
、String
和Message
类添加一个移动构造函数和一个移动赋值运算符。
解:
略
练习13.50
在你的
String
类的移动操作中添加打印语句,并重新运行13.6.1节的练习13.48中的程序,它使用了一个vector<String>
,观察什么时候会避免拷贝。
解:
String baz()
{
String ret("world");
return ret; // first avoided
}
String s5 = baz(); // second avoided
练习13.51
虽然
unique_ptr
不能拷贝,但我们在12.1.5节中编写了一个clone
函数,它以值的方式返回一个unique_ptr
。解释为什么函数是合法的,以及为什么它能正确工作。
解:
在这里是移动的操作而不是拷贝操作,因此是合法的。
练习13.52
详细解释第478页中的
HasPtr
对象的赋值发生了什么?特别是,一步一步描述hp
、hp2
以及HasPtr
的赋值运算符中的参数rhs
的值发生了什么变化。
解:
左值被拷贝,右值被移动。
练习13.53
从底层效率的角度看,
HasPtr
的赋值运算符并不理想,解释为什么?为HasPtr
实现一个拷贝赋值运算符和一个移动赋值运算符,并比较你的新的移动赋值运算符中执行的操作和拷贝并交换版本中的执行的操作。
解:
参考:https://stackoverflow.com/questions/21010371/why-is-it-not-efficient-to-use-a-single-assignment-operator-handling-both-copy-a
练习13.54
如果我们为
HasPtr
定义了移动赋值运算符,但未改变拷贝并交换运算符,会发生什么?编写代码验证你的答案。
解:
error: ambiguous overload for 'operator=' (operand types are 'HasPtr' and 'std::remove_reference<HasPtr&>::type { aka HasPtr }')
hp1 = std::move(*pH);
^
练习13.55
为你的
StrBlob
添加一个右值引用版本的push_back
。
解:
void push_back(string &&s) { data->push_back(std::move(s)); }
练习13.56
如果
sorted
定义如下,会发生什么?
Foo Foo::sorted() const & {
Foo ret(*this);
return ret.sorted();
}
解:
会产生递归并且最终溢出。
练习13.57
如果
sorted
定义如下,会发生什么:
Foo Foo::sorted() const & { return Foo(*this).sorted(); }
解:
没问题。会调用移动版本。
练习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 &"
}