《C++ Primer》第13章 拷贝控制
13.2节 拷贝控制和资源管理 习题答案
练习13.22:假定我们希望HasPtr的行为像一个值。即,对于对象所指向的string成员,每个对象都有一份自己的拷贝。我们将在下一节介绍拷贝控制成员的定义。但是,你已经学习了定义这些成员所需的所有知识。在继续学习下一节之前,为HasPtr编写拷贝构造函数和拷贝赋值运算符。
【出题思路】
本题练习如何让一个类“行为像值”。
【解答】
在之前的习题中,我们已经为HasPtr定义了拷贝构造函数和拷贝赋值运算符,两者相结合,再加上析构函数(delete ps即可),已可达到目的。
#include <iostream>
#include <string>
using std::cout;
using std::endl;
using std::string;
class HasPtr
{
public:
HasPtr(const string &s = string())
:ps(new string(s)), i(0)
{
}
HasPtr(const HasPtr &p) //拷贝构造函数
:ps(new string(*p.ps)), i(p.i)
{
}
HasPtr& operator=(const HasPtr&); //拷贝赋值运算符
HasPtr& operator=(const string&); //赋予新string
string& operator*(); //解引用
~HasPtr();
private:
string *ps;
int i;
};
HasPtr::~HasPtr()
{
delete ps;//释放string内存
}
inline HasPtr& HasPtr::operator=(const HasPtr &rhs)
{
auto newps = new string(*rhs.ps); //拷贝指针指向的对象
delete ps; //销毁原string
ps = newps; //指向新string
i = rhs.i; //使用内置的int赋值
return *this; //返回一个此对象的引用
}
HasPtr& HasPtr::operator=(const string &rhs)
{
*ps = rhs;
return *this;
}
string& HasPtr::operator*()
{
return *ps;
}
int main(int argc, const char * argv[])
{
HasPtr h("hi mom!");
HasPtr h2(h);//行为类值,h2, h3和h指向不同string
HasPtr h3 = h;
h2 = "hi dad!";
h3 = "hi son!";
cout << "h: " << *h << endl;
cout << "h2: " << *h2 << endl;
cout << "h3: " << *h3 << endl;
std::cout << "Hello, World!\n";
return 0;
}
运行结果
练习13.23:比较上一节练习中你编写的拷贝控制成员和这一节中的代码。确定你理解了你的代码和我们的代码之间的差异(如果有的话)。
【出题思路】
理解拷贝控制成员的规范写法。
【解答】
请仔细体会拷贝赋值运算符的规范写法,它是如何保证自赋值安全的。
练习13.24:如果本节中的HasPtr版本未定义析构函数,将会发生什么?如果未定义拷贝构造函数,将会发生什么?
【出题思路】
理解拷贝控制成员的作用。
【解答】
如果未定义析构函数,在销毁HasPtr对象时合成的析构函数不会释放指针ps指向的内存,造成内存泄漏。如果未定义拷贝构造函数,在拷贝HasPtr对象时,合成的拷贝构造函数会简单复制ps成员,使得两个HasPtr指向相同的string。当其中一个HasPtr修改string内容时,另一个HasPtr也被改变,这并不符合我们的设想。如果同时定义了析构函数,情况会更为糟糕,当销毁其中一个HasPtr时,ps指向的string被销毁,另一个HasPtr的ps成为空悬指针。
练习13.25:假定希望定义StrBlob的类值版本,而且我们希望继续使用shared_ptr,这样我们的StrBlobPtr类就仍能使用指向vector的weak_ptr了。你修改后的类将需要一个拷贝构造函数和一个拷贝赋值运算符,但不需要析构函数。解释拷贝构造函数和拷贝赋值运算符必须要做什么。解释为什么不需要析构函数。
【出题思路】
本题综合练习拷贝控制成员的使用。
【解答】
由于希望StrBlob的行为像值一样,因此在拷贝构造函数和拷贝赋值运算符中,我们应该将其数据——string的vector拷贝一份,使得两个StrBlob对象指向各自的数据,而不是简单拷贝shared_ptr使得两个StrBlob指向同一个vector。
StrBlob不需要析构函数的原因是,它管理的全部资源就是string的vector,而这是由shared_ptr负责管理的。当一个StrBlob对象销毁时,会调用shared_ptr的析构函数,它会正确调整引用计数,当需要时(引用计数变为0)释放vector。即,shared_ptr保证了资源分配、释放的正确性,StrBlob就不必进行相应的处理了。
练习13.26:对上一题中描述的StrBlob类,编写你自己的版本。
【出题思路】
本题综合练习使用拷贝控制成员实现类值行为。
【解答】
程序如下所示。可以看到,虽然主程序与练习12.19一样,但由于我们定义了拷贝构造函数和拷贝赋值运算符,使得StrBlob的行为像值一样,因此b2和b1、b3和b1不再共享vector,而是都指向自己的拷贝。当向其中之一添加元素时,另一个的内容不会发生改变。读者可以注释掉拷贝构造函数与(或)拷贝赋值运算符,观察有无拷贝控制成员程序输出结果的不同。
另外一个值得注意的是拷贝赋值运算符的写法,由于StrBlob是用shared_ptr而非内置指针类型来管理动态对象,因此直接将新创建的shared_ptr赋予了data,这不会导致自赋值错误。data指向新的动态对象,引用计数为1;而shared_ptr的赋值运算符会将data原来指向的对象的引用计数减1。当进行自赋值时,这显然不会导致非法指针问题,语义也是合理的——data脱离原共享对象,指向与原对象内容相同的新对象。
#ifndef STRBLOB13_26_H
#define STRBLOB13_26_H
#include <vector>
#include <string>
#include <initializer_list>
#include <memory>
#include <stdexcept>
using namespace std;
//提前声明,StrBlob中的友类声明所需
class StrBlobPtr;
class StrBlob{
friend class StrBlobPtr;
public:
typedef vector<string>::size_type size_type;
StrBlob();
StrBlob(initializer_list<string> i1);
StrBlob(vector<string> *p);
StrBlob(StrBlob &s);
StrBlob& operator=(StrBlob &rhs);
size_type size() const
{
return data->size();
}
bool empty() const
{
return data->empty();
}
//添加和删除元素
void push_back(const string &t)
{
data->push_back(t);
}
void pop_back();
//元素访问
string& front();
const string& front() const;
string& back();
const string& back() const;
//提供给StrBlobPtr的接口
StrBlobPtr begin();//定义StrBlobPtr后才能定义这两个函数
StrBlobPtr end();
//const版本
StrBlobPtr begin() const;
StrBlobPtr end() const;
private:
shared_ptr<std::vector<std::string>> data;
//如果data[i]不合法,抛出一个异常
void check(size_type i, const std::string &msg) const;
};
inline StrBlob::StrBlob()
:data(make_shared<vector<string>>())
{
}
inline StrBlob::StrBlob(initializer_list<string> i1)
:data(make_shared<vector<string>>(i1))
{
}
inline StrBlob::StrBlob(vector<string> *p)
:data(p)
{
}
inline StrBlob::StrBlob(StrBlob &s)
:data(make_shared<vector<string>>(*s.data))
{
}
inline StrBlob& StrBlob::operator=(StrBlob &rhs)
{
data = make_shared<vector<string>>(*rhs.data);
return *this;
}
inline void StrBlob::check(size_type i, const string &msg) const
{
if(i >= data->size())
throw out_of_range(msg);
}
inline string& StrBlob::front()
{
//如果vector为空,check会抛出一个异常
check(0, "front on empty StrBlob");
return data->front();
}
//const版本front
inline const string& StrBlob::front() const
{
check(0, "front on empty StrBlob");
return data->front();
}
inline string& StrBlob::back()
{
check(0, "back on empty StrBlob");
return data->back();
}
//const版本back
inline const string& StrBlob::back() const
{
check(0, "back on empty StrBlob");
return data->back();
}
inline void StrBlob::pop_back()
{
check(0, "back on empty StrBlob");
data->pop_back();
}
//当试访问一个不存的元素时,StrBlobPtr抛出一个异常
class StrBlobPtr
{
friend bool eq(const StrBlobPtr&, const StrBlobPtr&);
public:
StrBlobPtr():curr(0)
{
}
StrBlobPtr(StrBlob &a, size_t sz = 0)
:wptr(a.data), curr(sz)
{
}
StrBlobPtr(const StrBlob &a, size_t sz = 0)
:wptr(a.data), curr(sz)
{
}
string& deref() const;
string& deref(int off) const;
StrBlobPtr& incr();//前缀递增
StrBlobPtr& decr();//前缀递减
private:
//若检查成功,check返回一个指向vector的shared_ptr
shared_ptr<vector<string>> check(size_t, const string&) const;
//保存一个weak_ptr, 意味着底层vector可能会被销毁
weak_ptr<vector<string>> wptr;
size_t curr; //在数组中的当前位置
};
inline shared_ptr<vector<string>> StrBlobPtr::check(size_t i, const string &msg) const
{
auto ret = wptr.lock();//vector还存在吗?
if(!ret)
{
throw runtime_error("unbound StrBlobPtr");
}
if(i >= ret->size())
{
throw out_of_range(msg);
}
return ret;//否则,返回指向vector的shared_ptr
}
inline string& StrBlobPtr::deref() const
{
auto p = check(curr, "dereference past end");
return (*p)[curr];//(*p)是对象所指向的vector
}
inline string& StrBlobPtr::deref(int off) const
{
auto p = check(curr + off, "dereference past end");
return (*p)[curr + off];//(*p)是对象所指向的vector
}
//前缀递增:返回递增后的对象的引用
inline StrBlobPtr& StrBlobPtr::incr()
{
//如果curr已经指向容器的尾后位置,就不能递增它
check(curr, "increment past end of StrBlobPtr");
++curr;//推进当前位置
return *this;
}
//前缀递减:返回递减后的对象引用
inline StrBlobPtr& StrBlobPtr::decr()
{
//如果curr已经为0,递减它就会产生一个非法下标
--curr;//递减当前位置
check(-1, "decrement past begin of StrBlobPtr");
return *this;
}
//StrBlob的begin和end成员的定义
inline StrBlobPtr StrBlob::begin()
{
return StrBlobPtr(*this);
}
inline StrBlobPtr StrBlob::end()
{
auto ret = StrBlobPtr(*this, data->size());
return ret;
}
//const版本
inline StrBlobPtr StrBlob::begin() const
{
return StrBlobPtr(*this);
}
inline StrBlobPtr StrBlob::end() const
{
auto ret = StrBlobPtr(*this, data->size());
return ret;
}
//StrBlobPtr的比较操作
inline bool eq(const StrBlobPtr &lhs, const StrBlobPtr &rhs)
{
auto l = lhs.wptr.lock(), r = rhs.wptr.lock();
//若底层的vector是同一个
if(l == r)
{
//则两个指针都是空,或者指向相同元素时,它们相等
return (!r || lhs.curr == rhs.curr);
}
else
{
return false;//若指向不同vector,则不可能相等
}
}
inline bool neq(const StrBlobPtr &lhs, const StrBlobPtr &rhs)
{
return !eq(lhs, rhs);
}
#endif // STRBLOB13_26_H
#include "StrBlob13_26.h"
#include <iostream>
#include "StrBlob13_26.h"
int main(int argc, const char * argv[])
{
StrBlob b1;
{
StrBlob b2 = {"a", "an", "the"};
b1 = b2;
b2.push_back("about");
cout << "b2大小为" << b2.size() << endl;
cout << "b2首尾无素为" << b2.front() << " " << b2.back() << endl;
}
cout << "b1大小为" << b1.size() << endl;
cout << "b1首尾无素为" << b1.front() << " " << b1.back() << endl;
StrBlob b3 = b1;
b3.push_back("next");
cout << "b3大小为" << b3.size() << endl;
cout << "b3首尾无素为" << b3.front() << " " << b3.back() << endl;
cout << "b1全部元素:" << endl;
for(auto it = b1.begin(); neq(it, b1.end()); it.incr())
cout << it.deref() << endl;
std::cout << "Hello, World!\n";
return 0;
}
运行结果:
练习13.27:定义你自己的使用引用计数版本的HasPtr。
【出题思路】
本题练习实现类指针行为。
【解答】
参考本节内容,即可实现如下程序。请编译运行它,观察输出结果。
#include <iostream>
#include <string>
using namespace std;
class HasPtr
{
public:
//构造函数分配新的string和新的计数器,将计数器置为1
HasPtr(const string &s = string())
:ps(new string(s)), i(0), use(new size_t(1))
{
}
//拷贝构造函数拷贝所有三个数据成员,并递增计数器
HasPtr(const HasPtr &p)
:ps(p.ps), i(p.i), use(p.use) //拷贝构造函数
{
++*use;
}
HasPtr& operator=(const HasPtr&); //拷贝构造函数
HasPtr& operator=(const string&); //拷贝赋值运算符
string& operator*(); //解引用
~HasPtr();
private:
string *ps;
int i;
size_t *use; //用来记录有多少个对象共享*ps的成员
};
HasPtr::~HasPtr()
{
if(--*use == 0) //如果引用计数变为0
{
delete ps; //释放string内存
delete use; //释放计数器内存
}
}
HasPtr& HasPtr::operator=(const HasPtr &rhs)
{
++*rhs.use; //递增右侧运算对象的引用计数
if(--*use == 0) //然后递减本对象的引用计数
{
delete ps; //如果没有其他用户
delete use; //释放本对象分配的成员
}
ps = rhs.ps; //将数据从rhs拷贝到本对象
i = rhs.i;
use = rhs.use;
return *this; //返回本对象
}
HasPtr& HasPtr::operator=(const string &rhs)
{
*ps = rhs;
return *this;
}
string& HasPtr::operator*()
{
return *ps;
}
int main(int argc, const char * argv[])
{
HasPtr h("hi tome!");
HasPtr h2 = h;//未分配新string, h2和h指向相同的string
h = "hi camel!";
cout << "h: " << *h << endl;
cout << "h2: " << *h2 << endl;
std::cout << "Hello, World!\n";
return 0;
}
运行结果:
练习13.28:给定下面的类,为其实现一个默认构造函数和必要的拷贝控制成员。
(a) class TreeNode { (b) class BinStrTree {
private: private:
std::string value; TreeNode *root;
int count; }
TreeNode *left;
TreeNode *right;
}
【出题思路】
本题练习根据问题的实际需求设计拷贝控制成员。
【解答】
这是一个二叉树数据结构,若实现类指针行为,且count用作引用计数,默认构造函数如下:
TreeNode::TreeNode()
:value(""),count(1),left(nullptr),right(nullptr){}
BinStrTree::BinStrTree():root(nullptr){}
还可定义其他构造函数,如:
TreeNode::TreeNode(const string &s = string(),
TreeNode *lchild = nullptr, TreeNode *rchild = nullptr)
:value(s), count(1), left(lchild), right(rchild){}
BinStrTree::BinStrTree(TreeNode *t = nullptr):root(t) {}
当然,为了创建出二叉树,还会有创建节点、设置左右孩子节点等函数,但不是本题的重点,因此不再讨论。由于希望它的行为类指针,因此需要定义拷贝构造函数和拷贝赋值运算符来拷贝树节点指针而非树节点,并调整引用计数。还需定义析构函数,减少引用次数,当引用计数变为0时释放内存。需要注意的是,二叉树结构并非单一的节点,而是多层结构,需要递归遍历左右子树中的所有节点,进行相应的操作。