三五法則以及行爲像值的類和行爲像指針的類

  首先我們說明有三個基本操作可以控制類的拷貝操作:拷貝構造函數、拷貝賦值運算符(這兩個的區別之前有文章講到)、以及析構函數。

  那麼我們什麼時候需要自己構建拷貝構造函數和重載拷貝賦值運算符呢,在三/五法則中給出了自己構建拷貝構造函數和重載拷貝賦值運算符的充分條件。

  三/五法則的內容,當我們需要決定一個類是否要定義它自己版本的拷貝控制成員的時候,一個基本原則就是首先確定這個類是否需要我們自己手動定義一個析構函數,通常,對析構函數的需求要比對拷貝構造函數或賦值運算符的需求更加的明顯。簡單來說需要手動構造析構函數是需要手動構造拷貝構造函數和重載賦值運算符的充分條件。爲什麼這麼說呢?

  舉例如下:

//如果我們爲HasPtr定義了一個析構函數,但使用合成版本的拷貝構造函數和拷貝賦值函數運算符,考慮會發生什麼?
class HasPtr{
	public:
		HasPtr(string &s=string()):ps(new) string(s),i(0){}
		~HasPtr(){delete ps;}
} 
  自然,在這裏我們使用的合成版本的拷貝構造函數和拷貝賦值函數是看不見的,但是實際上默認的函數並沒有開闢新的內存,所以這樣導致的結果是多個對象會指向相同的內存,這樣當一個對象進行析構的時候,其他的對象指向的對象就會指向一個空的位置,而且甚至還會對於一個根本不存在的位置進行delete,這樣對於同一個指針進行delete兩次,這樣的行爲是未定義的。

  那麼一般再這個時候,我們就必須自己來定義拷貝構造函數和拷貝賦值運算符號了,同時,在這個時候我們也有兩種選擇,一種是使類表現的像值一樣,每個對象都保留有自己對應的副本,另一種選擇就是讓類表現得像指針一樣,在後面這種情況中,我們爲了避免出現上面這種連續delete兩次的情況,就需要像智能指針一樣定義一個use來記錄使用次數。

  行爲像值的類和行爲像指針的類

  首先解釋行爲像值,對於行爲像值,實際上就是對於每一個已經實例化的類的對象,對於每一個類管理的資源(這裏C++primer中是針對於可以動態分配內存的容器而言的,實際上應該所有非內置類型需要動態申請的類型都需要這麼做,暫時是只想到),當對象與對象之間進行復制操作的時候,每個對象都保存有一份副本,使得當有一個對象中對應的成員佔有的空間被釋放的時候,對應賦值過的對象中的相應成員不受影響。實際上這樣釋放空間的過程不應該僅僅存在於析構函數中,在我們重載賦值操作符(=)的時候也應該有釋放被賦值的對象原來佔有的類外空間的步驟,在C++primer P453中以string舉例的話,還可以注意得到,爲了避免自身向自身賦值的情況這裏用了一個newp變量來臨時進行存儲,有點類似於原來講swap函數的時候其中temp的意思,但是這裏也要注意,因爲每一個實例化的對象中都要存儲一份對應的成員的話,就不能像行爲像指針的類中賦值操作的那樣直接指針等於指針的方式,因爲在行爲像值的類中是有專門的計數器的,而在行爲像值的類中是沒有的,所以不能用同樣的指針,必須要開闢新的地址。

  C++primer中的例子如下:

HasPtr & HasPtr::operator=(const HasPtr &rhs)
{
	auto newp= new string(*rhs.ps)//進行備份,且這是一個複製過程
		delete ps;//對被賦值放原來的資源進行釋放
	ps =newp;
	i=rhs.i;
	return *this;
}
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){}//複製構造函數(本質上),即使是構造函數,依舊new出了空間,這裏是和合成得複製構造函數
	HasPtr& operator=(const HasPtr &);
	~HasPtr(){delete ps;}//析構函數中同樣加入了這樣一個過程
private:
	string *ps;
	int i; 
};

  對於想要使類的行爲像指針的話,就需要加入新的計數機制,解決的問題就是如果類中有上述所說的默認析構函數不能釋放的資源的時候需要程序員自己決定什麼時候進行資源的釋放。那麼這個時候的機制就有點類似於智能指針了,我們需要在類中加入一個私有成員用於引用計數。那麼按照以上定義的話,行爲像指針的類應該像下面這樣進行定義。

#include<iostream>
using namespace std;

class HasPtr{
    public:
        HasPtr(const string &s=string())://有string參數就執行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();
    private:
        string *ps;
        int i;
        size_t *use;//use一定要爲指針類型
};
    HasPtr::~HasPtr(){//析構函數,注意一點,自動調用析構函數的時候需要*use的值爲1,也就是沒有其他的對象指向這一塊內存了
        if(--*use==0)
            delete ps;
            delete use;
    }
    HasPtr& HasPtr::operator=(const HasPtr &rhs){
        ++*rhs.use;
        if(--*use==0){//如果先進行減*use並釋放內存的操作的話,那麼就無法去處理自我賦值的情況了。
            delete ps;
            delete use;
        }
        ps=rhs.ps;
        i=rhs.i;
        use=rhs.use;
        return *this;        
    }
    int main(){
        return 0;
}


 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章