天天看點

C++ 構造函數與析構函數

C++ 構造函數與析構函數

構造函數的基本概念

構造函數:

​ 構造函數是類的一種特殊成員函數,它的名字和類名相同,可以有參數,但是沒有傳回值。類中定義的構造函數在對象生成時被調用,其作用是對對象初始化,進行成員變量指派之類的操作。如果類中沒有定義構造函數,編譯器在編譯過程中會為類生成一個預設的無參構造函數,并不進行任何操作。

​ 構造函數的意義:簡化了對象的初始化工作,有了構造函數就不用專門再寫初始化函數,也不用擔心在生成對象時忘記調用初始化函數。對象名也相當于一個指針,如果沒被初始化就使用将導緻程式出錯。

class Complex{
    private:
    	double real;
    	double img;
    public:
    	// 一個類可以聲明多個重載的構造函數,參數個數或參數類型不同
    	Complex(double r){real = r;}
    	Complex(double real_, double img_ = 0);
    	Complex(Complex c1, Complex c2);
};

Complex::Complex(double real_, double img_){
    real = real_;
    img = img_;
}

Complex::Complex(Complex c1, Complex c2){
    real = c1.real + c2.real;
    img = c1.img + c2.img;
}

int main(){
    Complex c1; // 這種聲明方式是錯誤的,應為定義的構造函數為 Complex(r,i); 缺少構造函數參數
    Complex* pc = new Complex; // 也是錯誤的,沒有參數
    Complex c2(2);
    Complex* pc2 = new Complex(2,3);
    return 0;
}
           

構造函數在數組中的使用:

class Sample{
    private:
    	int x;
    	int y;
    public:
    	Sample(){cout<<"Constructor 1 called"<<endl;}
    	Sample(int x_){x = x_; y = 0; cout<<"Constructor 2 called"<<endl;}
    	Sample(int x_, int y_){x = x_; y = y_; cout<<"Constructor 3 called"<<endl;}
};

int main(){
    Sample array1[2];
    cout<<"step1"<<endl;
    Sample array2[2] = {4,5};
    cout<<"step2"<<endl;
    Sample array3[2] = {3}; // array3[0]用Sample(3)初始化,array3[1]用Sample()初始化
    cout<<"step3"<<endl;
    Sample array4[3] = {3, Sample(4,5)};
    cout<<"step4"<<endl;
    Sample* array4 = new Sample[2];
    cout<<"step5"<<endl;
    Sample* parray[3] = {new Sample(3), new Sample(4,5)}; // 注意和第18行代碼對比,parray是指針,沒有進行初始化就是空指針不會調用構造函數
    delete [] parray;
    delete [] array4; // 被new出來的對象一定要用delete釋放
    return 0;
}
/* output:
Constructor 1 called
Constructor 1 called
step1
Constructor 2 called
Constructor 2 called
step2
Constructor 2 called
Constructor 1 called
step3
Constructor 2 called
Constructor 3 called
Constructor 1 called
step4
Constructor 1 called
Constructor 1 called
step5
Constructor 2 called
Constructor 3 called
*/
           

拷貝構造函數

拷貝構造函數:

​ 拷貝構造函數有且僅有一個同類對象引用的參數 (參數隻能是引用不能是對象),形如

CClass:CClass(const CClass & c)

使用常量對象作為參數更安全,當然也可以不用

const

。如果類中沒有定義拷貝構造函數,編譯器會在編譯過程中為類生成預設的拷貝構造函數,其功能就是完成拷貝功能。

如果拷貝構造函數中的參數不是一個引用,即形如

CClass(const CClass c)

,那麼就相當于采用了傳值的方式,而傳值的方式會調用該類的拷貝構造函數,進而造成無窮遞歸地調用拷貝構造函數。是以拷貝構造函數的參數必須是一個引用。需要澄清的是,傳指針其實也是傳值,如果上面的拷貝構造函數寫成

CClass(const CClass* c)

,也是不行的。事實上,隻有傳引用不是傳值外,其他所有的傳遞方式都是傳值。

class Complex{
    private:
    	double real;
    	double img;
    public:
    	Complex(int r, int i){real = r; img = i;}
    	Complex(const Complex & c);
};

Complex::Complex(const Complex & c){
    real = c.real;
    img = c.img;
    cout<<"Copy Constructor called"<<endl;
}

int main(){
    Complex c1(1,2);
    Complex c2(c1);
}
           

拷貝構造函數起作用的情況

  • 用對象進行初始化:當用一個對象去初始化同類的另一個對象時,會引發拷貝構造函數被調用。
    Complex c2(c1);
    Complex c2 = c1; //調用拷貝構造函數
               
    ​ 上述示例中的兩條語句都會引發拷貝構造函數的調用,這兩條語句是等價的,都用以初始化 c2。值得注意的是,第二條語句是初始化語句,不是指派語句。指派語句的等号左邊通常是一個早已有定義的變量,指派語句不會引發複制構造函數的調用,示例如下:
    Complex c1(1,2);
    Complex c2;
    c2 = c1; //指派語句
               
  • 對象作為函數形參:如果函數 F 的一個參數是類 A 的對象,那麼當 F 被調用時,類 A 的拷貝構造函數将被調用。換句話說,作為形參的對象是用拷貝構造函數初始化的,而且調用拷貝構造函數對其進行初始化的參數就是調用函數時所給的實參。
  • 對象作為函數傳回值:如果函數的返冋值是類 A 的對象,則函數返冋時,類 A 的拷貝構造函數被調用。換言之,作為函數傳回值的對象是用拷貝構造函數初始化的,而調用拷貝構造函數時的實參,就是 return 語句所傳回的對象。
    class A{
        public:
        	int x;
        	A(int n){x=n;}
        	A(const A & a){
                x = a.x; // 這個語句說明,拷貝構造函數的實參和形參的值不一定一樣,這取決于該類的拷貝構造函數的定義方式(x = 100;)
                cout<<"Copy construct called"<<endl;
            }
    };
    
    void F(A a){ // a做為形參,通過拷貝構造函數進行初始化
        cout<<a.x<<endl;
    }
    // 對象形參常引用參數的使用:優點在于減少了生成形參對象是調用拷貝構造函數的開銷,如果需要保證明參的值不被改變加上 const
    void F(const A & a){
        cout<<a.x<<endl;
    }
    
    A Func(){
        A b(2);
        return b; // b作為傳回值,通過拷貝構造函數進行初始化
    }
    
    int main(){
        A a1(1);
        F(a1); // a1作為拷貝構造函數的實參
        cout << Func().v << endl;
        return 0;
    }
               

深拷貝和淺拷貝:

  • 淺拷貝:又稱值拷貝,将源對象的值拷貝到目标對象中去,本質上來說源對象和目标對象共用一份實體,隻是所引用的變量名不同,位址其實還是相同的。(問題:淺拷貝和對象引用的差別是什麼?)
  • 深拷貝:深拷貝的時候先開辟出和源對象大小一樣的空間,然後将源對象裡的内容拷貝到目标對象中去,這樣兩個指針就指向了不同的記憶體位置。
  • 深拷貝和淺拷貝可以簡單了解為:如果一個類擁有資源,當這個類的對象發生複制過程的時候,資源重新配置設定,這個過程就是深拷貝,反之,沒有重新配置設定資源,就是淺拷貝。

類型轉換構造函數

類型轉換構造函數:用途在于自動将其他類型的資料對象轉換為該類對象。類型轉化構造函數和拷貝構造函數類似,隻有一個參數,但是其參數不是該類對象。在需要的時候,編譯系統在編譯過程中自動調用轉換構造函數,建立一個無名的臨時對象。

class Complex(){
    private:
    	double real,img;
    public:
    	Complex(double r, double i){
            real = r;
            img = i;
        }
    	Complex(int n){
            real = n;
            img = 0;
            cout << "Int Constructor called"<<endl;
        }
    	getReal(){return real;}
    	getImg(){return img;}
};

int main(){
    Complex c1(1,2);
    c1 = 3; // 調用類型構造函數,将3轉換一個臨時Complex對象
    cout << c1.getReal() << c1.getImg() << endl;
    return 0;
}
/* output:
3 0
*/
           

析構函數

析構函數:

​ 析構函數與構造函數對應,在對象生命周期結束時被自動調用,析構函數的作用是在對象消亡前做類似釋放記憶體空間等善後工作。其名字和類目相同,在前面加

~

,沒有參數和傳回值,一個類最多隻有一個析構函數。如果類中沒有定義析構函數,編譯器在編譯過程中會生成預設析構函數,并不進行任何操作。

class String{
    private:
    	char* p;
    public:
    	String(){
            p = new char[10]; // new 申請動态記憶體空間
        }
    	~String();
};
String::~String(){
    delete [] p; // delete 釋放動态記憶體空間
}
           

析構函數在數組中的使用:對象數組生命周期結束時,對象數組中的每個元素的析構函數都會被調用。

析構函數與 delete :被 new 出來的對象一定要用 delete 釋放,否則不會調用析構函數去釋放對象;如果 new 的是一個對象數組,那麼使用

delete []

釋放,如果隻使用 delete ,那麼隻會調用一次析構函數釋放一個對象。

構造函數和析構函數的調用時機:值得注意的是,對象作為函數形參和傳回值均會調用拷貝構造函數産生臨時對象,那麼這些對象在函數調用完成之後會自動調用析構函數釋放資源。

class Sample{
    private:
    	int x;
    public:
    	Sample(int n){x=n; cout<<x<<"constructor called"<<endl;}
    	~Sample(){cout<<x<<"destructor called"<<endl;}
};

Sample s1(1); // 全局變量,調用構造函數 

void F(){
    static Sample s2(2); // 靜态局部變量在函數調用結束時不會消亡,整個程式執行完畢之和才會消亡
    Sample s3(3);
    cout << "F called" << endl;
}

int main(){
    Sample s4(4); 
    s4 = 6; // 類型轉換構造函數參數臨時對象 
    cout<<"main"<<endl;
    if(true){
        Sample s5(5); // 局部變量在生命周期結束後調用析構
    }
    F();
    cout<<"main end"<<endl;
    return 0;
}

/* output:
1 constructor called
4 constructor called
6 constructor called
6 destructor called
main
5 constructor called
5 destructor called
2 constructor called
3 constructor called
F called
3 destructor called
main end
6 destructor called (s4)
2 destructor called (static)
1 destructor called (global)
*/
           

​ 值得注意的是,構造函數隻負責初始化工作不負責記憶體空間配置設定,析構函數不負責記憶體空間回收;将對象比作房子,入住之前調用構造函數進行裝修,拆遷之前調用析構函數搬東西。

Reference

拷貝構造函數詳解

深拷貝于淺拷貝

繼續閱讀