一、拷贝控制与资源管理
- 拷贝构造函数:https://blog.csdn.net/qq_41453285/article/details/88866650
- 拷贝赋值运算符:https://blog.csdn.net/qq_41453285/article/details/88866703
- 前面我们总结了一个定义:只要一个类型定义有析构函数,那么我们几乎可以肯定这个类也需要一个拷贝构造函数与一个拷贝赋值运算符
二、行为像值的类
- 类的行为像一个值,意味着它应该有自己的状态
- 当我们拷贝一个像值的对象时,副本和原对象之间是完全独立的,改变副本不会对原对象有任何影响,反之亦然
定义行为像值的类
- 下面是一个类,我们在其中定义了拷贝构造函数与拷贝赋值运算符:
class 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& rhs); //拷贝辅助运算符 ~HasPtr() { delete ps; } private: std::string *ps; int i; };
- 拷贝赋值运算符定义如下:
//我们这个operator=可以处理自我赋值的情况 HasPtr& HasPtr::operator=(const HasPtr& rhs) { auto newp = new std::string(*(rhs.ps)); delete ps; //释放旧内存 ps = newp; //使用新内存 i = rhs.i; return *this; //返回自身 }
- 赋值运算符的两点注意事项:
- ①必须处理自我赋值的情况
- ②大多数赋值运算符组合了析构函数和拷贝构造函数的工作
赋值运算符自我赋值导致的错误
- 下面是自我赋值导致的错误,如果传入的参数与*this指向同一个对象,那么就会产生未定义的后果:
HasPtr& HasPtr::operator=(const HasPtr& rhs) { delete ps; //释放自身的指针 //如果rhs与*this是同一个对象,那么此处相当于用一个已经释放了的对象来进行构造,那么将出错 ps = new string(*(rhs.ps)); i = rhs.i; return *this; }
三、行为像指针的类
- 行为像指针的类则共享状态。当我们拷贝一个这种类的对象时,副本和原对象使用相同的底层数据。改变副本也会改变原对象,反之亦然
- 对于行为类似指针的类,我们需要为其定义拷贝构造函数和拷贝赋值运算符,来拷贝指针成员本身而不是它指向的string。析构函数不能单方面地释放相关的string。只有当最后一个指向string的HasPtr销毁时,才能释放string
- 指针指针类就是行为像指针的类,例如share_ptr
计数器
- 在这种情况下,我们需要使用引用计数
- 引用计数的工作方式如下:
- 除了初始化对象外,每个构造函数(拷贝构造函数除外)还要创建一个引用计数,用来记录有多少对象与正在创建的对象共享状态。当我们创建一个对象时,只有一个对象共享状态,因此将计数器初始化为1
- 拷贝构造函数不分配新的计数器,而是拷贝给定对象的数据成员,包括计数器。拷贝构造函数递增共享的计数器,指出给定对象的状态又被一个新用户所共享
- 析构函数递减计数器,指出共享状态的用户少了一个。如果计数器变为0,则析构函数释放状态
- 拷贝赋值运算符递增右侧运算对象的计数器,递减左侧运算对象的计数器。如果左侧运算对象的计数器变为0,意味着它的共享状态没有用户了,拷贝赋值运算符就必须效率
- 计数器如何在类中表示:
- 因为每个类都是独立的,计数器不好设计,于是可以在类中定义一个动态内存来表示计数器,这样我们通过修改动态内存就可以将副本和原对象指向相同的计数器(见下面的演示案例)
定义行为像指针的类
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& rhs); ~HasPtr(); private: std::string* ps; int i; std::size_t *use; //引用计数器 };
- 析构函数:当计数器变为0时,表示没有用户使用这份对象了,此时才释放对象
HasPtr::~HasPtr() { if (--(*use) == 0) { delete ps; delete use; } }
- 拷贝赋值运算符:
- 递增右侧运算对象的引用计数
- 递减左侧运算对象的引用计数(在必要时释放内存)
- 必须处理自我赋值:先递增rhs中计数然后再递减左侧对象中的技术来实现这一点。当两个对象相同时,在检查ps(及use)是否应该释放之前,计数器就已经递减过了
HasPtr& HasPtr::operator=(const HasPtr& rhs) { ++(*rhs.use); //递增右侧对象计数器 ///递减左侧对象计数器,如果为0就释放 if (--(*use) == 0) { delete ps; delete use; } //下面是相关的赋值 ps = rhs.ps; i = rhs.i; use = rhs.use; //返回自身 return *this; }