1. 程式人生 > >c++建構函式詳解及顯式呼叫建構函式

c++建構函式詳解及顯式呼叫建構函式

1>建構函式是幹什麼的

class Counter {
public:
Counter() {// 特點:以類名作為函式名,無返回型別
m_value = 0;
}
private:
int m_value;// 資料成員
}
該類物件被建立時,編譯系統為物件分配記憶體空間,並自動呼叫該建構函式->由建構函式完成成員的初始化工作
eg: Counter c1;
編譯系統為物件c1的每個資料成員(m_value)分配記憶體空間,並呼叫建構函式Counter( )自動地初始化物件c1的m_value=0

建構函式的作用:初始化物件的資料成員。

2>建構函式的種類

class Complex {
private :
double m_real;
double m_imag;
public:
// 無引數建構函式
// 如果類沒寫任何建構函式,則系統會自動生成預設的無參建構函式,函式為空,什麼都不做
// 只要寫了下面某一種建構函式,系統就不會再自動生成這樣一個預設的建構函式,如果希望有一個這樣的無參建構函式,則需要自己顯示地寫出來
// 一般建構函式(也稱過載建構函式),可有各種引數形式,類可以有多個一般建構函式,但彼此引數個數/型別不同(基於c++的過載原理)
Complex(void) {
m_real = 0.0;
m_imag = 0.0;
}

// 複製建構函式(也稱為拷貝建構函式)
// 複製建構函式引數為類物件本身的引用,用於根據一個已存在的物件複製出一個新的該類物件,一般在函式中會將已存在物件的資料成員的值複製一份到新建立的物件中
// 若沒有顯示的寫複製建構函式,則系統會預設建立一個複製建構函式,但當類中有指標成員時,預設的複製建構函式會存在風險(見淺拷貝/深拷貝)
Complex(const Complex & c) { // 將物件c中的資料成員值複製過來
m_real = c.m_real;
m_imag = c.m_imag;
}

//型別轉換建構函式,根據一個指定型別的物件建立一個本類物件
//注意:這個其實就是一般建構函式,但對於單引數建構函式,c++會預設將引數對應的型別轉換為該類型別,有時這種隱式轉換不是想要的,所以要用explicit來限制這種轉換
//eg:將根據一個double型別物件建立一個Complex物件
Complex(double r){
m_real = r;
m_imag = 0.0;
}

// 等號運算子過載(也叫賦值建構函式)
// 注意:這個類似複製建構函式,將=右邊本類物件的值複製給等號左邊的物件,它不屬於建構函式,等號左右兩邊物件必須已經被建立
// 若沒顯示寫=運算子過載,則系統會建立預設的=運算子過載,只做一些基本的拷貝
Complex &operator=( const Complex &rhs ) {
if ( this == &rhs ) {
return *this;
} // 首先檢測右邊物件是否就是左邊物件本身,若是本身,則直接返回
this->m_real = rhs.m_real;  // 複製等號右邊物件的成員到左邊物件中
this->m_imag = rhs.m_imag;
// ret等號左邊物件是為了支援連等 eg>a=b=c系統首先執行b=c,然後執行 a= (b=c的返回值,這裡應該是複製c值後的b物件)
return *this;
}
};

用法:
int main() {
Complex c1,c2; //呼叫了無參建構函式,資料成員初值被賦為0.0
Complex c3(1.0,2.5); //呼叫一般建構函式,資料成員初值被賦為指定值
Complex c3 = Complex(1.0,2.5); //也可以使用下面的形式
c1 = c3; //把c3資料成員的值賦值給c1
//由於c1已事先被建立,故不會呼叫任何建構函式,只調用 = 號運算子過載函式
c2 = 5.2; //呼叫型別轉換建構函式
//系統首先呼叫型別轉換建構函式,將5.2建立為一個本類的臨時物件,然後呼叫等號運算子過載,將該臨時物件賦值給c1
Complex c5(c2); //呼叫拷貝建構函式
Complex c4 = c2; //呼叫拷貝建構函式
// 注意和 = 運算子過載區分,等號左邊物件不是事先已經建立,故需呼叫拷貝建構函式,引數為c2
//這是初始化,不是賦值。其實這涉及了c++兩種初始化的方式:複製初始化和賦值初始化,對應c5和c4,都要呼叫拷貝建構函式。
}

3>思考與測驗

  1. 複製建構函式
    Complex(const Complex & c) {
    m_real = c.m_real; //將物件c中的資料成員值複製過來
    m_img = c.m_img;
    }
    為什麼函式中可以直接訪問物件c的私有成員?
    答:類中函式可以訪問這個類物件的所有成員
  2. 引用與傳值的區別
    Complex test1(const Complex& c) {
    return c;
    }
    Complex test2(const Complex c) {
    return c;
    }
    Complex test3() {
    static Complex c(1.0,5.0);
    return c;
    }
    Complex& test4() {
    static Complex c(1.0,5.0);
    return c;
    }
    void main() {
    Complex a,b;
    // 下面函式執行過程中各會呼叫幾次建構函式,呼叫的是什麼建構函式
    test1(a);
    test2(a);
    b = test3();
    b = test4();
    test2(1.2);
    // 下面這條語句會出錯嗎?
    test1(1.2); //test1( Complex(1.2)) 呢?
    }
    答:為了便於看建構函式的呼叫效果,新增一些輸出資訊,程式碼如下:
class Complex {        
private:
double    m_real;
double    m_imag;
int id;
static int counter;
public:   
Complex(void) {//無引數建構函式
    m_real = 0.0;
    m_imag = 0.0;
    id=(++counter);
    cout<<"Complex(void):id="<<id<<endl;
}  
Complex(double real, double imag){//一般建構函式(也稱過載建構函式)
    m_real = real;
    m_imag = imag;        
    id=(++counter);
    cout<<"Complex(double,double):id="<<id<<endl;
}
Complex(const Complex & c) {//複製建構函式(也稱為拷貝建構函式)        
    m_real = c.m_real;//將物件c中的資料成員值複製過來
    m_imag = c.m_imag;
    id=(++counter);
    cout<<"Complex(const Complex&):id="<<id<<" from id="<<c.id<<endl;
}               
Complex(double r){// 型別轉換建構函式,根據一個指定的型別的物件建立一個本類的物件
    m_real = r;
    m_imag = 0.0;
    id=(++counter);
    cout<<"Complex(double):id="<<id<<endl;
}
~Complex() {
    cout<<"~Complex():id="<<id<<endl;
}
Complex &operator=( const Complex &rhs ) {// 等號運算子過載
    if ( this == &rhs ) {
        return *this;
    }
    this->m_real = rhs.m_real;
    this->m_imag = rhs.m_imag;
    cout<<"operator=(const Complex&):id="<<id<<" from id="<<rhs.id<<endl;
    return *this;
}
};
int Complex::counter=0;
Complex test1(const Complex& c) {
    return c;
}
Complex test2(const Complex c) {
    return c;
}
Complex test3() {
    static Complex c(1.0,5.0);
    return c;
}
Complex& test4() {
    static Complex c(1.0,5.0);
    return c;
}

int main() {
    Complex a,b;
    // 下面函式執行過程中各會呼叫幾次建構函式,呼叫的是什麼建構函式?
    Complex c=test1(a);
    Complex d=test2(a);
    b = test3();
    b = test4();
    Complex e=test2(1.2);
    Complex f=test1(1.2);
    Complex g=test1(Complex(1.2));
}

第1次執行結果:
這裡寫圖片描述
第2次執行結果:
這裡寫圖片描述
第3次執行結果:
這裡寫圖片描述

4>淺拷貝與深拷貝

如果沒有自定義複製建構函式,則系統會建立預設的複製建構函式,但預設複製建構函式只會執行“淺拷貝”,即將被拷貝物件的資料成員的值一一賦值給新建立的物件,若該類的資料成員中有指標成員,則會使得新的物件的指標所指向的地址與被拷貝物件的指標所指向的地址相同,delete該指標時則會導致兩次重複delete而出錯。

class Person {
public :
Person(char * pN) {
    cout << "一般建構函式被呼叫 !\n";
    m_pName = new char[strlen(pN) + 1];//在堆中存放pN所指的字串
    if(m_pName != NULL) {//把形參指標pN所指的字串複製給它
        strcpy(m_pName ,pN);
    }
}                
Person(Person & p) {// 系統建立的預設複製建構函式,只做位模式拷貝          
    m_pName = p.m_pName;//使兩個字串指標指向同一地址位置         
}
~Person() {
    delete m_pName;
}
private :
char * m_pName;
};

void main() { 
    Person man("lujun");
    Person woman(man);// 結果導致man和woman的指標都指向了同一個地址,函式結束析構時,同一個地址被delete兩次
}
Person(Person & chs){//自己設計複製建構函式,實現“深拷貝”
    m_pName=new char[strlen(p.m_pName)+ 1];
    if(m_pName){
        strcpy(m_pName ,chs.m_pName);
    }
// 則新建立的物件的m_pName與原物件chs的m_pName不再指向同一地址了
}

5>建構函式的顯式呼叫

class CTest {
public:
CTest() {  
    m_a = 1;  
}  
CTest(int b) {  
    m_b = b;  
    CTest();  
}  
~CTest() {}  
void show {  
    std::cout << m_a << std::endl;  
    std::cout << m_b << std::endl;  
}  
private:  
int m_a;  
int m_b;  
}; 

void main() {  
    CTest myTest(2);  
    myTest.show();  
}

結果:m_a是一個不確定的值,因為沒有被賦初值,m_b 為2
CTest(int b) {
m_b = b;
CTest();
}
呼叫CTest()實際上是建立了一個匿名臨時CTest類物件,CTest()中賦值 m_a = 1也是對該匿名物件賦值,故定義的myTest的m_a其實沒有被賦值。
即:其實建構函式並不像普通函式那樣進行一段處理,而是建立了一個物件,並且對該物件賦初值,所以顯式呼叫建構函式無法實現給私有成員賦值的目的

不要用一個建構函式顯式呼叫另外一個建構函式,這樣會出現不確定性。其實一些初始化程式碼可以寫在一個單獨的init函式中,然後每一個建構函式都呼叫一下該初始化函式即可。

思考:
CTest *p = NULL;
void func() {
p = new CTest();
}
=右邊顯示呼叫CTest(),是否會產生一個匿名臨時物件a,然後將該匿名臨時物件a的地址賦給指標p? 如果如此,出了func()後,臨時物件a是否會被析構? 那指標p就是野指標了?
答:不會產生臨時物件a,直接將產生的物件指標賦給p