建構函式(C++學習筆記 17)
一、建構函式
- 建構函式是屬於某一個類的
- 類是一種抽象的資料型別,它不佔儲存空間,不能容納具體的資料,因此在類宣告中不能給資料成員賦初值。
例如,下面的描述是錯誤的:
class Complex{
double real=0; //錯誤,在類宣告中不能給資料成員賦初值
double imag=0; //錯誤,在類宣告中不能給資料成員賦初值
};
如果一個類中的所有成員,都是公有的,則可以在定義物件時對資料成員進行初始化。
class Complex{
public:
double real;
double imag;
};
Complex c1= {1.1,2.2}; //定義類Complex的物件c1,將c1的資料成員real和imag分別初始化為1.1和2.2
如果類中包含私有的或保護的成員時,就不能用這種方法進行初始化。 這時,可以採用類中的公有成員函式來對物件中的資料成員賦初值,例如:
#include<iostream>
#include<cmath>
using namespace std;
struct Complex{
private:
double real; //複數的實部
double imag; //複數的虛部
public:
void init(double r, double i) //定義函式init,給real和imag賦初值
{
real=r;
imag=i;
}
double abscomplex(){ //定義函式abscomplex,求複數的絕對值
double t;
t=real*real+imag*imag;
return sqrt(t);
}
};
int main()
{
Complex A;
A.init(1.1,2.2);
cout<<"複數的絕對值是:"<<A.abscomplex()<<endl;
return 0;
}
但是,使用成員函式給資料成員賦初值既不方便也容易忘記,甚至可能出錯,所以,C++利用建構函式來完成物件的初始化。
建構函式是一種特殊的成員函式,
建構函式作用: 為物件分配空間,對物件進行初始化,即對資料成員賦初值,這些資料成員通常為私有成員。
要求: ①建構函式的名字必須與類名相同,而不能由使用者任意命名;
②它可以有任意型別的引數,也可以不帶引數(不帶引數的建構函式對物件的初始化是固定的)。但不能具有返回值型別,甚至也不能說明為void型別;
③它不需要由使用者來呼叫,而是在建立物件時自動執行的,因此,“A.Complex(1.1,2.2);”這種用法是錯誤的;
④建構函式必須為公有型別,寫在Public之下;
⑤與普通的成員函式一樣,建構函式的函式體可寫在類體內,也可寫在類體外;
⑥在建構函式的函式體中不僅可以對資料成員賦初值,而且可以包含其它語句,但是,為了保持建構函式的功能清晰,一般不提倡在建構函式中加入與初始化無關的內容
⑦通常需要給每個類定義建構函式,如果沒有給類定義建構函式,則編譯系統自動地生成一個預設建構函式。 例如,編譯系統為類Complex生成一個預設建構函式:Complex::Complex(){};
這個預設的建構函式不帶任何引數,函式體是空的,它只能為物件開闢資料成員儲存空間,而不能給物件中的資料成員賦初值。
例如,為類Complex建立一個建構函式。
class Complex{
private:
double real; //表示複數的實部
double imag; //表示複數的虛部
public:
Complex(double r,double i){ //定義建構函式,其名與類名相同
real=r; //在建構函式中,對私有資料real和imag賦值
imag=i;
}
double abscomplex(){
double t;
t=real*real+imag*imag;
return sqrt(t);
}
};
也可把建構函式的函式體寫在類體外:
class Complex{
private:
double real; //表示複數的實部
double imag; //表示複數的虛部
public:
Complex(double r,double i); //宣告建構函式原型
double abscomplex(); //宣告成員函式原型
};
Complex::Complex(double r,double i){ //在類外定義建構函式
real=r;
imag=i;
}
double Complex::abscomplex(){ //在類外定義成員函式
double t;
t=real*real+imag*imag;
return sqrt(t);
}
建構函式可以不帶引數,不帶引數的建構函式對物件的初始化是固定的:
class Complex{
private:
double real; //表示複數的實部
double imag; //表示複數的虛部
public:
Complex(){ //不帶引數的建構函式
real=0;
imag=0;
}
void init(double r,double i){ //公有成員函式,作為類的外部介面
real=r;
imag=i;
}
double abscomplex(){
double t;
t=real*real+imag*imag;
return sqrt(t);
}
};
int main(){
Complex A;
A.init(1.1,2.2); //呼叫公有成員函式init
cout<<"複數的絕對值是:"<<A.abscomplex()<<endl;
return 0;
}
二、物件的初始化(在建構函式中用賦值語句進行)
在建立物件的同時,採用建構函式給資料成員賦初值,通常有以下兩種形式:
形式1: 類名 物件名[(實參表)];
(這裡的“類名”與建構函式名相同,“實參表”是為建構函式提供的實際引數)
形式2: 類名 *指標變數名=new 類名[ ( 實參表 ) ]
例如, 建立物件的同時,採用形式1,利用建構函式給資料成員賦初值。
#include<iostream>
#include<cmath>
using namespace std;
class Complex{
private:
double real; //表示複數的實部
double imag; //表示複數的虛部
public:
Complex(double r,double i){ //定義建構函式,其名與類名相同
real=r; //在建構函式中,對私有資料real和imag賦值
imag=i;
}
double abscomplex(){
double t;
t=real*real+imag*imag;
return sqrt(t);
}
};
int main(){
Complex A(1.1,2.2); //定義類Complex的物件A時呼叫建構函式Complex
cout<<"複數的絕對值是:"<<A.abscomplex()<<endl;
return 0;
}
從上邊的例子中可看出,在main函式中,沒有顯示呼叫建構函式Complex的語句。建構函式是在定義物件時被系統自動呼叫的。
例如, 採用形式2,利用建構函式給資料成員賦初值。
#include<iostream>
#include<cmath>
using namespace std;
class Complex{
private:
double real; //表示複數的實部
double imag; //表示複數的虛部
public:
Complex(double r,double i){ //定義建構函式,其名與類名相同
real=r; //在建構函式中,對私有資料real和imag賦值
imag=i;
}
double abscomplex(){
double t;
t=real*real+imag*imag;
return sqrt(t);
}
};
int main(){
Complex *pa=new Complex(1.1,2.2);
cout<<"複數的絕對值是:"<<pa->abscomplex()<<endl;
delete pa;
return 0;
}
這時,編譯系統開闢了一段記憶體空間,並在此空間中存放了一個Complex類的物件,同時呼叫了該類的建構函式給資料成員賦初值。這個物件沒有名字,稱為無名物件。但是該物件有地址,這個地址存放在指標pa中,訪問用new建立的物件時一般是不用物件名的,而是通過指標訪問。當new建立的物件使用結束,不再需要它時,可用delete運算子予以釋放。
三、用成員初始化列表對資料成員初始化
- 用成員初始化列表對資料成員初始化,不在函式體內用賦值語句對資料成員初始化,而是在函式首部實現的。
- 成員初始化列表寫法方便、簡練,尤其當需要初始化的資料成員較多時更顯其優越性。
- 對於用const修飾的資料成員,或是引用型別的資料成員,是不允許用賦值語句直接賦值的,因此只能用成員初始化列表對其進行初始化。
- 帶有成員初始化列表的建構函式的一般形式為:
類名::建構函式名([引數表])[:成員初始化列表]
{
//建構函式體
}
成員初始化列表的一般形式為:
資料成員名1(初始值1), 資料成員名1(初始值1), ··· ···
例如, 用成員初始化列表對引用型別的資料成員和const修飾的資料成員初始化。
#include<iostream>
using namespace std;
class A{
public:
A(int x1):x(x1),rx(x),pi(3.14) //用成員初始化列表對引用型別的資料成員rx和const修飾的資料成員pi初始化
{
}
void print(){
cout<<"x="<<x<<" "<<"rx="<<rx<<" "<<"pi="<<pi<<endl;
}
private:
int x;
int ℞ //rx是整形變數的引用
const double pi; //pi是用const修飾的常量
};
int main(){
A a(10);
a.print();
return 0;
}
資料成員是按照它們在類中宣告的順序進行初始化的。
例如, 用成員初始化列表對資料成員進行初始化。
#include<iostream>
using namespace std;
class D{
public:
D(int i):mem2(i),mem1(mem2+1){
cout<<"mem1:"<<mem1<<endl;
cout<<"mem2:"<<mem2<<endl;
}
private:
int mem1;
int mem2;
};
int main(){
D d(15);
return 0;
}
結果為:
在上例中,用“mem2+1”來初始化“mem1”,但是資料成員是按照它們在類中宣告的順序進行初始化的,資料成員mem1應在mem2 之前被初始化,因此,在mem2沒有被初始化時,mem1使用“mem2+1”的值來初始化,所得結果是隨機值,而不是16。
四、建構函式的過載
例如, 建構函式過載的應用。
#include<iostream>
using namespace std;
class Date{
public:
Date(); //無參的建構函式 ,對物件的初始化是固定的
Date(int y,int m,int d); //帶有3個引數的建構函式
void showDate();
private:
int year;
int month;
int day;
};
Date::Date() //定義一個無引數的建構函式
{
year=2018;
month=11;
day=10;
}
Date::Date(int y,int m,int d){
year=y;
month=m;
day=d;
}
inline void Date::showDate(){
cout<<year<<"."<<month<<"."<<day<<endl;
}
int main(){
Date date1;
cout<<"Date1 output:"<<endl;
date1.showDate();
Date date2(2008,8,8);
cout<<"Date2 output:"<<endl;
date2.showDate();
return 0;
}
輸出結果:
注意:
①如果在類中沒有定義建構函式,系統會自動提供一個函式體為空的預設建構函式。但是,只要類中定義了一個建構函式(不一定是無參建構函式),系統將不再給它提供預設建構函式。
例如,
#include<iostream>
using namespace std;
class Location{
public:
Location(int m,int n){
X=m;
Y=n;
}
void init(int initx,int inity){
X=initx;
Y=inity;
}
int getx(){
return X;
}
int gety(){
return Y;
}
private:
int X,Y;
};
int main(){
Location A3;
A3.init(785,900);
cout<<A3.getx()<<" "<<A3.gety()<<endl;
return 0;
}
此程式,在“Location A3;”處出現錯誤,如果改為“Location A3(0,0);”即呼叫了建構函式。
②儘管在一個類中可以包含多個建構函式,但對每一個物件而言,建立物件時只執行其中的一個建構函式。(bad.cpp)
#include<iostream>
#include<stdlib.h>
#include<string>
using namespace std;
class Timer{
public:
Timer(){ //無參建構函式
seconds=0;
}
Timer(const char *t){ //含1個字串引數的建構函式
seconds=atoi(t); //atoi將字串轉換為int
}
Timer(int t){ //含1個整型引數的建構函式
seconds=t;
}
Timer(int min,int sec){ //含2個整型引數的建構函式
seconds=min*60+sec;
}
int gettime(){
return seconds;
}
private:
int seconds;
};
int main(){
Timer a; //定義類Timer的物件a,呼叫無引數的建構函式
Timer b(10); // 定義類Timer的物件b,呼叫含1個整型引數的建構函式
Timer c("20"); //定義類Timer的物件c,呼叫含1個字串引數的建構函式
Timer d(1,10); //定義類Timer的物件d,呼叫含2個整型引數的建構函式
cout<<"seconds1="<<a.gettime()<<endl;
cout<<"seconds2="<<b.gettime()<<endl;
cout<<"seconds3="<<c.gettime()<<endl;
cout<<"seconds4="<<d.gettime()<<endl;
return 0;
}
執行結果:
五、帶預設引數的建構函式
對於帶引數的建構函式,在定義物件時必須給建構函式的形參傳遞引數的值,否則建構函式將不被執行。
例如,帶預設引數的建構函式。
#include<iostream>
#include<cmath>
using namespace std;
class Complex{
public:
Complex(double r=0.0,double i=0.0); //在宣告建構函式時指定預設引數值
double abscomplex();
private:
double real;
double imag;
};
Complex::Complex(double r,double i){
real=r;
imag=i;
}
double Complex::abscomplex(){
double t;
t=real*real+imag*imag;
return sqrt(t);
}
int main(){
Complex S1;
cout<<"複數1的絕對值是:"<<S1.abscomplex()<<endl;
Complex S2(1.1);
cout<<"複數2的絕對值是:"<<S2.abscomplex()<<endl;
Complex S3(1.1,2.2);
cout<<"複數1的絕對值是:"<<S3.abscomplex()<<endl;
}
執行結果:
說明:
在主函式main中定義了3個物件S1,S2,S3,它們都是合法的物件,由於傳遞引數的個數不同,使它們的私有資料成員real和imag取得不同的值。
①定義物件S1時,沒有傳遞引數,所以real和imag均取建構函式的預設值。
②定義物件S2時,只傳遞了一個實參,這個引數傳遞給建構函式的第一個形參,而第2個形參取預設值。
③定義物件S3時,傳遞了兩個實參,這兩個實參分別傳給了real和imag。
注意:
①如果建構函式在類的宣告外定義,那麼預設引數應該在類內宣告建構函式原型時指定,而不能再類外建構函式定義時指定。
②如果建構函式的全部引數都指定了預設值,則在定義物件時可以指定1個或幾個實參,也可以不給出實參,這時的建構函式也屬於預設建構函式,因此不能同時再宣告無引數的預設建構函式:“Complex();”。因為如語句:“Complex S1;” 編譯系統將無法識別應該呼叫哪個建構函式,因此產生二義性。
③在一個類中定義了全部是預設引數的建構函式後,不能再定義過載建構函式,因此,一般不要同時使用建構函式的過載和有預設引數的建構函式。如:
Complex(double r=0.0,double i=0.0); //宣告全部是預設引數的建構函式
Complex(double r); //宣告有1個引數的建構函式
編譯系統將無法判斷應該呼叫哪個建構函式。
六、預設建構函式
預設建構函式的作用:
當 物件被 預設初始化 或 值初始化 時自動執行預設建構函式。
只有當類沒有宣告任何建構函式時,編譯器才會自動地生成預設建構函式。
在實際中,如果定義了其他建構函式,那麼最好也提供一個預設建構函式。
預設初始化 在以下情況下發生:
- 當我們在塊作用域內不使用任何初始值定義一個非靜態變數或者陣列時。
- 當一個類本身含有類型別的成員且使用合成的預設建構函式時。
- 當類型別的成員沒有在建構函式初始值列表中顯式地初始化時。
值初始化 在以下情況發生:
- 在陣列初始化的過程中如果我們提供的初始值數量少於陣列大小時
- 當我們不使用初始值定義一個區域性靜態變數時。
- 當我們通過書寫形式如T( )的表示式顯式地請求值初始化時,其中T是型別名(vector 的一個建構函式只接受一個實參用於說明vector 大小,它就是使用一個這種形式的實參來對它的元素初始化器進行值初始化)。
類必須包含一個預設建構函式以便在上述情況下使用,其中大多數情況非常容易判斷,不那麼明顯的一種情況是類的某些資料成員缺少預設建構函式。