1. 程式人生 > >[C++]類的設計(2)——拷貝控制(拷貝控制和資源管理)

[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;
}