C++ 構造函數的理解

C++構造函數的理解

相對於C語言來講,C++有一個比較好的特性就是構造函數,即類經過一個或者幾個特殊的成員函數來控制其對象的初始化過程。構造函數的任務,就是初始化對象的數據成員,不管什麼時候只要類的對象被建立,就會執行構造函數。c++

構造函數的語法

構造函數的名字必須和類名相同,與其餘函數不同的是,構造函數沒有返回值,並且其必須是公有成員,由於私有成員不容許外部訪問,且函數不能聲明爲const類型,構造函數的語法是這樣的:函數

class Test
{
    public:
        Test(){std::cout<<"Hello world!"<<std::endl;}
};
Test object; 
int main(){return 1;}

在main函數執行以前,object被定義時就會調用Test函數,輸出"Hello world!"。this

這裏只是示範了一個最簡單的構造函數的形式,其實構造函數是個比較複雜的部分,有很是多神奇的特性。指針

構造函數的種類

默認構造函數

當咱們程序中並無顯式的定義構造函數時,系統會提供一個默認的構造函數,這種編譯器建立的構造函數又被稱爲合成的默認構造函數,合成構造函數的初始化規則是這樣的:c++11

  • 若是存在類內的初始值,用它來初始化成員。在C++11的新特性中,C++11支持爲類內的數據成員提供一個初始值,建立對象時,類內初始值將用於初始化數據成員。若是在構造函數中又顯式地初始化了數據成員,則使用顯式初始化的值。
  • 不然,默認初始化該成員。默認初始化意味着和C語言同樣的初始化方式,當類對象爲全局變量時,在系統加載時初始化爲0,而做爲局部變量時,因爲數據在棧上分配,成員變量值不肯定。

須要注意的是,只有當用戶沒有顯式地定義構造函數時,編譯器纔會爲其定義默認構造函數。code

在某些狀況下,默認構造函數是不合適的:對象

  • 如上所說,內部定義的類調用默認構造函數會致使成員函數的值是未定義的。
  • 若是類中包含其餘類類型的數據成員或者繼承自其餘類,且這個類沒有默認構造函數,那麼編譯器將沒法初始化該成員。上面提到了能夠在類內給成員一個初始值,可是這隻對於普通變量,並不支持類的構造。
    當咱們除了自定義的其餘構造函數,還須要一個默認構造函數時,咱們能夠這樣定義:繼承

    Test() = default;
    這個構造函數不接受任何參數,等於默認構造函數。接口

初始化列表的構造方式

首先,咱們先須要分清初始化和賦值的概念,初始化就是在新建立對象的時候給予初值,而賦值是在兩個已經存在的對象之間進行操做。在構造方式上,這兩種是不一樣的。內存

構造函數支持初始化列表,它負責爲新建立的對象的一個或者幾個數據成員賦初值,初始化列表的語法是這樣的:

class Test
{
    public:
        Test(int a):x(a),y(2){}
        int x;
        int y;
};

初始化的列表的一個優點是時間效率和空間效率比賦值要高,同時在const類型成員的構造時,普通的賦值構造函數是非法的。當咱們建立一個const對象時,直到構造函數完成初始化過程,對象才能真正取得其常量屬性。

因此咱們能夠用這種方式爲const成員變量寫值。

拷貝構造函數

拷貝構造函數的通常形式是這樣的:

class Test
{
    public:
        Test(const Test &ob){
            x = ob.x;
            y = ob.y;
        }
    private:
        int x;
        int y;
};

能夠很清楚地看出來,構造過程就是將另外一個同類對象的成員變量一一賦值,const修飾是由於限定傳入對象的只讀屬性。看到上面的示例,不知道有沒有朋友有所疑問:

爲何在構造函數中,用戶能夠訪問到外部同類對象ob的私有變量,不是說私有變量只能經過類的公共函數(通常是get()方法)來訪問嗎,爲何這裏能夠直接使用ob.x,ob.y ??

若是你有這樣的問題,首先不得不認可你是個善於觀察且有必定基礎的學者,可是對封裝的概念並非很清楚。

其實不只僅構造函數能夠訪問同類對象的私有變量,普通成員函數也能夠訪問:

class Test
{
    public:
        Test(){};
        void func(const Test& ob){
            std::cout<<ob.x<<std::endl;
        }
        
    private:
        int x=2;
};

這樣的寫法不會報錯且可以正常運行,可是若是func()的函數是這樣的:

void func(const AnotherClass& ob){
            std::cout<<ob.x<<std::endl;
        }

那咱們還能不能訪問ob的私有變量呢?答案確定是不行的,這不用說。那咱們回到上面的問題,爲何能夠訪問同類對象的私有變量?

其實答案並不難理解,類的封裝性是針對類而不是針對類對象。

通俗地來講,咱們定義類中成員訪問權限的初衷是爲了保護私有成員不被外部其餘對象訪問到,通常狀況下私有成員被外部訪問的方式就是經過公共的函數接口(public),而在類的內部,任何成員函數都能訪問私有成員,這種保護是針對不一樣的類之間的,因此咱們是在定義類的時候來指定訪問權限,而不是在定義對象的時候再指定訪問權限。

再者,相同類對象,對於全部的私有變量,彼此知根知底,也就沒有什麼保護的必要。

既然是這樣,類內的構造函數以及其它函數都是類的成員函數,天然能夠訪問全部數據。

賦值運算符重載

同時,類的構造能夠用重載賦值運算符來實現,即"="。

class Test
{
    public:
        Test& operator=(const Test &ob){
            x = ob.x;
            y = ob.y;
            return this;
        }
    private:
        int x;
        int y;
};

在定義類的時候,咱們能夠這樣:

Test ob1;
Test ob2 = ob1;

默認拷貝構造函數的陷阱

當咱們沒有指定拷貝構造函數或者沒有重載賦值運算符時,系統會生成默認的對應構造函數,分別爲合成拷貝構造函數和合成拷貝賦值運算符。即便用戶沒有在類中定義相對應拷貝賦值操做,咱們照樣可使用它:

Test ob1;
Test ob2(ob1);
Test ob3 = ob2;

編譯器生成的默認拷貝賦值構造函數會將對應的成員一一賦值,是否是很是方便?

既然編譯器生成的默認拷貝賦值構造函數就能完成任務,爲何咱們還要本身去定義構造函數呢?這是否是畫蛇添足?

非也!!!

若是類型成員所有都是普通變量是沒有問題的,可是若是涉及到指針,簡簡單單地複製指針也是沒有問題的,最要命的是若是指針指向的動態內存,這樣就會有兩個不一樣類的成員指向同一片動態內存,而析構函數在釋放內存時,必然形成double free,咱們能夠看下面的例子:

class Test
{
    public:
        Test(){p = new int(4);}
        ~Test(){delete p;}
        int *p;
};
Test ob1;
Test ob2 = ob1;
int main(){}

而後編譯運行:

g++ -std=c++11 test.cpp -o test
./test

這段程序不作任何事,僅僅是經過編譯器生成的合成拷貝賦值運算符,運行結果:

*** Error in `./a.out': double free or corruption (fasttop): 0x085dca10 *** Aborted (core dumped)

很明顯,和上面所提到的同樣,動態內存的double free致使程序終止。爲了觀衆朋友們能更清晰地理解這個過程,咱們再來對程序作一個step by step解析:

  • 構造類對象ob1,這是調用了構造函數,爲ob1.p分配了內存空間。
  • 用合成拷貝賦值構造函數構造類對象ob2 = ob1,至關於執行了語句:ob2.p = ob1.p;
  • main()函數執行完畢,全局函數的運行週期結束,系統回收內存,先調用ob1的析構函數,將ob1.p指向的內存釋放。
  • 調用ob2的析構函數,將ob2.p指向的內存釋放,可是因爲ob2.p的內存已經在上一步被釋放,因此形成了double free。

事實上,這種現象在C++中有兩個專用名詞來描述:"淺拷貝"和"深拷貝"。

因此,在使用編譯器默認的合成構造函數時,咱們要很是當心這一類的陷阱,即便是目前沒有指針成員函數,也要本身寫拷貝賦值構造函數,這樣有利於代碼的擴展和維護。

可是,話說回來,若是我每次實現一個很簡單的需求,都要定義複製拷貝構造函數,一個一個成員去賦值,這樣也是很煩人的,在新標準下,C++提供了一種方法來"解決"這個問題。

阻止拷貝

用戶能夠禁止使用拷貝函數,只要做這樣聲明:

Test(Test &ob) = delete;
Test &operator(Test &ob) = delete;
事實上,部分編譯器默認禁止合成的拷貝賦值構造函數。

這樣,在使用者想使用默認的拷貝賦值構造函數時,編譯器將無情地報錯。
***

移動構造函數

在說到移動構造函數以前,咱們得先介紹一下新標準下一種新的引用類型——右值引用。右值引用就是必須綁定到右值的引用,左值的引用用&,而右值的引用則用&&。右值引用有一個重要的性質,即只能綁定到一個將要銷燬的對象。

通俗地說,右值一般爲臨時變量,字面值,未接受的返回值等等,它們沒有固定地址。
而左值一般是變量。總而言之,左值持久,右值短暫。

下面是引用和右值引用的示例:

int x = 30;
int &r = x;  //正確,左值引用
int &&r = x; //錯誤,x爲左值,&&r爲右值引用
int &&r = 3; //正確,右值引用
const int &r = 3;  //正確,const左值能夠對右值引用

因爲右值引用只能綁定到臨時對象,咱們能夠知道它的特色:

  • 所引用的對象將要被銷燬
  • 該對象沒有其餘用戶
    這兩個特性則意味着:使用右值引用的代碼能夠自由地接管所引用的對象的資源。可想而知,右值引用的特色是"竊取"而不是"生成",在效率上天然就有所提升。

若是如今有一個左值,咱們想將它做爲右值來處理,應該怎麼辦呢?答案是std::move()函數,語法是這樣的:

int x = 30;
int &&r = std::move(x);

可是正如右值的特性而言,將左值轉換成右值的時候,你得確保這個左值將再也不使用,建議使用std::move(),由於這樣的函數名老是容易出現命名衝突。

讓咱們再回到移動構造函數,各位朋友們應該從前面的鋪墊已經猜到了這是個什麼樣的實現,是的,它的特色就是接受一個右值做爲參數來進行構造。實現是這樣的:

class Test
{
    public:
        Test(){p = new int(10);}
        ~Test(){delete p;}
        Test(Test &&ob) noexcept{
            p = ob.p;
            ob.p = nullptr;
        }
        int *p;
};

可能朋友們看了上面的實現會有兩個疑問:

  • 爲何函數要加上noexcept聲明?
  • 爲何要加上 ob.p=nullptr 這個操做?
    剛剛咱們提到了拷貝賦值構造函數的淺拷貝問題(即指針部分僅僅是複製),很顯然,那樣是不行的。可是在移動構造函數中,咱們依然是淺拷貝,爲何這樣又能夠?

從上面的示例能夠看出移動構造函數的參數是一個右值引用,咱們上面有提到,移動構造函數的特色是"竊取"而不是生成。就至關於將目標對象的內容"偷過來",既然目標對象的內存原本就是存在的,因此不會由於失敗問題而拋出異常。當咱們編寫一個不拋出異常的移動操做時,有必要通知標準庫,這樣它就不會爲了可能的異常處理而作一些額外工做,這樣能夠提高效率。

再者,咱們將右值對象的內容偷過來,可是右值對象依然是存在的,它依舊會調用析構函數,若是咱們不將右值的動態內存指針賦值爲null,右值對象調用析構函數時將釋放掉這部分咱們好不容易偷過來的內存。就像上面的例子所示,咱們不得不將ob.p指針置爲空。
口說無憑,咱們來看下面的示例:

class Test
{
    public:
        Test(void){p=new int(50);
        }
        Test(Test &&ob) noexcept{
            p = ob.p;
            //ob.p = nullptr;     
        }
        ~Test(){delete p;}
        int *p;
};
Test ob1;
int main()
{
    Test ob2 (std::move(ob1));
}

在示例中,咱們將ob.p = nullptr;這條語句註釋,而後使用無參構造函數構造ob1,而後將ob1轉爲右值來構造ob2.咱們來看運行結果:

*** Error in `./a.out': double free or corruption (fasttop): 0x09f12a10 ***
Aborted (core dumped)

果真如我所料,出現了double free的錯誤,這是由於在移動構造函數中傳入的右值對象ob在使用完後調用了析構函數釋放了p,而對象ob2偷到的僅僅是一個指針的值,指針指向的內容已經被釋放了,因此在程序執行完成以後再調用析構函數時就會出現double free的錯誤。
爲了再驗證一個問題,咱們將上面的例子中加上ob.p = nullptr;,並將main()函數改爲這樣:

class Test
{
    public:
        Test(void){p=new int(50);
        }
        Test(Test &&ob) noexcept{
            p = ob.p;
            ob.p = nullptr;
        }
        ~Test(){delete p;}
        int *p;
};
Test ob1;
int main()
{
    Test ob2 (std::move(ob1));
    cout<<*ob1.p<<endl;
}

咱們來看看已經被轉換成右值的ob1個什麼狀況,運行結果是這樣的:

Segmentation fault (core dumped)

好吧,其實這是顯而易見的,ob1.p已經在移動構造函數中被置爲nullptr了。

爲何C++11要添加這個新的特性呢?從效率上出發,在程序運行的時候,因爲中間過程會出現各類各樣的臨時變量,每建立一個臨時變量,就會多一次對資源的構造和析構的消耗,若是咱們能將臨時變量的資源接管過來,就能夠省下相應的構造和析構所帶來的消耗。

隱式轉換構造函數

C++中,當類有一個構造函數接收一個實參,它實際上定義了轉換爲此類類型的隱式轉換機制,又是咱們把這種構造函數稱爲轉換構造函數。

官方解釋老是像數學公式同樣難以理解,通俗地說,當一個類A有其中一個構造函數接受一個實參(類型B)時,在使用時咱們能夠直接使用那個構造函數參數類型B來臨時構造一個類A的對象,好像我也沒解釋清楚?好吧,直接上代碼看:

class Test{
public:
    Test(string s,int para = 1){
        str = s;
    }
    void add(Test ob){
        str += ob.str;
    }
    string str;
};
Test ob1("downey");
int main()
{
    ob1.add(string("downey!"));
    cout<<ob1.str<<endl;
}

運行結果:

downeydowney!

如碼所示,Test類有一個構造函數,能夠接收一個string類的實參(能夠由一個實參構造並不表明只能有一個形參),而add()方法接受一個Test類類型參數,在調用add()方法時,咱們直接傳入一個string類型,觸發隱式轉換功能,編譯器將自動以string做爲實參構造一個Test的臨時類對象來傳入add()方法,程序結束以後將釋放臨時變量。
須要注意的是,隱式轉換隻支持一次轉換,若是咱們將main()函數改爲這樣:

int main()
{
    ob1.add("downey!");   
    cout<<ob1.str<<endl;
}

編譯器須要將"downey"轉換成string類型,而後再進行一次轉換,這樣是不支持的。在編譯階段就會報錯:

error: no matching function for call to XXX

同時,若是咱們在聲明add()函數時習慣性地使用了左值引用:

void add(Test &ob){      //使用引用,&
        str += ob.str;
    }

這樣又是什麼結果呢?

答案是,編譯出錯。這又是爲何?若是你有仔細看上面的隱式轉換過程就能夠知道,在使用隱式轉換時生成了一個臨時變量(類型同函數形參),而臨時變量是右值,是不能使用左值引用的。報錯信息以下:

error: no matching function for call to XXX  //左值引用不匹配,因此這裏找不到匹配的方法。

阻止隱式轉換

使用explicit關鍵字修飾函數能夠阻止構造函數的隱式轉換,並且explicit只支持直接初始化時使用,也就是在類內使用,同時,只對一個實參的構造函數有效。在STL中咱們隨時能夠看到explicit的影子。
下面是示例:

class Test{
public:
    explicit Test(string s,int para = 1){
        str = s;
    }
    void add(Test ob){
        str += ob.str;
    }
    string str;
};
Test ob1("downey");
int main()
{
    ob1.add(string("downey!"));    //報錯,no matching function for call to XXX,由於這裏不支持隱式轉換
    cout<<ob1.str<<endl;
}

同時,若是用戶試圖在類外聲明時使用explicit關鍵字,將會報錯:

error: only declarations of constructors can be ‘explicit’

結語

C++真是魔鬼!!!

好了,關於C++構造函數的討論就到此爲止啦,若是朋友們對於這個有什麼疑問或者發現有文章中有什麼錯誤,歡迎留言

原創博客,轉載請註明出處!

祝各位早日實現項目叢中過,bug不沾身.

相關文章
相關標籤/搜索