溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

C++中各種初始化方式示例詳解

發(fā)布時(shí)間:2020-09-11 05:01:20 來源:腳本之家 閱讀:242 作者:daisy 欄目:編程語言

前言

本文主要給大家介紹了關(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)化:

  • 默認(rèn)初始化 :如果 T 是一個(gè)類,那么調(diào)用默認(rèn)構(gòu)造函數(shù)進(jìn)行初始化;如果是一個(gè)數(shù)組,每個(gè)元素默認(rèn)初始化,否則不進(jìn)行初始化,其值未定義。至于 合成的 默認(rèn)構(gòu)造函數(shù)初始化數(shù)據(jù)成員的規(guī)則是:1.如果類數(shù)據(jù)成員存在類內(nèi)初始值,則用該值初始化相應(yīng)成員(c++11);2.否則,默認(rèn)初始化數(shù)據(jù)成員。
  • 值初始化 :如果 T 是一個(gè)類,那么類的對(duì)象進(jìn)行默認(rèn)初始化( 如果T類型的默認(rèn)構(gòu)造函數(shù) 不是 用戶自定義的,默認(rèn)初始化之前先進(jìn)行零初始化 );如果是一個(gè)數(shù)組,每個(gè)元素值初始化,否則進(jìn)行零初始化。
  • 零初始化 :對(duì)于static或者thread_local變量將會(huì)在其他類型的初始化之前先初始化。如果T是算數(shù)、指針、枚舉類型,將會(huì)初始化為0;如果是類類型,基類和數(shù)據(jù)成員會(huì)零初始化;如果是數(shù)組,數(shù)組元素也零初始化。

看一下上面的例子,如果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ì)被合成。

  • 對(duì)于Init1,由于我們對(duì)其沒有做任何構(gòu)造函數(shù)的聲明和定義,因此會(huì)合成默認(rèn)構(gòu)造函數(shù)。
  • 對(duì)于Init2,我們?cè)陬悆?nèi)部聲明處要求合成默認(rèn)構(gòu)造函數(shù),因此也會(huì)有合成的默認(rèn)構(gòu)造函數(shù)。

由于Init1和Init2它們擁有類似的合成默認(rèn)構(gòu)造函數(shù),因此它們的ia1.i和ib1.i值相同,應(yīng)該都是隨機(jī)值,而ia2.i和ib2.i被要求值初始化,因此它們的值都是0。

  • 對(duì)于Init3,我們?cè)陬愅獠慷x處要求編譯器為我們生成默認(rèn)構(gòu)造函數(shù),此默認(rèn)構(gòu)造函數(shù)為用戶自定義的默認(rèn)構(gòu)造函數(shù)。
  • 對(duì)于Init4,我們顯式的定義了用戶自定義默認(rèn)構(gòu)造函數(shù)。

由于Init3和Init4它們擁有類似的用戶自定義默認(rèn)構(gòu)造函數(shù),因此它們的ic1.i和id1.i值相同,應(yīng)該都是隨機(jī)值,而ic2.i和id2.i雖然被要求值初始化,但也是隨機(jī)值。

  • 對(duì)于Init5,我們顯式的定義了用戶自定義默認(rèn)構(gòu)造函數(shù),并且使用了構(gòu)造函數(shù)初始化列表來值初始化數(shù)據(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ì)億速云的支持。

向AI問一下細(xì)節(jié)

免責(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)容。

AI