在建立一個對象時,通常最需要立即做的工作是初始化對象,如對數據成員賦初值。為了解決對象初始化的問題,C++提供了構造函數來處理對象的初始化。
(一)
構造函數是一種特殊的成員函數,與其它成員函數不同,它不需要人為調用,而是建立對象時自動被執行。C++規定構造函數的名稱與類的名稱相同,並且不能指定返回類型。
比如:
Data (int i,int j) //構造函數
{
a=i;b=j;
}
Data () //構造函數
{
a=1;b=2;
}
定義對象的形式:Data A(1,2); //定義Data對象A,調用構造函數初始化
Data A; //構造函數沒有形參時候
1)構造函數初始化列表
構造函數可以包含一個構造函數初始化列表:
類名(形參)
:構造函數初始化列表 //初始化階段
{ //計算階段
函數體
}
構造函數分為兩個階段進行:初始化階段與計算階段。初始化階段由構造函數初始化列表組成,計算階段由構造函數函數體的所有語句組成,初始化階段先於普通的計算階段。使用初始化列表的構造函數初始化數據成員;而沒有定義初始化列表的構造函數版本在函數體中對數據成員賦值。
不管成員是否在構造函數函數初始化列表中顯式地初始化,類的成員對象初始化總是發生在計算階段之前。
注意:構造函數和其他成員函數一樣,可以定義在類的內部或者外部,但是構造函數的初始化列表只在構造函數的定義中而不是在函數原型聲明中指定。
關於構造函數初始化列表的幾點說明:
a)有時必須使用構造函數初始化列表
如果沒有為類類型的數據成員提供初始化表,編譯器會隱式使用該成員的默認構造函數。如果沒有那個類的默認構造函數,編譯器回報錯。這種情況下,必須提供初始化列表。
2)構造函數重載與帶默認參數的構造函數
Point(){x=y=0;}
Point(int a,int b):x(a),y(b){}
Point m,n(1,2);
系統根據對象建立的形式確定對應的構造函數。
Point(int a=0,int b=0):x(a),y(b){} //帶默認參數的構造函數
Point k ,m(1),n(1,2);
定義k時候沒有給出實參,默認為構造函數中的默認參數;同理m(1),形參a=1,b=0。
從上面可以看到在構造函數中使用默認參數方便且有效,它提供了建立對象時的多個選擇,作用相當於多個重載的構造函數。
3)默認構造函數
定義對象時沒有提供初始化式,就會使用默認構造函數,定義默認構造函數的一般形式:
類名()
{
函數體
}
任何類有且只有一個默認構造函數。如果定義的類中沒有顯示定義任何構造函數,編譯器會自動為該類生成默認構造函數,稱為合成默認構造函數。合成默認構造函數使用與變量初始化相同的規則來初始化成員。具有類類型的成員通過運行各自的默認構造函數來進行初始化。通常在默認構造函數中給成員提供的初始值應該指出該對象是“空的”,即按二進制位置0。
一個類哪怕只定義一個構造函數,編譯器也不會再生成默認構造函數,換言之,如果為類定義一個帶參數的構造函數,還想要無參數的構造函數,就必須自己定義它。
一般地,任何一個類都應該定義一個默認構造函數。因為很多情況下,默認構造函數是編譯器隱式調用的。下面是一個例子:
class Data{
public:
Data(string str):s1(str){}
private:
string s1;
};
class Data1{
public:
Data1(){} //錯誤,Data對象沒有合適的默認構造函數可用
private:
Data one;
};
只要Data1類的構造函數初始化列表中沒有形如one("hello")之類的初始化式,則Data1類的構造函數總是錯的。因為Data1類的構造函數試圖使用Data的默認構造函數,但Data沒有默認構造函數。
4)隱式類型轉換(其他類型轉換為類類型)
class Data{
public:
Data(const string& str=""):s1(str){}
void SetString(const Data& r){s1=r.s1;}
private:
string s1;
}
Data one;
string str="hello";
one.SetString(str);
編譯器使用接收string實參的Data構造函數從str生成一個新的Data對象。
可以禁止由構造函數定義的隱式轉換,方法是通過將構造函數聲明為explicit,來防止在需要隱式轉換的上下文使用構造函數:
class Data{
public:
explicit Data(const string& str=""):s1(str){}
void SetString(const Data& r){s1=r.s1;}
private:
string s1;
}
one.SetString(str); //錯誤,構造函數必須是顯式的
one.SetString(Data(str)); //正確,顯式地構造對象
C++關鍵字explicit用來修飾類的構造函數,指明該構造函數是顯式的。explicit關鍵字只能用於類的內部構造函數聲明上,在類定義外部不能重復它。
一般地,除非有明顯的理由想要定義隱式轉換,否則單形參構造函數應該為explicit。將構造函數設置為explicit可以避免錯誤,如果真需要轉換,可以顯式構造對象。
(二)復制構造函數
只有單個形參,而且該形參是對本類類型對象的引用常量,這樣的構造函數稱為復制構造函數:
類名(const 類名 & obj)
{
}
例子:
class Point{
public:
Point():x(0),y(0){} //默認構造函數
Point(const Point& r):x(r.x).y(r.y){} //復制構造函數
Point(int a,int b):x(a),y(b){} //帶參數的構造函數
Point(const string& str); //帶參數的構造函數
private:
int x,y;
}
Point:Point(const string& srt)
{ //從“x,y”形式的字符串中解析出x和y
char buf[100];
int loc=str.find(','),ylen=str.size()-loc-1;
str.copy(buf,loc,0);buf[loc]=0;x=atoi(buf);
str.copy(buf,ylen,loc+1);buf[ylen]=0;y=atoi(buf);
}
復制構造函數有且只有一個本類類型對象的引用形參,通常使用const限定。形參聲明為引用類型可以減少時間和空間開銷,使用const是必需的,因為復制構造函數只是復制對象,沒有必要改變傳遞過來的對象的值。復制構造函數的功能是利用一個已知的對象來初始化一個被創建的同類的對象。
與復制構造函數對應的對象的定義形式為:
類名 對象名(類對象)
Point b(1,2);
Point c(b);
1)合成復制構造函數
每個類必須有一個復制構造函數。如果沒有定義復制構造函數,編譯器會自動合成一個,稱為合成復制構造函數。與合成默認構造函數不同,即使定義了其他構造函數,也會合成復制構造函數
2)何時使用復制構造函數
a)用一個對象顯式或隱式初始化另一個對象時,即復制初始化時。
C++支持兩種初始化形式,復制初始化與直接初始化。復制初始化使用等號(=),而直接初始化將初始化式放在圓括號中。
當用於類類型對象時,復制初始化與直接初始化是有區別的:直接初始化直接調用與實參匹配的構造函數,而復制初始化總是調用復制構造函數。復制初始化首先使用指定構造函數創建一個臨時對象,然后用復制構造函數將臨時對象復制到正在創建的對象。
Point direct; //直接調用默認構造函數
Point d(1,3); //直接調用Point(int a,int b)構造函數
Point copy=Point(); //復制初始化,調用默認構造函數
Point d1="lzb"; //復制初始化,調用Point(const string &str)構造函數
b)函數參數按值傳遞對象時或函數返回對象時
當函數形參為對象類型,而非指針或引用類型時,函數調用按值傳遞對象,即編譯器調用復制構造函數產生一個實參對象副本傳遞到函數中。
類似地,當以對象類型作為返回值時候,編譯器調用復制構造函數產生一個return語句中的值的副本返回到調用函數。
Point x,y,c;
c=fun(x,y);
c)根據元素初始化式列表初始乎數組元素時
如果沒有為類類型數組提供元素初始化式,則將使用默認構造函數初始化每一個元素。然而,如果使用常規的大括號的數組初值列表形式來初始化數組時,則使用復制初始化來初始化每一個元素。
總的來說,正是有了復制構造函數,函數才能傳遞對象與返回對象,對象數組才能用初值列表的形式初始化。
3)深復制與淺復制
淺復制就是僅僅復制指針變量;深復制是復制指針對應的內容。
(三)總結
1)構造函數的作用是初始化數據成員。如果類的數據成員是const對象,類類型對象,那這些對象的初始化只能在初始化列表中進行。
2)構造函數決定了對象定義時的形式。
3)類有重載的構造函數,或者類帶默認參數的構造函數時候,需要注意不能有沖突的構造函數形式。
4)只要自己定義了任何形式的構造函數,則編譯器不會合成默認構造函數。如果需要默認構造函數,那需要顯式定義無參數的構造函數。
5)除非自己定義復制構造函數,否則類總是會合成一個復制構造函數。復制構造函數的形式為:
類名(const 類名 &obj)
當用一個對象初始化另一個對象,函數參數使用對象,函數返回使用對象或運算中出現臨時對象時,要求類必須要有復制構造函數(無論合成還是自定義的)。
6)單個參數的構造函數形式為:
類名(const 指定數據類型是&obj)
可以實現將指定數據類型轉換為類類型。
7)對象賦值和對象復制概念上,實現上,形式上是不同的。
對象賦值是對一個已經存在的對象賦值,因此必須先定義被賦值的對象,才能進行賦值。而對象的復制則是從無到有建立一個新對象,並且使它與一個已有對象完全相同(包括對象的結構與成員值)。
對象的賦值是因為類重載了賦值運算符,因為任何類默認都會重載賦值運算符,因此任何類的的對象都可以賦值。對象的復制是因為類有復制構造函數,因此任何類的對象在定義時都可以使用復制初始化,函數形成與返回可以是類對象。
Data a,b;
a=b; //對象賦值
Data c(a),d=b; //對象復制
Data fun(Data x,Data y); //函數原型,對象復制