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