在C++ Primer第五版39頁提到:「在C++語言中,初始化時一個異常複雜的問題」。html
而後在第235頁中又提到:「構造函數是一個很是複雜的問題」。程序員
剛好這兩個問題連在一塊兒,就成了一個異常很是複雜的問題,把我折磨的夠嗆。編程
1.初始化數組
不少程序員對於用等號 = 來初始化變量的方式倍感困惑,這種方式容易讓人認爲初始化是賦值的一種。事實上,在C++語言中,初始化與賦值是兩個徹底不一樣的編程語言
操做。然而在不少編程語言中兩者的區別幾乎能夠忽略不計,即便在C++語言有時這種區別也可有可無,因此人們特別容易把兩者混爲一談。須要強調的是,這個ide
概念相當重要。函數
初始化不是賦值,初始化的含義是建立一個變量時賦予其一個初始值,而賦值的含義是把對象當前的值擦除,而以一個新值來替代。學習
列表初始化優化
C++語言定義了初始化的好幾種不一樣形式,這也是初始化問題複雜性的一個體現。例如,要想定義一個名爲 units_sold 的 int 變量並初始化爲 0 ,如下的 4 條語句ui
均可以作到這一點:
1 int units_sold = 0; 2 int units_sold = {0}; 3 int units_sold{0}; 4 int units_sold(0);
做爲C++11新標準的一部分,用花括號來初始化變量獲得了全面應用,而在此以前,這種初始化的方式僅在某些受限的場合才能使用。
默認初始化
若是定義變量時沒有指定初值,則變量被默認初始化,此時變量被賦予了默認值,默認值究竟是什麼由變量類型決定,同時定義變量的位置也會對此有影響。
若是是內置類型的變量未被顯式初始化,它的值由定義的位置決定。定義於任何函數體(塊)以外的變量被初始化爲 0 ,而定義於函數體(塊)內部的內置類型變量將不被
初始化。一個未被初始化的內置類型變量的值是未定義的,若是試圖拷貝或以其餘形式訪問此類值將引起錯誤。
定義在塊內部的局部變量有一個例外,即若是是static型變量,即便不顯式地給予初始值,也會被默認初始化爲零。
未初始化的變量含有一個不肯定的值,使用未初始化變量的值是一種錯誤的編程行爲而且很難調試。儘管大多數編譯器都能對一部分使用未初始化變量的行爲提出警告,
但嚴格來講,編譯器並未被要求檢查此類錯誤。
每一個類各自決定其初始化對象的方式。並且,是否容許不經初始化就定義對象也由類本身決定。若是類容許這種行爲,它將決定對象的初始值究竟是什麼。
絕大多數類都支持無須顯式初始化而定義對象,這樣的類提供一個合適的默認值。
例如,string 類規定若是沒有指定初值則生成一個空串。
一些類要求每一個對象都顯式初始化,此時若是建立了一個類的對象而並未對其做出明確的初始化操做,將引起錯誤。
2.構造函數的初始化做用
構造函數特色
和類同名。沒有返回值。形參列表可能爲空。函數體可能爲空。能夠重載。不能聲明爲 const。
合成默認構造函數
若是咱們的類沒有顯式地定義構造函數,那麼編譯器就會爲咱們隱式地定義一個默認構造函數,即合成默認構造函數。
合成默認構造函數將按照以下規則初始化類的數據成員:
· 若是存在類內的初始值,用它來初始化成員,即C++11新標準中新增的類內初始化。
· 不然,執行默認初始化該成員。
某些類不能依賴於合成的默認構造函數
合成默認構造函數只適合很是簡單的類,對於大多數普通的類來講,必須定義它本身的默認構造函數,緣由有三:第一個緣由也是最容易理解的一個緣由就是編譯器只有
在發現類不包含任何構造函數的狀況下才會替咱們生成一個默認的構造函數。一旦咱們定義了一些其餘的構造函數,那麼除非咱們再定義一個默認的構造函數,不然類將
沒有默認構造函數。
只有當類沒有聲明任何構造函數時,編譯器纔會自動的合成默認構造函數。
第二個緣由是對於某些類來講,合成的默認構造函數可能執行錯誤的操做。含有內置類型或複合類型成員的類應該在類的內部初始化這些成員,或者定義一個本身的默認構造函數。不然,用戶在建立類的對象時就可能獲得未定義的值。
第三個緣由是有的編譯器不能爲某些類合成默認的構造函數。例如,若是類中包含一個其餘類類型的成員且這個成員的類型沒有默認構造函數,那麼編譯器將沒法初始化該成員。對於這樣的類來講,咱們必須自定義默認構造函數,不然該類將沒有默認的構造函數可用。
構造函數初始值列表
當咱們定義變量時習慣當即對其進行初始化,而非先定義、再賦值。
1 string foo = "Hello World"; //定義並初始化 2 string bar; //默認初始化成空string對象 3 bar = "Hello World"; //爲bar賦一個新值
就對象的數據成員而言,初始化和賦值也有相似的區別。若是沒有在構造函數的初始值列表中顯式地初始化成員,則該成員將在構造函數體以前執行默認初始化。
例如:
1 Sales_data::Sales_data(const string &s, unsigned cnt, double price) 2 { 3 bookNo = s; 4 units_sold = cnt; 5 revenue = cnt * price; 6 }
這段代碼與下面這段代碼的原始定義效果是相同的:
1 Sales_data(const std::string &s, unsigned n, double p):bookNo(s), units_sold(n),revenue(p*n) 2 {}
當構造函數完成後,數據成員的值相同。區別是下面的代碼初始化了它的數據成員,而這個上面的版本是對數據成員執行了賦值操做。這一區別到底會有什麼深層次的
影響徹底依賴於數據成員的類型。
有時咱們能夠忽略數據成員初始化和賦值之間的差別,但並不是老是這樣。若是成員是const或者引用的話,必須將其初始化。相似的,當成員屬於某種類類型且該類沒有
默認的構造函數時,也必須將這個成員初始化。
隨着構造函數體一開始執行,初始化就完成了。
咱們初始化const或者引用類型的數據成員的惟一機會就是經過構造函數初始值。
若是成員是const、引用,或者屬於某種未提供默認構造函數的類類型,咱們必須經過構造函數初始值列表爲這些成員提供初值。
在不少類中,初始化和賦值的區別事關底層效率問題:前者直接初始化數據成員,後者則先初始化再賦值。
成員初始化的順序
成員初始化的順序與他們在類定義中出現的順序一致。
3.C++ 11中關於構造函數的新語法
=default的含義
如:
1 Sales_data() = default;
咱們但願這個函數的做用徹底等同於以前使用的合成默認構造函數。
在C++11新標準中,若是咱們須要默認的行爲,那麼能夠經過在參數列表後面寫上 = default 來要求編譯器生成默認構造函數。
其中, = default 既能夠和聲明一塊兒出如今類的內部,也能夠做爲定義出如今類的外部。和其餘函數同樣,若是 = default 在類的內部,則默認構造函數是內聯的;
若是它在類的外部,則該默認構造函數默認不是內聯的。
委託構造函數
C++ 11新標準擴展了構造函數初始值的功能,使得咱們能夠定義所謂的委託構造函數(delegating constructor)。
一個委託構造函數使用它所屬類的其餘構造函數執行它本身的初始化過程,或者說它把它本身的一些(或者所有)職責委託給了其餘構造函數。
1 class Sales_data 2 { 3 public: 4 //非委託構造函數使用對應的實參初始化成員 5 Sales_data(std::string s, unsigned cnt, double price):bookNo(s), units_sold(cnt), revenue(cnt * price) {} 6 //其他構造函數全都委託給另外一個構造函數 7 Sales_data(): Sales_data("", 0 , 0) {} 8 Sales_data(std::string s): Sales_data(s, 0, 0) {} 9 Sales_data(std::istream &is): Sales_data() 10 { 11 read(is, *this); 12 } 13 }
4.合成默認構造函數再探
其實我先前大部分的糾結都是在與編譯器合成的默認構造函數上面,正如《Inside the C++ Object Model》中所說,面向對象的編程過程當中,編譯器揹着咱們作了太多的事,以致於不少東西看起來很不天然。
而對於合成默認構造函數,首先,它是在程序員沒有顯式地定義任何一個構造函數時編譯器自動生成的,也就是說,只要咱們自定義了任何一個構造函數,編譯器將再也不自動添加。而關於合成默認構造函數的做用,C++ Primer 第五版中文版中這樣說到:
· 若是存在類內的初始值,用它來初始化成員,即C++11新標準中新增的類內初始化。
· 不然,執行默認初始化該成員。
也就是說,若是存在類內初始值,一切都好辦,編譯器將利用類內初始化賦予數據成員初始值,這是合成默認構造函數的工做。
而若是不考慮C++ 11新標準,即若是沒有類內初始值的狀況下,執行默認初始化,即:
對於基本內置類型和指針,引用等複合類型,其值是未定義的。(注1:static 型變量除外,注2:有時可能也依賴於編譯器的實現,有的編譯器可能就直接賦初值0)。
對於含有默認構造函數的類類型成員變量,合成默認構造函數會調用該類的默認構造函數來初始化類類型的數據成員。
然而,合成默認構造函數可能並無這麼簡單,由於牽扯到編譯器的優化問題,因此在《Inside the C++ Model》中,又詳細闡述了根據不一樣的狀況,編譯器會怎樣來合成
默認構造函數:
例如,一個很典型的例子就是,若是一個類的全部數據成員都是內置類型或複合類型(非static),那麼編譯器將很是聰明地再也不合成默認構造函數,由於即便合成了默認構造函數也會什麼都不作,因此就會優化掉,成員變量的值仍是未定義的。
(注:其實仍是那樣的,這只是C++標準的規定,針對具體的編譯器實現,可能這時會有不一樣,也會合成默認構造函數來初始化成員爲 0)。
而若是一個類中含有其餘類類型的數據成員,這個數據成員的類類型又剛好有默認構造函數,這時,編譯器合成默認構造函數就是責無旁貸的了,由於在這個合成默認構造函數中有很是重要的工做來作:即調用數據成員類類型的默認構造函數來初始化數據成員。
對於其餘的編譯器必須合成默認構造函數的狀況,《Inside the C++ Model》總結了四種狀況:
注:後面兩種狀況其實涉及到了深入的C++中多態機制和虛繼承機制的實現。
其實能夠歸結爲一句話:只有編譯器不得不爲這個類生成函數的時候(nontrival),編譯器纔會真正的生成它。
參考:http://www.cnblogs.com/fanzhidongyzby/archive/2013/01/12/2858040.html
參考:http://www.tuicool.com/m/articles/iYveIjR
5. 前天晚上的疑惑
前天我一直疑惑於下面一段代碼:
1 class Foo 2 { 3 std::string str_; 4 };
我當時的想法是在類內定義str_的時候已經調用了string的默認構造函數來完成了初始化,那麼在定義一個對象 Foo a 時,編譯器默認生成的構造函數爲何還要調用一次
string的默認構造函數呢?這樣來說,《Inside the C++ Model》中第一種狀況就不對了啊。
後來通過在知乎私信兩位陳碩,dong大神,問題才得以解決。
其實個人理解是沒錯的,string類 無論在任何地方定義一個對象(全局或者局部處),都會調用默認的構造函數。
然而正如陳碩大大所說,「定義」 class member 和 「定義」 class object是不同的 「定義」,其實前者更像是聲明一點。
也就是說,在定義一個Foo類的時候,實際上是沒有分配內存的,str_更沒有存儲空間,更不用提初始化的問題了。只有在實例化即定義一個對象的時候,纔會完成內存分配,
此時合成默認構造函數就會調動string的默認構造函數來初始化str_,這和《Inside the C++ Model》中是一致的。
當運用sizeof運算符作以下運算的時候,sizeof(Foo),實際上是調用默認構造函數生成了一個臨時Foo對象,獲得的也是臨時對象的存儲空間大小,類自己並無存儲空間。
關於類對象的存儲機制:
參考:http://blog.csdn.net/zhangliang_218/article/details/5544802
參考:http://blog.sina.com.cn/s/blog_69c189bf0100mkeu.html#cmt_2743824
參考:http://www.cnblogs.com/jerry19880126/p/3616999.html
6. 其餘本身的一些思考
1)其實關於默認構造函數我以爲可能還依賴於不一樣的編譯器的實現,儘管C++標準上說的是未定義、默認初始化什麼的,還有《Inside the C++ Model》說的編譯器的優化問題,可是我以爲可能還有賴於編譯器的具體實現,會不會所有初始化爲 0 什麼的。
2)關於C++ 11標準新增的類內初始化,我以爲可能會影響以往的一些編譯器實現。
首先若是類的全部的數據成員都進行了類內初始化,那麼編譯器可能也會將默認構造函數優化掉。
而後就是若是存在類內初始化的話,就沒有必要非要在構造函數初始值列表中初始化引用和const數據了,能夠直接類內初始化。
注:畢竟《Inside the C++ Model》是根據C++95標準寫的,如今來看有些地方過期也很正常。
我我的以爲,儘管C++的標準在變化,可是有一點確定是不變的,就是在C++語言中,建立一個對象,首先就會調用構造函數。
而針對於合成默認構造函數的編譯器優化,則可能會隨着標準的變化而變化。一個原則:只有在被須要的時候,編譯器纔會作。畢竟在C++中,效率很主要,
並且編譯器是很聰明的。
3)關於對象數組。
對象數組的定義的話,我以爲仍是和每個對象定義時同樣,若是有默認構造函數才能定義對象數組,而後數組的每個元素都會按照默認構造函數來初始化。
7. 感想
坦白講,一直在這上面糾結真的是很浪費時間的。並且我如今都快想的走火入魔了,其實我如今根本就不在學習編程的正確的軌道上,
學習編程實踐很中重要,我卻老是糾結於一些語法的具體實現,和一些看起來無傷大雅的東西。
真的,不要再糾結於這些,一來可能如今以你的知識你還搞不定這些東西,理解不了,想太多也是浪費時間,還有可能會走火入魔,本身跳不出來,
二來編程真的實踐特別中重要,必定要多敲代碼多敲代碼!!!!!
還有就是關於這些問題,其實等你能力上去了,這些問題都是能夠經過本身的實踐來驗證的。
因此如今糾結這些沒有任何意義!!!多敲代碼纔是王道!!!
4月18日晚上更新(如下內容均在VS2013下獲得驗證):
C++ Primer 第236頁是對的,合成默認構造函數將:
· 若是存在類內的初始值,用它來初始化成員,即C++11新標準中新增的類內初始化。
· 不然,執行默認初始化該成員。
也就是說利用類內初始值進行對象的初始化是合成默認構造函數的工做,(唉原本還覺得是primer寫錯了呢,如今發現不是,又更加加深了對C++ Primer的崇敬之感)。
那麼在C++ 11的標準之下,《Inside the C++ Model》在原來的四種狀況的基礎上,應該再加一種狀況了(原來的四種狀況仍然成立):
就是即便類的數據成員都是內置類型或者複合類型,只要存在類內初始化(哪怕只有一個成員),編譯器就會合成默認構造函數來初始化數據成員,而對於沒有類內初始值的
其餘成員,執行默認初始化(未定義的)。固然,這些都是創建在程序員沒有顯式地定義一個構造函數的前提下。
此外,在VS2013環境下獲得的一些結論:
···若是存在構造函數,將優先執行構造函數初始值列表,而後執行類內初始化,若是初始值列表提供了一些數據成員初始值,編譯器將再也不調用這些成員的類內的初始化(如 果有的話),來提升效率。
···在建立一個對象的時候,首先會調用構造函數,任何狀況下都是如此,沒有例外。
···在構造函數體開始執行之前,對象的初始化其實已經完成了。
···在VS2013 的環境下,編譯器彷佛必需要生成一個默認構造函數,就是若是定義一個類數據成員都是內置類型,且沒有類內初始化,且沒有顯式地定義構造函數,這時,
編譯器是不答應的,也就是說若是你不給它生成默認構造函數的機會,它是不肯意的。
···彷佛別人都是在Linux下能夠方便的查看合成默認構造函數能夠調用,然而我在VS下找了半天也沒找到。
參考:https://www.zhihu.com/question/30804314/answer/49894954
參考:http://www.cnblogs.com/fanzhidongyzby/archive/2013/01/12/2858040.html
至此爲止,不折不扣搞清楚了C++的構造函數與初始化問題!!!
神清氣爽!!!實踐出真知啊!!!