1. 程式人生 > >c++合成預設建構函式與new關鍵字帶不帶括號的分析

c++合成預設建構函式與new關鍵字帶不帶括號的分析

宣告或定義一個類/物件的時候,會因為類本身的成員結構而會引起不同的建構函式的呼叫,之前的學習中或多或少有些總結。《c++primer(第五版)》《深度探索c++物件模型》《More Effective C++》三本書中都有總結,自己也簡單的理解了下

全篇總結:

一,宣告一個類物件時,不一定是呼叫了預設的建構函式;只有在沒有任何建構函式且AA xx{}宣告的時候,編譯器才會對內建型別進行“零值化”;其他情況按照四種情況進行分析

二,編譯器有四種情況會合成預設的建構函式
1. 包含了一個類的物件,這個物件有一個建構函式(包括編譯器合成的預設建構函式)
2. 繼承自一些基類,其中某些基類有一個建構函式(包括編譯器合成的預設建構函式)
3. 有一個虛擬函式,或者繼承到了虛擬函式
4. 有虛基類
兩種錯誤的觀點:
a) 任何類如果沒有定義建構函式,則編譯器會幫我們合成一個預設建構函式。
b) 合成預設建構函式會對類中的每一個數據成員進行初始化。

三,new物件的時候帶不帶括號
對於自定義類型別:
如果該類沒有定義建構函式(由編譯器合成預設建構函式)也沒有虛擬函式,那麼class c = new class;將不呼叫合成的預設建構函式,而class c = new class();則會呼叫預設建構函式。
如果該類沒有定義建構函式(由編譯器合成預設建構函式)但有虛擬函式,那麼class c = new class;將不呼叫合成的預設建構函式;class c = new class()會呼叫預設建構函式。
如果該類定義了預設建構函式,那麼class c = new class;和class c = new class();一樣,都會呼叫預設建構函式。。

對於內建型別:
int *a = new int;不會將申請到的int空間初始化,而int *a = new int();則會將申請到的int空間初始化為0。

1,宣告一個類的時候,不一定是呼叫了預設建構函式

如下:

class HasExplicitCon{
public:
    HasExplicitCon(){
cout<<"顯式的定義一個預設建構函式"<<endl;
    }
    HasExplicitCon(int a){
cout<<"顯式的定義一個有引數預設建構函式"<<endl;
    }

    int
a; }; int main(int argc, char **argv) { HasExplicitCon hasObject1; HasExplicitCon hasObject2(2); HasExplicitCon hasObject3(); cout<<hasObject1.a<<endl; cout<<hasObject2.a<<endl; //cout<<hasObject3.a<<endl; 編譯不通過 return 0; }

結果為:

顯式的定義一個預設建構函式
顯式的定義一個有引數預設建構函式
4213675
7602016

是的,沒看錯,結果只輸出了兩行,因為hasObject3後面跟的是小括號,會引起c++歧義,誤以為是定義的一個函式,這點在之前的文章中說過了http://blog.csdn.net/hll174/article/details/78309212。用中括號的話則不會,中括號實際是initializer_list,http://zh.cppreference.com/w/cpp/language/list_initialization
std::initializer_list 物件在這些時候自動構造:
- 花括號初始化器列表用於列表初始化,包括函式呼叫列表初始化和賦值表示式
- 花括號初始化器列表被繫結到 auto ,包括在帶範圍 for 迴圈中
這裡預設建構函式沒有對變數a進行初始化,且其在類中沒有初始值,new出的物件在堆中,編譯器進行初始化的時候是對其進行隨機初始化的。
所以明顯看到定義(也就是我們所說的宣告時),會呼叫無參的建構函式(預設建構函式),如果自己定義了則使用自己的,自己沒有定義,則因為不滿足四種情況,編譯器也不會生成合成的預設建構函式,因此值是隨機的。
這裡HasExplicitCon自定義了預設建構函式,但是沒有對a進行初始化,因此,a的值還是編譯器隨機化的,這種隨機化的與建構函式無關。a的值是程式的責任,而非編譯器的責任,預設建構函式只會執行編譯器的責任,這點下面會將具體介紹。
而如果隱去所有的預設建構函式,則a的值仍然是隨機值,因為這時候編譯器也不會生成合成的預設建構函式,其不滿足下面要介紹的編譯器合成預設建構函式的四種情況。
這裡又發現一種情況,
情況1:
A a;宣告,且有顯示的預設建構函式和帶引數建構函式

class HasExplicitCon{
public:
    HasExplicitCon(){
cout<<"顯式的定義一個預設建構函式"<<endl;
    }
    HasExplicitCon(int a){
cout<<"顯式的定義一個有引數預設建構函式"<<endl;
    }
    char *str;
    int a;
};
int main(int argc, char **argv) {
    HasExplicitCon hasObject1;

    if(hasObject1.str){
   cout<<"str是非空值"<<endl;
   cout<<*hasObject1.str<<endl;
   cout<<hasObject1.a<<endl;
    }else{
   cout<<"str是空值"<<endl;
   cout<<*hasObject1.str<<endl;
   cout<<hasObject1.a<<endl;
    }
    return 0;
}

結果為:

顯式的定義一個預設建構函式
str是非空值
?
4213819

呼叫了顯示的預設建構函式,但是沒有例項化a和str的值,因為這是程式的責任

情況2:
A a;宣告,無顯示的預設建構函式和帶引數建構函式
也就是把上面情況的建構函式全部去掉,這裡只貼結果

str是非空值
?
4213803

結果相同,但原因不同。不滿足四種情況,因此實質是編譯器沒有為其合成預設的建構函式,因為也就是沒有建構函式呼叫,編譯器處理的隨機值。

情況3:
A a{};宣告,有顯示的預設建構函式和帶引數建構函式
在情況1的基礎上,只將宣告改為 HasExplicitCon hasObject1{};,結果為:

顯式的定義一個預設建構函式
str是非空值
?
4213819

呼叫預設建構函式,但是認為str和a是程式的責任,情況與1相同。

情況4:
A a{};宣告,無顯示的預設建構函式和帶引數建構函式
在情況3的基礎上,去掉顯示的建構函式,這個時候只輸出了

str是空值

然後程式死亡,這裡斷點看到hasObject1.a的值為0。
情況2的時候是沒有合成建構函式的,也沒有對內建型別進行初始值。而這個時候內建型別的值都被“零化”,因為中括號{}的存在,而中括號本質是initializer_list,顯示的建構函式中,編譯器對於{}還是和普通的()一樣,認為不是編譯器的責任,例如情況3;而當沒有建構函式的時候,編譯器會合成了預設的建構函式且對內建型別的物件進行了初始化,這與initializer_list有很大關係,因此對於初始化的時候initializer_list還需要進一步研究。

2,《深度探索c++物件模型》關於預設建構函式的解釋

首先,《深度探索c++物件模型》這本書告訴我們有兩個誤解:
a) 任何類如果沒有定義建構函式,則編譯器會幫我們合成一個預設建構函式。
b) 合成預設建構函式會對類中的每一個數據成員進行初始化。
以及編譯器為我們生成預設建構函式的四種情況:
1. 包含了一個類的物件,這個物件有一個建構函式(包括編譯器合成的預設建構函式)
2. 繼承自一些基類,其中某些基類有一個建構函式(包括編譯器合成的預設建構函式)
3. 有一個虛擬函式,或者繼承到了虛擬函式
4. 有虛基類

2.1 包含了一個類的物件,這個物件有一個建構函式(包括編譯器合成的預設建構函式)

看下面這段程式碼

class Foo{
public:
    Foo(){cout<<"Foo顯示定義的預設建構函式"<<endl;};
    Foo(int){};
};

class Bar{
public:
    Foo foo;
    char *str;
};

void f(){
    Bar bar;
    if(bar.str){
        cout<<"編譯器對str進行了預設優化"<<endl;
        cout<<*bar.str<<endl;
    }
}
int main(int argc, char **argv) {
 f();
    return 0;
}

我在gcc6.2的平臺下結果為;

Foo顯示定義的預設建構函式
編譯器對str進行了預設優化
?

Bar類沒有任何建構函式,含有類成員物件foo,且foo類有預設建構函式(顯示自定義的),因此編譯器會為Bar類生成合成的預設建構函式,對變數進行初始化,注意這裡編譯器生成的預設建構函式首先是擴充套件建構函式,先呼叫了基類的預設建構函式對foo進行了初始化,這就是其建構函式擴充套件規則
同時,編譯器生成的預設建構函式初始化只會完成編譯器責任的初始化,而不會完成程式責任的初始化,這裡的foo物件就是編譯器的責任,而str指標則是程式的責任,因此str在這裡是一個棧區物件,編譯器對其是進行隨機值初始化(不同編譯平臺的處理可能不同),因此我們需要構造Bar自己的建構函式完成程式的初始化,即對str進行初始化,

Bar(){str=0;}

因此,上面文章作者幫我們總結了合成預設建構函式總是不會初始化類的內建型別及複合型別的資料成員,因為這是程式的責任,我們得分清楚編譯器的責任與程式的責任。

另外,這篇文章https://www.cnblogs.com/QG-whz/p/4676481.html“這是不可能的,建構函式是用來負責類物件的初始化的,一個類物件無論如何一定會被初始化,不論是預設初始化,還是值初始化。也就是說,當例項化類物件時,一定會呼叫建構函式。那也就不存在“需要/不需要”這回事,必然會合成。”
這是我們之前普遍的想法,我們看到認為的“類例項化”,其實並沒有真正的例項化出來,可能類沒有建構函式,編譯器也無法合成預設的建構函式,“類例項化”僅僅只是編譯器隨機化的一個垃圾值,我們的例項化是我們知道或者變數物件按照我們需要的去進行初始值,而不是隨機值。沒有一定會調建構函式,值被初始化有可能只是編譯器的處理而非各種建構函式的處理。

2.2 繼承自一些基類,其中某些基類有一個建構函式(包括編譯器合成的預設建構函式)

這與上面類似,只是這裡有建構函式的擴充套件規則
基類預設建構函式(按照基類被宣告的順序先宣告優先,多層基類按照上層優先)—>類物件的預設建構函式(按照類物件在類中宣告的先後順序)—>類自身的預設建構函式

2.3 有一個虛擬函式,或者繼承到了虛擬函式

同時還有可能:
- 類宣告或繼承一個虛擬函式
- 類派生自一個繼承鏈,其中有一個或更多的虛基類
總之這種情況的就是類有虛函數了,因為在編譯期會有虛擬函式表被生成出來,同時還有指向虛擬函式表的指標vptr被生成出來,vptr需要被初始化才能完成虛機制。這些都是在建構函式中完成的,因此沒有建構函式的類會由編譯器合成預設的建構函式。因此擴充套件建構函式的規則就是設定正確的虛擬函式表地址。

2.4 帶有虛基類的類

這種典型的虛基類的繼承有“菱形繼承”,為了讓派生類中只有一個虛基類的物件,以前編譯器的做法是在派生類的每個虛基類中安插一個指標,所有由引用或指標來存取一個虛基類的操作都可以通過相關指標完成。這個指標也是在類構造期間完成的,因此若類沒有任何建構函式,編譯器會合成預設的建構函式。同樣,擴充套件建構函式的規則是設定正確的虛基類指標的值。
因此需要理解編譯器合成預設建構函式的四種情況,同時還得區分編譯器初始化的責任與程式初始化的責任。

3. new建立一個物件時候帶不帶小括號的區別

如果不用new建立物件,最好是用{},而不是(),如上所述。當用()的時候,其實有一些誤解。同時上面也介紹了編譯器生成預設建構函式的四種情況,這裡繼續看看new一個物件的區別。
我們知道如果直接XX xx;則物件的記憶體是在棧區;
而XX xx =new XX;則是在堆區(這裡先忽略帶不帶括號的事)
如果沒有預設例項化,那麼棧區和堆區的值都是隨機初始化的,這種隨機性很多時候是對程式有害的。

3.1內建型別的new

關於內建型別的new情況,這點沒有爭議:
帶了()的時候,在分配記憶體的時候初始化零值(int的0或者string的0值);沒有帶()的時候只分配了記憶體,其值是隨機性的。看下面程式碼:

int main(int argc, char **argv) {
int *b=new int[100];
for(int i=0;i<100;i++){
cout<<b[i]<<endl;
}
return 0;
}

結果為:

18353232
18382632
0
0
...
-2147450880
-2147450880

而加上()後的結果(建議還是用中括號好,前面小括號很多時候會有意想不到的Bug)

int *b=new int[100]{};

這樣的結果為:

0
...
0

全都是0,符合預設初始化的預期。

3.2 自定義型別的new

對於自定義型別的new,這裡貌似不同的部落格還是有點分歧,然後自己嘗試了下。

class A{
public:
    int a;
};

int main(int argc, char **argv) {
   A *a1=new A;
   A *a2=new A();

  cout<<a1->a<<endl;
  cout<<a2->a<<endl;

  A a3;
  cout<<a3.a<<endl;

  return 0;
}

結果為:

17465128
0
4213632

只有a2的a值為0,a1和a3的值是隨機的。這證實了一點:

如果該類沒有定義建構函式(由編譯器合成預設建構函式)也沒有虛擬函式,那麼class c = new
class;將不呼叫合成的預設建構函式,而class c = new class();則會呼叫預設建構函式。
a1不呼叫合成的預設建構函式,因此值是隨機的;a3也是是因為無法生成合成的預設建構函式,而是隨機值。

然後文章說

如果該類沒有定義建構函式(由編譯器合成預設建構函式)但有虛擬函式,那麼class c = new class;和class c = new class();一樣,都會呼叫預設建構函式。
嘗試了下只加虛擬函式,結果為

18481344
0
4213600

仍然是隻有()的例項為0,而如果該類定義了預設建構函式,那麼class c = new class;和class c = new class();一樣,都會呼叫預設建構函式,這點毫無爭議。

因此總結下自定義型別的new時候:
如果該類沒有定義建構函式(由編譯器合成預設建構函式)也沒有虛擬函式,那麼class c = new class;將不呼叫合成的預設建構函式,而class c = new class();則會呼叫預設建構函式。
如果該類沒有定義建構函式(由編譯器合成預設建構函式)但有虛擬函式,那麼class c = new class;將不呼叫合成的預設建構函式;class c = new class()會呼叫預設建構函式。
如果該類定義了預設建構函式,那麼class c = new class;和class c = new class();一樣,都會呼叫預設建構函式。
另外,這裡的new呼叫預設建構函式的時候,是對內建型別物件進行“零值化”的,這點與第一章和第二章說的非編譯器的責任則不初始化,因此值是隨機的是存在區別的,注意區分。

後續需要對initializer_list在建構函式初始化的情況進行分析,還有沒有呼叫建構函式且自身沒有定義建構函式記憶體的分配問題還需要學習。