[C++]类的设计(2)——拷贝控制(拷贝控制和资源管理)

 
1、类的行为分类:看起来像一个值;看起来想一个指针。
    1)类的行为像一个值,意味着他应该有自己的状态。当我们拷贝一个像值的对象时,副本和原对象是完全独立的。改变副本不会对原有对象有任何影响,反之亦然。
    2)行为像指针的类则共享状态。当我们拷贝一个这种类的对象时,副本和原对象使用相同的底层数据。改变副本也会改变原对象,反之亦然。
    3)举例:标准库容器和string类行为像一个值;shared_ptr行为类似指针;IO类型和unique_ptr不允许拷贝或者赋值,他们既不像值也不像指针。
    4)我们如何拷贝指针成员决定了我们的类是具有类值行为还是类指针行为。
 
2、例1——定义类值行为的类
class HasPtr {
public:
       HasPtr(const string &s = string()) :ps(new string(s)), i(0) { cout << "这是普通构造函数" << endl; }
       //定义析构函数,手动删除指针
       ~HasPtr() { delete ps; }
       //定义拷贝构造函数,拷贝指针指向的对象,而不是指针
       HasPtr(const HasPtr &p):ps(new string(*p.ps)),i(p.i){ cout << "这是拷贝构造函数" << endl; }
       //定义拷贝赋值运算符,拷贝参数对象的指针的内容,同时把原来的指针删除
       HasPtr& operator=(const HasPtr &p);
       void setPs(const string &s) { *ps = s; }//改变一下值
       string getS()const { return *ps; }
private:
       string *ps;
       int i;
};
HasPtr& HasPtr::operator=(const HasPtr &rhs) {
       auto newp = new string(*rhs.ps);//自赋值的情况下也正确,因为这是底层string的一份拷贝
       delete ps;//释放原来指向的内存;
       ps = newp;//指向新的内存
       i = rhs.i;
       cout << "这是拷贝赋值运算符" << endl;
       return *this;
}
int main() {
       string s = "jjj";
       HasPtr *lhs=new HasPtr(s);//使用普通构造函数
       HasPtr rhs(*lhs);//使用拷贝构造函数
       HasPtr tmp = *lhs;//使用拷贝构造函数
       delete lhs;//删除lhs对rhs和tmp没影响
       tmp = rhs;//使用拷贝赋值运算符
       rhs.setPs("iiii");//改变rhs对tmp没影响
       cout <<"rhs="<< rhs.getS() << endl;
       cout << "tmp=" << tmp.getS() << endl;
       return 0;
}
赋值运算符的注意点
1)如果将一个对象赋予它自身,赋值运算符也必须正常工作。
2)大多数赋值运算符组合了析构函数和拷贝构造函数的工作,换言之, 赋值=析构+构造
3)一个好的方法是在销毁左侧运算对象资源之前, 先拷贝右侧运算对象。(如果成员都是值的话无所谓删除与否,如果有指针的话一定要先拷贝后删除,然后再重新赋值,这就是2)的含义)
 
3、例2——定义类指针行为的类
 1)使用shared_ptr,不用定义析构函数,也不需要拷贝构造函数和拷贝赋值运算符
/**
* 拷贝控制与资源管理-定义类指针行为的类
* 首先使用shared_ptr
* 然后自己实现引用计数器
* yzy on 2018-12-20
*/
class HasPtr {
public:
       HasPtr(const string &s = string()) :ps(new string(s)), i(0) { cout << "这是普通构造函数" << endl; }
       //不需要拷贝构造函数和拷贝赋值运算符,也不需要析构函数,为了看明白,我们定义一个析构函数
       ~HasPtr() { cout << "调用了析构函数" << endl; }
       void setPs(const string &s) { *ps = s; }
       string getPs() { return *ps; }
private:
       shared_ptr<string>ps;
       int i;
};
int main() {
       string s = "iii";
       HasPtr *hp1=new HasPtr(s);
       HasPtr hp2 = *hp1;//调用合成的拷贝构造函数
       HasPtr hp3(*hp1);//调用合成的拷贝构造函数
       delete hp1;//删除hp1对hp2和hp3没影响,会调用析构函数,但是ps不会释放
       cout << "修改前:hp2="<<hp2.getPs() << endl;
       hp3.setPs("jjjjj");//修改hp3会对hp2产生影响
       cout << "修改后:hp2=" << hp2.getPs() << endl;
       cout << "-------------------------------------" << endl;
       HasPtr *new1 = new HasPtr(s);//产生一个新的shared_ptr
       hp2 = *new1;//调用合成的拷贝赋值运算符
       hp3 = *new1;//调用合成的拷贝赋值运算符,这个时候最开始的shared_ptr应该释放资源了
       cout << "hp2=" << hp2.getPs() << "  hp3=" << hp3.getPs() << endl;
       //手动删除new1
       delete new1;
       cout << "-----------------------------------" << endl;
       return 0;
}

2)自己实现引用计数器

/**
* 使用引用计数器,手动实现类似shared_ptr行为,引用计数器工作方式如下:
* 除了初始化对象外,每个构造函数(拷贝构造函数除外)还要创建一个引用计数,用来记录有多少对象与正在创建的对象共享状态。初始化为1
* 拷贝构造函数不分配新的计数器,而是拷贝给定对象的数据成员,包括计数器。拷贝构造函数递增共享的计数器,指出给定对象的状态又被一个新用户所共享
* 析构函数递减计数器,指出共享状态用户少了一个。如果计数器变为0,则析构函数释放状态
* 拷贝赋值运算符递增右侧对象的计数器,递减左侧对象的计数器。如果左侧运算对象的计数器变为0,则意味着它的共享状态没有用户了,那么拷贝赋值运算符就需要销毁状态。、
* 引用计数器要被所有用户共享,所以采用指针的形式
*/
class HasPtr{
       HasPtr(const string&s=string()):ps(new string(s)),i(0),use(new size_t(1)){}
       //析构函数
       ~HasPtr() {
              --(*use);
              if (*use == 0) { delete ps; delete use; }
       }
       //拷贝构造函数
       HasPtr(const HasPtr &p) :ps(p.ps), i(p.i), use(p.use) { ++(*use); }
       //拷贝赋值运算符
       HasPtr& operator=(const HasPtr&);
private:
       string *ps;
       int i;
       size_t *use;
};
HasPtr& HasPtr::operator=(const HasPtr&p) {
       ++(*p.use);//先加后减,确保自赋值状态下也正确
       --(*use);
       if (*use == 0) {
              delete ps;
              delete use;//不只要删除ps,还要删除use
       }
       ps = p.ps;
       i = p.i;
       use = p.use;
       return *this;
}

猜你喜欢

转载自www.cnblogs.com/zhizhiyu/p/10151505.html