您好,登錄后才能下訂單哦!
前言
本文主要給大家介紹了關于C++初始化方式的相關內容,分享出來供大家參考學習,下面話不多說了,來一起看看詳細的介紹吧。
C++小實驗測試:下面程序中main函數里a.a和b.b的輸出值是多少?
#include <iostream> struct foo { foo() = default; int a; }; struct bar { bar(); int b; }; bar::bar() = default; int main() { foo a{}; bar b{}; std::cout << a.a << '\t' << b.b; }
答案是a.a是0,b.b是不確定值(不論你是gcc編譯器,還是clang編譯器,或者是微軟的msvc++編譯器)。為什么會這樣?這是因為C++中的初始化已經開始畸形發展了。
接下來,我要探索一下為什么會這樣。在我們知道原因之前,先給出一些初始化的概念:默認初始化,值初始化,零初始化。
T global; //T是我們的自定義類型,首先零初始化,然后默認初始化 void foo() { T i; //默認初始化 T j{}; //值初始化(C++11) T k = T(); //值初始化 T l = T{}; //值初始化(C++11) T m(); //函數聲明 new T; //默認初始化 new T(); //值初始化 new T{}; //值初始化(C++11) } struct A { T t; A() : t() //t將值初始化 { //構造函數 } }; struct B { T t; B() : t{} //t將值初始化(C++11) { //構造函數 } }; struct C { T t; C() //t將默認初始化 { //構造函數 } };
上面這些不同形式的初始化方式有點復雜,我會對這些C++11的初始化做一下簡化:
看一下上面的例子,如果T是int類型,那么global和那些T類型的使用值初始化形式的變量都會初始化為0(因為int是內置類型,不是類類型,也不是數組,將會零初始化,又因為int是算術類型,如果進行零初始化,則初始值為0)而其他的默認初始化都是未定義值。
回到開頭的例子,現在我們已經有了搞明白這個例子所必要的基礎知識。造成結果不同的根本原因是:foo和bar被它們不同位置的默認構造函數所影響。
foo的構造函數在起初聲明時是要求默認合成,而不是我們自定義提供的,因此它屬于編譯器 合成的 默認構造函數 。而bar的構造函數則不同,它是在定義時被要求合成,因此它屬于我們用戶 自定義的 默認構造函數 。
前面提到的關于值初始化的規則時,有說明到: 如果T類型的默認構造函數不是用戶自定義的,默認初始化之前先進行零初始化 。因為foo的默認構造函數不是我們自定義的,是編譯器合成的,所以在對foo類型的對象進行值初始化時,會先進行一次零初始化,然后再調用默認構造函數,這導致a.a的值被初始化為0,而bar的默認構造函數是用戶自定義的,所以不會進行零初始化,而是直接調用默認構造函數,從而導致b.b的值是未初始化的,因此每次都是隨機值。
這個陷阱迫使我們注意:如果你不想要你的默認構造函數是用戶自定義的,那么必須在類的內部聲明處使用"=default",而不是在類外部定義處使用。
對于類類型來說,用戶提供自定義的默認構造函數有一些額外的“副作用”。比如,對于缺少用戶提供的自定義默認構造函數的類,是無法定義該類的const對象的。示例如下:
class exec { int i; }; const exec e; //錯誤!缺少用戶自定義默認構造函數,不允許定義const類對象
通過開頭的例子,我們已經對C++的一些初始化方式有了直觀的感受。 C++中的初始化分為6種:零 初始化、 默認初始化、值初始化、直接初始化、拷貝初始化、列表初始化。
零初始化和變量的類型和位置有關系,比如是否static,是否aggregate聚合類型。能進行0初始化的類型的對象的值都是0,比如int為0,double為0.0,指針為nullptr;
現在我們已經了解了幾種初始化的規則,下面則是幾種初始化方式的使用形式:
1. 默認初始化是定義對象時,沒有使用初始化器,也即沒有做任何初始化說明時的行為。典型的:
int i; vector<int> v;
2. 值初始化是定義對象時,要求初始化,但沒有給出初始值的行為。典型的:
int i{}; new int(); new int{}; //C++11
3. 直接初始化和拷貝初始化主要是相對于我們自定義的對象的初始化而言的,對于內置類型,這兩者沒有區別。對于自定義對象,直接初始化和拷貝初始化區別是直接調用構造函數還是用"="來進行初始化。典型的:
vector<int> v1(10); //直接初始化,匹配某一構造函數 vector<string> v2(10); //直接初始化,匹配某一構造函數 vector<int> v3=v1; //拷貝初始化,使用=進行初始化
對于書本中給出的示例:
string dots(10, '.'); //直接初始化 string s(dots); //直接初始化
這里s的初始化書本說是直接初始化,看起來似乎像是拷貝初始化,其實的確是直接初始化,因為直接初始化是用參數來直接匹配某一個構造函數,而拷貝構造函數和其他構造函數形成了重載,以至于剛好調用了拷貝構造函數。
事實上,C++語言標準規定復制初始化應該是先調用對應的構造函數創建一個臨時對象,然后拷貝構造函數再將構造的臨時對象拷貝給要創建的對象。例如:
string a = "hello";
上面代碼中,因為“hello"的類型是const char *,所以string類的string(const char *)構造函數會被首先調用,創建一個臨時對象,然后拷貝構造函數將這個臨時對象復制到a。但是標準還規定,為了提高效率,允許編譯器跳過創建臨時對象這一步,直接調用構造函數構造要創建的對象,從而忽略調用拷貝構造函數進行優化,這樣就完全等價于直接初始化了,當然可以使用-fno-elide-constructors選項來禁用優化。
如果我們將string類型的拷貝構造函數定義為private或者定義為delete,那么就無法通過編譯,雖然能夠進行優化省略拷貝構造函數的調用,但是拷貝構造函數在語法上還是要能正常訪問的,這也是為什么C++ primer第五版第13章拷貝控制13.1.1節末尾442頁最后一段話中說:
“即使編譯器略過了拷貝/移動構造函數,但在這個程序點上,拷貝/移動構造函數必須是存在且可訪問的(例如,不能是priviate的)。
拷貝初始化不僅在使用=定義變量時會發生,在以下幾種特殊情況中也會發生:
1.將一個對象作為實參傳遞給一個非引用的形參;
2.從一個返回類型為非引用的函數返回一個對象;
3.用花括號列表初始化一個數組中的元素或一個聚合類中的成員。
其實還有一個情況,比如:當以值拋出或捕獲一個異常時。
另外還有比較讓人迷惑的地方在于vector<string> v2(10)
,在《C++ Primer 5th》中說這是值初始化的方式,但是仔細看書本,這里的值初始化指的是容器中string元素,也就是說v2本身是直接初始化的,而v2中的10個string元素,由于沒有給出初始值,因此標準庫對容器中的元素采用了值初始化的方式進行初始化。
結合來說:
只要使用了括號(圓括號或花括號)但沒有給出具體初始值,就是值初始化。可以簡單理解為括號告訴編譯器你希望該對象初始化。
沒有使用括號,就是默認初始化。可以簡單理解成,你放任不管,允許編譯器使用默認行為。通常這是糟糕的行為,除非你真的懂自己在干什么。
4. 列表初始化是C++新標準給出的一種初始化方式,可用于內置類型,也可以用于自定義對象,前者比如數組,后者比如vector。典型的:
int array[5]={1,2,3,4,5}; vector<int> v={1,2,3,4,5};
文章寫到這里,讀者認真的看到這里,似乎已經懂了C++的各種初始化規則和方式,下面用幾個例子來檢測一下:
#include <iostream> using namespace std; class Init1 { public: int i; }; class Init2 { public: Init2() = default; int i; }; class Init3 { public: Init3(); int i; }; Init3::Init3() = default; class Init4 { public: Init4(); int i; }; Init4::Init4() { //constructor } class Init5 { public: Init5(): i{} { } int i; }; int main(int argc, char const *argv[]) { Init1 ia1; Init1 ia2{}; cout << "Init1: " << " " << "i1.i: " << ia1.i << "\t" << "i2.i: " << ia2.i << "\n"; Init2 ib1; Init2 ib2{}; cout << "Init2: " << " " << "i1.i: " << ib1.i << "\t" << "i2.i: " << ib2.i << "\n"; Init3 ic1; Init3 ic2{}; cout << "Init3: " << " " << "i1.i: " << ic1.i << "\t" << "i2.i: " << ic2.i << "\n"; Init4 id1; Init4 id2{}; cout << "Init4: " << " " << "i1.i: " << id1.i << "\t" << "i2.i: " << id2.i << "\n"; Init5 ie1; Init5 ie2{}; cout << "Init5: " << " " << "i1.i: " << ie1.i << "\t" << "i2.i: " << ie2.i << "\n"; return 0; }
試問上面代碼中,main程序中的各個輸出值是多少?先不忙使用編譯器編譯程序,根據之前介紹的知識先推斷一番:
首先,我們需要明白,對于類來說,構造函數是用來負責類對象的初始化的,一個類對象無論如何一定會被初始化。也就是說,當實例化類對象時,一定會調用構造函數,不論構造函數是否真的初始化了數據成員。故而對于沒有定義任何構造函數的自定義類來說,該類的默認構造函數不存在“被需要/不被需要”這回事,它必然會被合成。
由于Init1和Init2它們擁有類似的合成默認構造函數,因此它們的ia1.i和ib1.i值相同,應該都是隨機值,而ia2.i和ib2.i被要求值初始化,因此它們的值都是0。
由于Init3和Init4它們擁有類似的用戶自定義默認構造函數,因此它們的ic1.i和id1.i值相同,應該都是隨機值,而ic2.i和id2.i雖然被要求值初始化,但也是隨機值。
由于Init5我們為它顯式提供了默認構造函數,并且手動的初始化了數據成員,因此它的ie1.i和ie2.i都會被初始化為0。
以上是我們的預測,結果會是這樣嗎?遺憾的是,結果不一定是這樣。是我們哪里出錯了?我們并沒有錯誤,上面的程序結果取決于你使用的操作系統、編譯器版本(比如gcc-5.0和gcc-7.0)和發行版(比如gcc和clang)。可能有的人能獲得和推測完全相同的結果,而有的人不能,比如在經常被批不遵守C++標準的微軟VC++編譯器(VS 2017,DEBUG模式)下,結果卻完全吻合(可能是由于微軟開始接納開源和Linux,逐漸的嚴格遵守了語言標準),GCC的結果也是完全符合,而廣受好評的Clang卻部分結果符合。當然,相同的Clang編譯器在Mac和Ubuntu下結果甚至都不一致,GCC在某些時候甚至比Clang還人性化的Warning告知使用了未初始化的數據成員。
雖然,上面程序中有一些地方因為操作系統和編譯器的原因和我們預期的結果不相同,但也有必然相同的地方,比如最后一個使用了構造函數初始化列表的類的行為就符合預期。還有在合成的默認構造函數之前會先零初始化的地方,必然會初始化為0。
至此,我們已經對C++的初始化方式和規則已經有了一個了然于胸的認識,那就是:由于平臺和編譯器的差異,以及對語言標準的遵守程度不同,我們決不能依賴于合成的默認構造函數。這也是為什么C++ Primer中多次強調我們不要依賴合成的默認構造函數,也說明了C++ Primer在關于手動分配動態內存那里告訴我們,對于我們自定義的類類型來說,為什么要求值初始化是沒有意義的。
C++語言設計的一個基本思想是“自由”,對于某些東西它既給出了具體要求,又留出了發揮空間,而那些未加以明確的地方是屬于語言的“灰暗地帶”,我們需要小心翼翼的避過。在對象的初始化這里,推薦的做法是將默認構造函數刪除,由我們用戶自己定義自己的構造函數,并且合理的初始化到每個成員,如果需要保留默認構造函數,一定要對它的行為做到心里有數。
總結
以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,如果有疑問大家可以留言交流,謝謝大家對億速云的支持。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。