複製構造函數是一種特殊的構造函數,具有單個形參,該形參(常用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;
}