每種型別,無論是內建型別還是類型別,都對該型別物件的一組操作的含有進行了定義。每種型別還定義了建立該型別的物件時會發生什麼——建構函式定義了該類型別物件的初始化。型別還能控制複製、賦值或撤銷該型別的物件時會發生什麼——類通過特殊的成員函式:複製建構函式、賦值操作符和解構函式來控制這些行為。
複製建構函式是一種特殊的建構函式,具有單個形參,該形參是對該類型別的引用。當定義一個新物件並用一個同型別的物件對它進行初始化時,將顯示使用賦值建構函式。當該型別的物件傳遞給函式或從函式返回該型別的物件時,將隱式使用複製建構函式。
解構函式是建構函式的互補:當物件超出作用域或動態分配的物件被刪除時,將自動應用解構函式 。解構函式可用於釋放物件時構造或在物件的生命期中所獲取的資源。不管是否定義解構函式,編譯器將自動執行類中非static資料成員的解構函式。
賦值操作符,和建構函式一樣,它也可以通過指定不同型別的右運算元而過載。
制建構函式、賦值操作符和解構函式總稱為複製控制。
複製建構函式
只有單個形參,而且該形參是本類型別物件的引用(常用const修飾),這樣的建構函式稱為複製建構函式。與預設建構函式一樣,複製建構函式可以由編譯器隱式呼叫。
複製建構函式可用於:
(1)根據另一個同型別的物件顯示或隱式初始化一個物件;
(2)複製一個物件,將它作為實參傳遞給一個函式;
(3)從函式返回時複製一個物件;
(4)初始化順序容器中的元素;
(5)根據元素初始化式列表初始化陣列元素;
物件的定義形式
C++中有兩種初始化形式:複製初始化和直接初始化。複製初始化使用=符號,而直接初始化將初始化式放在圓括號中。
當用於類型別物件時,初始化的複製形式和直接形式有所不同:直接初始化直接呼叫與實參匹配的建構函式,複製初始化總是呼叫複製建構函式。複製初始化首先使用建構函式建立一個臨時物件,然後用複製建構函式將那個臨時物件複製到正在建立的物件:
1 2 3 4 |
string null_book = "9-9999"; //copy-initialization string dots(10, '.'); //direct-initialization string empty_copy = string(); //copy-initialization string empty_direct; //direct-initialization |
對於類型別的物件,只有指定單個實參或顯示建立一個臨時物件用於複製時,才使用複製初始化。
注意:由於不能複製IO型別的物件,所以不能對那些型別的物件使用複製初始化。
形參和返回值
當形參為非引用型別時,將複製實參。類似的,以非引用型別作返回值時,將返回return語句中的值的副本。
當形參或返回值為類型別時,由複製建構函式進行復制。
初始化容器元素
複製建構函式可用於初始化順序容器中的元素。例如,可以用表示容量的單個形參來初始化容器。容器的這種構造方式使用了預設建構函式和複製建構函式。
1 |
vector<string> svec(5); |
編譯器首先使用string預設建構函式建立一個臨時值來初始化svec,然後使用複製建構函式將臨時值複製到svec的每個元素。
建構函式與陣列元素
如果沒有為類型別的陣列提供元素初始化式,則將用預設建構函式初始化每個元素。然後,如果使用常規的花括號括住的陣列初始化列表來提供顯示元素初始化,則使用複製初始化每個元素。根據指定值建立適當型別的元素,然後用複製建構函式將該值複製到相應元素。
1 2 3 4 5 |
Sales_item_primer_eds[] = {string("00000"), string("11111"), string("22222"), Sales_item() }; |
如前三個元素的初始化中所示可以直接指定一個值,用於呼叫元素型別的單個形參建構函式。如果不希望指定實參或指定多個實參,就需要使用完整的建構函式語法。
合成的複製建構函式
如果沒有定義複製建構函式,編譯器會為我們合成一個。與合成的預設建構函式不同,即使定義了其他的建構函式,也會合成複製建構函式。
合成複製建構函式的行為:執行逐個成員初始化,將新物件初始化為原物件的副本。
所謂逐個成員,指的是編譯器將現有物件的每個非static成員,依次複製到正建立的物件。只有一個例外,每個成員的型別決定了複製該成員的含義。合成複製建構函式直接複製內建型別成員的值,類型別成員使用該類的複製建構函式進行復制。陣列成員是個例外,雖然一般不能複製陣列,但如果一個類具有陣列成員,則合成複製建構函式將複製陣列。複製陣列時合成複製建構函式將複製陣列的每一個元素。
對許多類而言,合成複製建構函式只完成必要的工作。只包含類型別成員或內建型別(但不是指標型別)的類,無須顯示地定義複製建構函式,也可以複製。
有些類必須對複製物件時發生的事情加以控制,這樣的類經常有一個資料成員是指標,或者有的成員在建構函式中分配的其他資源。而另一些類在建立新物件時必須做一些特定的工作,這兩種情況下,都必須定義複製建構函式。
禁止複製
有些類需要完全禁止複製,例如iostream類就不允許複製。
為了防止複製,類必須顯式宣告其複製建構函式為private。
如果複製建構函式是私有的,將不允許使用者程式碼複製該類型別的物件,編譯器將拒絕任何進行復制的嘗試。
然而,類的友元和成員仍然可以進行復制。如果想要連友元和成員中的複製也禁止,就可以宣告一個private複製建構函式但不對其定義。
賦值操作符
過載操作符是一些函式,其名字為operator後跟著所定義的操作的符號。因此,通過定義名為operator=的函式,可以對賦值進行定義。形參表必須定義具有與該操作符運算元數目相同的形參(如果操作符為一個成員,則包括隱式this指標)。
當操作符為成員函式時,它的第一個運算元隱式繫結到this指標,有些操作符(包括賦值操作符)必須是定義自己的類的成員。
例如,Sales_item的賦值操作符可以宣告為:
1 2 3 4 5 |
class Sales_item { public: Sales_item& operator=(const Sales_item &); }; |
合成賦值操作符與合成複製建構函式操作類似,它會執行逐個成員賦值:右運算元物件的每個成員賦值給左運算元物件的對應成員。除陣列之外,每個成員用所屬類的常規方式進行賦值。對於陣列,給每個陣列元素賦值。
1 2 3 4 5 6 7 |
Sales_item& Sales_item::operator=(const Sales_item & rhs) { isbn = rhs.isbn; units_sold = rhs.units_sold; revence = rhs.revence; return *this; } |
合成賦值操作符根據成員型別使用適合的內建或類定義的賦值操作符,依次給每個成員賦值,該操作符返回*this,它是對左運算元物件的引用。
解構函式
建構函式的一個用途就是自動獲取資源。解構函式就是與建構函式配套的函式,它是一個特殊的函式,它可以完成所需資源的回收,作為類的建構函式的補充。
撤銷物件時會自動呼叫解構函式。動態分配的物件只有在指向該物件的指標被刪除時才撤銷。如果沒有刪除指向動態物件的指標,則不會執行解構函式,物件一直存在,從而導致記憶體洩露,而且,物件內部使用的任何資源也不會釋放。
當物件的引用或指標超出作用域時,不會執行解構函式。只有刪除指向動態分配物件的指標或實際物件(而不是物件的引用)超出作用域時,才會執行解構函式。
與複製建構函式或賦值操作符不同,編譯器總是會為我們合成一個解構函式。合成解構函式按物件建立時的逆序撤銷,每個非static成員,因此,它按成員在類中宣告的次序的逆序撤銷成員。
即使我們編寫了解構函式,合成解構函式仍然執行。
1 |
~Sales_item(){} |
撤銷Sales_item型別的物件時,將執行這個什麼也不做的解構函式,它執行完畢後,將執行合成解構函式以撤銷類的成員。