《c++primer》讀書筆記二 複製控制

複製構造函數是一種特殊的構造函數,具有單個形參,該形參(常用const修飾)是對該類型的引用。

1.當定義一個新對象並用一個同類型的對象對它進行初始化時,將顯示使用複製構造函數;

2.當將該類型的對象傳遞給函數或從函數返回該類型的對象時,將隱式使用複製構造函數。

當對象超出作用域或動態分配的對象被刪除時,將自動應用析構函數。析構函數可用於釋放對象構造時或在對象生命期中所獲取的資源。不管類是否定義了自己的析構函數,編譯器都自動執行類中非static數據成員的析構函數。

複製構造函數,賦值操作符和析構函數總稱爲複製控制。有一種特別常見的情況需要類定義自己的複製控制成員的:類具有指針成員。

13.1 複製構造函數

c++支持兩種初始化形式:直接初始化和複製初始化。複製初始化使用"=",直接初始化將初始化式放在圓括號中。

string null_book="9-999-99999-9";//複製

string dot(10,'.');//直接

string empty_copy=string();//複製

string empty_direct;//直接

當形參爲非引用類型的時候,將複製實參的值。類似地,以非引用類型做返回值時,將返回return語句中的值的副本。當形參或返回值爲類類型時,由複製構造函數進行復制。

即使定義了其他構造函數,也會合成複製構造函數。合成複製構造函數的行爲是,執行“逐個成員”初始化,將新對象初始化爲原對象的副本。

“逐個成員”是指編譯器將現有對象的每個非static成員,依次複製到正在創建的對象。合成複製構造函數直接複製內置類型成員的值,類類型成員使用該類的複製構造函數進行復制。數組成員的複製是一個例外。雖然一般不能複製數組,但如果一個類加油數組成員,則合成複製構造函數將複製數組。複製數組時合成複製構造函數將複製數組的每一個元素。

複製構造函數就是接受單個類類型引用形參(通常爲const)的構造函數,如:

class Foo{

Foo();//構造函數

Foo(Const Foo &);//複製構造函數

};

有些類必須對複製對象時發生的事情加以控制。這樣的類經常有一個數據成員是指針,或者有成員表示在構造函數中分配的其他資源。而另一些類在創建新對象時必須做一些特定工作。在這兩種情況下,都必須定義複製構造函數。

複製構造函數的形參並不限制爲const,但必須是一個引用。我自己的理解是:“如果不是引用,形參需調用拷貝構造函數來複制實參,這種調用是不合理的。”

Sales_item::Sales_item(Const Sales_item rhs);//error

爲了防止複製,類必須顯示聲明其複製構造函數爲private。

如果複製構造函數是private,將不允許用戶代碼複製該類類型的對象,編譯器將拒絕任何進行復制的嘗試。然而類的友元和成員仍可進行復制。如果想要連友元和成員中的複製也禁止,就可以聲明一個(private)構造函數但不對其定義。

一般來說,最好顯示或隱式定義默認構造函數和複製函數。只有不存在其他構造函數時才合成默認構造函數。如果定義了複製構造函數,也必須定義默認構造函數。

13.2賦值操作符

class Sales_item{

public:

Sales_item& operator=(const Sales_item&);//賦值操作符

};

合成賦值操作符與合成複製構造函數的操作類似。它逐個成員賦值:右操作數的每個成員賦值給左操作數對象的對應成員。除數組之外,每個成員用所屬類型的常規方式賦值。對數組,每個數組元素賦值。

Sales_item& Sales_item::operator=(const Sales_item &rhs)

{

isbn=rhs.isbn;

units_sold=rhs.units_sold;

revenue=rhs.revenue;

return *this;

}

13.3析構函數

撤銷類對象時會自動調用析構函數:

Sales_item *p=new Sales_item;//調用默認構造函數

{

Sales_item item(*p);//調用拷貝構造函數複製*p到item

delete p;//調用析構函數析構*p

}//變量item在超出作業域時應該自動撤銷。因此,當遇到右花括號時,將運行item的析構函數。

動態分配的對象只有在指向該對象的指針被刪除時才撤銷。如果沒有刪除指向動態對象的指針,則不會與運行該對象的析構函數,對象就一直存在,從而導致內存泄露,而且,對象內部使用的如何資源也不會釋放。

當對象的引用或指針超出作用域時,不會運行析構函數。只有刪除指向動態分配對象的指針或實際對象(而不是引用)超出作用域時,纔會運行析構函數。

撤銷一個容器(標準庫容器或內置數組)時,都會運行容器中類類型元素的析構函數。容器中的元素總是按逆序撤銷:首先撤銷下標爲size()-1的元素,然後是size()-2的元素......直至最後撤銷下標爲0的元素。

#include<iostream>
using namespace std;

class A{
public:
        A();
        ~A();
private:
        static int data;
};

int A::data=0;

A::A(){
        data=data+1;
        cout<<"constructed "<<data<<endl;
}
A::~A(){
        data=data-1;
        cout<<"destructed "<<data<<endl;
}
int main(){
        A *a=new A[10];
        delete []a;//如果不顯示的delete,並不會調用析構函數來釋放動態分配的對象
        return 0;
}

一般如果類需要析構函數,則它需要賦值操作符和複製構造函數,這是一個有用的經驗法則。

合成析構函數按對象創建時的逆序撤銷每個非static成員。

析構函數是個成員函數,它的名字是在類名字之前加上“~”,它沒有返回值,沒有形參,所以不能重載析構函數。析構函數與複製構造函數或賦值操作符之間的一個重要區別是,即使編寫了自己的析構函數,合成析構函數仍然運行。

13.5 管理指針成員

包含指針的類需要特別注意複製控制,原因是複製指針時只需複製指針中的地址,而不會複製指針指向的對象。

大多數C++採用以下三種方法之一管理指針成員:

1.指針成員採取常規指針型行爲。這樣的類具有指針所有的缺陷但無需特殊的複製控制。

2.類可以實現所謂的“智能指針”行爲。指針所指向的對象是共享的,但類能夠防止“懸垂指針”。

3.類採取值型行爲。指針所指向的對象是唯一的,由每個類對象獨立管理。

#include<iostream>
using namespace std;

class HasPtr{
public:
	HasPtr(int *p,int i):ptr(p),val(i){}
	int *get_ptr()const {return ptr;}
	int get_int()const {return val;}
	void set_ptr(int *p){ptr=p;}
	void set_int(int i){val=i;}
	int get_ptr_val()const {return *ptr;}
	void set_ptr_val(int val)const {*ptr=val;}
private:
	int *ptr;
	int val;
};

int main()
{
	int obj=0;
	HasPtr ptr1(&obj,42);
	cout<<ptr1.get_ptr_val()<<'\t'<<ptr1.get_int()<<endl;//0 42
	HasPtr ptr2(ptr1);//複製之後,int的值是獨立的,而指針的值卻糾纏在一起
	cout<<ptr2.get_ptr_val()<<'\t'<<ptr2.get_int()<<endl;//0 42
	ptr1.set_int(0);
	cout<<ptr1.get_int()<<endl;//0
	cout<<ptr2.get_int()<<endl;//42
	ptr1.set_ptr_val(42);
	cout<<ptr2.get_ptr_val()<<endl;//42
	return 0;
}

可能出現懸垂指針。

int *ip=new int(42);

HasPtr ptr(ip,10);

delete ip;

ptr.set_ptr_val(0);

這裏的問題是ip和ptr中的指針指向同一對象。刪除了該對象時,ptr中的指針不再指向有效對象。然而,沒有辦法得知對象已經不存在了。

定義智能指針的通用技術是採用一個使用計數。智能指針類將一個計算器與類指向的對象相關聯。使用計數跟蹤該類有多少個對象共享同一指針。使用計數爲0時,刪除對象。

每次創建類的新對象時,初始化指針並將使用計數置爲1。當對象作爲另一對象的副本而創建時,複製構造函數複製指針並增加與之相應的使用計數的值。對一個對象進行賦值時,賦值操作符減少左操作數所指對象的使用計數的值(如果使用計數減至0,則刪除對象),並增加右操作數所指對象的使用計數的值。最後調用析構函數時,析構函數減少使用計數的值,如果計數減至0,則刪除基礎對象。

使用計數的一種策略是:需要定義一個單獨的具體類用以封裝使用計數和相關指針:

class U_Ptr{

friend class HasPtr;

int *ip;

size_t use;

U_Ptr(int *p):ip(p),use(1){}

~U_Ptr(){delete ip;}

};

class HasPtr{

public:

HasPtr(int *p,int i):ptr(new U_Ptr(p)),val(i){}

HasPtr(const HasPtr &orig):ptr(orig.ptr),val(orig.val){++ptr->use;}

HasPtr& operator= (const HasPtr&);

~HasPtr(){if(--ptr->usr==0) delete ptr;}

private:

U_Ptr *ptr;

int val;

};

HasPtr& HasPre::operator=(const HasPtr &rhs)

{

++rhs->use;

if(--ptr->use==0)

delete ptr;

ptr=rhs.ptr;

val=rhs.val;

return *this;

}

爲了管理具有指針成員的類,必須定義三個複製控制成員:複製構造函數,賦值操作符,析構函數。這些成員可以定義指針成員的指針型行爲或值型行爲。

值型類將指針成員所指的基礎值的副本給每個對象。複製構造函數分配新元素並從被複制對象處複製值,賦值操作符撤銷所保存的原對象並從右操作數向左操作數複製值,析構函數撤銷對象。

“智能指針”的類在對象間共享同一基礎值,從而提供指針行爲。爲了實現智能指針的行爲,類需要保證基礎對象一直存在,直到最後一個副本消失。複製構造函數將指針從舊對象複製到新對象時,會將使用計數加1。賦值操作符將左操作數的使用計數減1並將右操作數的使用計數加1,如果左操作數的使用計數減爲0,賦值操作符必須刪除它所指向的對象,最後,賦值操作符將指針從右操作數複製到左操作數。析構函數將使用計數減1,並且,如果使用計數減至0,就刪除基礎對象。

class HasPtr{

public:

HasPtr(const int &p,int i):ptr(new int(p)),val(i){}

HasPtr(const HasPtr &orig):ptr(new int(*orig.ptr)),val(orig.val){}

HasPtr& operator=(const HasPtr&);

~HasPtr(){delete ptr;}

int get_ptr_val()const {return *ptr;}

int get_int()const {return val;}

void set_ptr(int *p){ptr=p;}

void set_int(int i){val=i;}

int *get_ptr()const {return ptr;}

void set_ptr_val(int p)const { *ptr=p;}

private:

int *ptr;

int val;

};

 

HasPtr& operator=(const HasPtr& rhs)

{

*ptr=*rhs.ptr;

val=rhs.val;

return *this;

}

 


 

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