拷貝、賦值和析構函數(稍微進階)

拷貝構造函數

基本形式

注意形參類型是類類型,不能聲明爲explicit,因爲有些地方需要隱式轉換

class Foo
{
public:
    Foo();
    Foo(const Foo&);
};

合成拷貝構造函數

你不聲明構造函數,也會給你一個合成的。淺拷貝問題

調用時機

1.非引用類型形參

2.返回非引用類型

3.使用{}初始化一個數組

4.標準庫容器的push或insert操作。emplace調用的是構造函數。

爲啥形參是引用的

不是引用的話,會一直調用拷貝構造函數,會死循環下去。

拷貝初始化與explicit構造函數

vector構造函數爲explicit類型,所以以下寫法不正確。

vector<int> v1(10);  //直接初始化
veector<int> v2=10; //xxx,構造不被允許

解釋:其實veector<int> v2=10; 包含兩句意思,第一句是調用構造函數創一個臨時對象,第二句是拷貝。

下列語句也不行:

void f(vector<int>);
f(10);  //xxx,也是不允許構造,即第一步都不行
f(vector<int>(10));  //正確

編譯器優化: 

有時編譯器會做出優化。把兩步優化成一步。前提是拷貝構造是private,不是private直接在第二步拷貝時就報錯了。

string null_book = "000000";  //包含兩步,創建對象和拷貝構造

//優化成
string null_book("000000");

 賦值運算符

基本形式

class Foo
{
public:
    Foo& operator=(const Foo&);
};

當重載運算符函數是成員函數時,第一個參數就綁定到this指針。

Sales_data a1,a2;
a1 = a2; //調用=運算符

合成 賦值運算符

你不寫也會送你一個合成的。淺拷貝問題


析構函數

基本形式

class Foo
{
public:
    ~Foo();
};

函數執行流程

構造函數先執行初始化列表,再執行函數體;析構函數先執行函數體,再銷燬成員(逆序銷燬)。

類的析構函數和指針類型成員

調用成員的析構函數,內置類型的成員沒有析構函數。

注意不會自動幫內置指針成員調用delete p;

而智能指針是類類型,具有析構函數。

調用時機(句句箴言)

1.離開作用域被銷燬時。

2.會調用對象的成員的析構函數。

3.容器(標準庫容器或數組)被銷燬時,調用元素的析構。

4. 對new這些動態創建的對象,你得調用delete,纔會幫你調用析構。

5. 臨時對象,它的完整表達式結束時。

注意局部指針或引用離開作用域時,不會自動幫你調用

合成析構函數

你不寫,也會幫你合成一個,但功能受限。


三五法則

1.一般你定義了析構函數,說明裏面有動態內存釋放,所以你必須得定義析構函數。

class HasPtr
{
    public:
        HasPtr(const string&s):ps(new string(s)){}
        ~HasPtr(){
            delete ps;   //需要手動釋放
        }
    private:
        string *ps;
};

如果你不自己定義拷貝函數的話,就會用合成的,會發生淺拷貝

HasPtr f(HasPtr p)  //這裏一次拷貝
{
    HasPtr ret = p; //這裏一次拷貝
    return ret;   //xxx,p和ret都被銷燬了,delete兩次
}

更有甚者,你一調用就銷燬實參的成員了。

HasPtr p("some");
f(p);  //會釋放p.ps
HasPtr q(p);  //xxx,都沒了ps了

2. 定義了拷貝函數也必須定義賦值運算符,反之亦然,如果沒有成員涉及動態內存分配,不需要再定義析構函數。

比如爲對象分配一個新的序號

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章