標籤:

C++構造函數參數列表初始化與直接在函數內部初始化有何區別?

struct Base_s2{
Base_s2() = default;
Base_s2(string s):name(s){}
virtual void p(ostream os){
os &<&< name &<&< "base" &<&< endl; } private: std::string name; }; struct Derived_s2 : public Base_s2{ Derived_s2() = default; // Derived_s2(string s): Base_s2(s){} Derived_s2(string s){ Base_s2(s); } void p(ostream os) override { Base_s2::p(os); os &<&< "derived " &<&< endl; } }; 使用上面的代碼Derived類的構造函數會報錯,但是使用注釋掉的那句構造函數就能初始化。請問這兩種初始化方式有什麼區別嗎?以及為什麼上面的寫法會報錯?


進入函數體之後,類的所有成員和基類,就已經初始化完畢了,你這個時候再進行所謂的初始化,只不過是賦值而已,就是用新的值覆蓋掉已經初始化的值

並且既然進入函數體之後基類已經初始化完畢,這個時候你想通過調用基類構造函數來構造基類,也是沒影的事


看你到處都是string s,時間都浪費在pass-by-value了。

運行到函數內部的時候,初始化列表已經執行完了。所以,你這裡寫在函數內部的話,是先用默認構造初始化了一遍,再到函數內部賦值一遍。所以應該優先用初始化列表。更何況如果是沒有默認構造的類型,只能通過初始化列表搞。


有區別的,你注釋的那一行其實是在調用父類的構造函數,這一個只能在initializer-list中做。


因為進入派生類構造函數體的時候其基類和其數據成員已經構造完畢了,所以在派生類構造函數體內只能對數據成員進行賦值而不是構造。

關於類的構造和析構順序我之前寫過一篇文章(從標準中摘錄出來的構造和析構的相關內容),可以參考一下: http://imzlp.me/posts/16550


感覺標題和問題詳細描述在說的是兩個問題。那分開說。

1. C++構造函數參數列表初始化與直接在函數內部初始化有何區別?

就像其他答主講的,先進行列表初始化然後才會進入構造函數體內。函數體內再賦值是重複的,低效的,一般建議成員列表初始化。

當實例化一個類(如Base base)的時候實質上會發生5步:

1.分配memory給對象base

2.調用類Base相應的構造函數

3.先進行初始化列表的初始化

4.再進入構造函數體內,進行一些賦值什麼的

5.跳出函數體,控制權還給調用者

這是針對一個非派生類或者一個類自己的成員初始化而說的。

2. Code里暴露的是子類中如何初始化父類成員的問題。

C++中,子類實例化時若沒有特殊聲明,會先自動調用父類的默認構造函數,再點用自己的。如果子類對象初始化時顯式調用其它父類的構造函數對父類成員進行初始化,C++規定,可以在子類的初始化列表中,像列表初始化其他成員函數一樣在冒號後面「call」所需要的父類的構造函數。

如題中被注釋掉的 Derived_s2(string s): Base_s2(s){}。

這是規定。其他時候你是不能調用一個類的構造函數的。

C++也不允許在列表中直接對父類變數name進行初始化。

但允許在函數體中進行對name的賦值,不過這樣比較危險。

所以在子類列表初始化時「調用」父類的某個構造函數,是最符合規範的。

詳細解釋見

11.4 — Constructors and initialization of derived classes


你這個代碼相當於把Dervied_S2的Base_S2部分給構造了兩次,當然會報錯。

注意構造器不是函數,至少不是正常的函數。


區別挺大的

以下四種情況,你必須使用member initialization list:

1. 當初始化一個reference member時;

2. 當初始化一個const member時;

3.當調用一個base class 的constructor,而它擁有一組參數時;

4.當調用一個member class的constructor,而它擁有一組參數時;

另外值得一提的是,使用member initialization list 和 在constructor里賦值,是完全不一樣的事情。

class Base {
public:
Base(const string s) : s_(s) {}
/*
Base(const string s) {
s_ = s;
}
*/
private:
string s_;
};

編譯器大概會把以上代碼擴張成

Base(const string s) {
s_.string::string(s);
}
/*
Base(const string s) {
s_.string::string();
string tmp = string(s); // create temporary object
s_.string::operator=(tmp);
tmp.string::~string();
}
*/

所以 被注視掉的寫法,多了一次構造函數和析構函數的開銷

再者,再member initialization list的初始化順序,與你的寫法無關,只與成員對象在類中聲明的先後有關。

class Base {
public:
// 實際上,i_先被初始化,j_在後,結果是 i_的值未定義,j_ = val
Base(int val) : j_(val), i_(j_) {}
private:
int i_;
int j_;
};


首先題主應該理解這樣幾點:

1. 初始化和賦值的區別。初始化指的是定義一個對象並給其賦初值。賦值指的是將當前對象的值擦除並用新值代替。

2. 程序先執行構造函數的初始值列表,再執行構造函數體。

3. 如果沒有初始值列表,則類成員變數會執行默認初始化。

所以,一般情況下,通常可以忽略兩者的差異,都能給成員賦值。但是上面第三條也說了,如果沒有初始值列表,則類成員變數會執行默認初始化,所以對於引用類型的變數或const變數必須要用初始值列表初始化,因為引用類型或const必須初始化。


調用基類構造函數只能在初始化列表裡完成,這個和Java不同,順便說一句,你這個寫法也並沒有對this調用基類構造函數,而是創建了一個基類的匿名臨時對象而已


看了大部分人的回答,都是在強調構造函數初始值列表(樓主可以看看c++ primer 5th中文版第258頁,對這個問題有較深說明),一個是直接初始化數據成員,後者則先初始化再賦值。

還有

Derived_s2(string s): Base_s2(s){} //委託Base_s2()這個函數完成Derived_s2()這個函數的功能

Derived_s2(string s){
Base_s2(s); //如果把這句去掉,編譯也是沒問題的。因為父類有默認構造函數。
}

測試代碼,編譯運行都沒問題呀:

#include &

#include &

using std::string;

struct Base_s2

{

public:

Base_s2()=defau<

Base_s2(string s):name(s){}

private:

string name;

};

struct Derived_s2:public Base_s2

{

Derived_s2()=defau<

Derived_s2(string s)

{

;

}

};

int main()

{

Derived_s2 der1;

Derived_s2 der2("hello");

return 0;

}


一句話,在進入派生類的構造函數內的時候,基類的部分已經構造完成了,你沒有使用成員初始化列表初始化基類,因此基類採用的是基類自己默認的構造函數初始化的,然後進入構造函數後,你又使用基類的構造函數構造一次,說明基類部分構造了兩次,肯定報錯。


編譯器不想讓你多此一舉……

進入派生類構造函數基類的構造器已經調用過,內存裡面都已經初始化了,這時候,你卻要再構造一次,這就gg了唄,你不寫初始化列表基類就給你調用默認的構造函數


初始化比賦值效率高,初始化順按聲明順序來,列表與順序無關。出自cpp primer5


去看深入理解c++對象模型。


c++到處都是workround。

從語義上講,構造順序是先base後derived ,可是偏偏很多參數必須通過子類的參數表傳遞,好了這下麻煩了,只能在子類構造函數調用之前搞一個機制把參數傳給base,這個機制就是初始化列表。

所以語義上看,構造函數應該調用順序是

先base後derived

實際上是

先derived 後base

我問問你們,感覺怎麼樣?哈哈

----------------------------------------------------

為了不引起誤解,這裡補充說明一下:

class A
{
public:
A()
{
std::cout&<&<"base"&<&

毫無疑問,輸出應該是

base
derived

表面上看調用順序是先A後B的。

而反彙編結果:

00000000004008b6 &:
...
4008c7: 48 89 45 f8 mov %rax,-0x8(%rbp)
4008cb: 31 c0 xor %eax,%eax
4008cd: 48 8d 45 f7 lea -0x9(%rbp),%rax
4008d1: 48 89 c7 mov %rax,%rdi
4008d4: e8 9b 00 00 00 callq 400974 &<_ZN1BC1Ev&>/*調用了B的構造函數*/
...
0000000000400974 &<_ZN1BC1Ev&>:/*這是B的構造函數
...
400987: e8 bc ff ff ff callq 400948 &<_ZN1AC1Ev&>/*這裡調用了A的構造函數*/
40098c: be 39 0a 40 00 mov $0x400a39,%esi
400991: bf 80 10 60 00 mov $0x601080,%edi
400996: e8 d5 fd ff ff callq 400770 &<_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@plt&>
...

實際調用順序是先B後A。


推薦閱讀:

C++ 的 string 為什麼不提供 split 函數?
C++函數返回值拷貝問題?
operator=重載時,是否可以用一個按值傳遞版本取代按const引用傳遞和右值引用傳遞?
關於C++右值及std::move()的疑問?
C++11 中 typedef 和 using 有什麼區別?

TAG:C |