一文总结现代 C++ 中的初始化
本文嘗試回答:
- 現(xiàn)代 C++ 有哪幾種初始化形式?分別能夠用于什么場景?有什么限制?
-
MyClass obj();為什么沒有調(diào)用默認無參構(gòu)造函數(shù)創(chuàng)建一個對象? -
new int和new int()有什么區(qū)別? - 直接初始化、拷貝初始化、列表初始化、默認初始化、值初始化、類內(nèi)初始值、構(gòu)造函數(shù)初始值列表的區(qū)別與聯(lián)系?
- 初始化和賦值的區(qū)別?
- 類成員有幾種初始化方式,其初始化順序是由什么決定的?
- 初始化相關(guān)的注意事項及最佳實踐?
1. 內(nèi)置類型和類類型
正式開始介紹初始化之前,先要區(qū)分 C++ 中的兩種數(shù)據(jù)類型:內(nèi)置類型和類類型。
- 內(nèi)置類型:char、bool、short、int、float、double、指針等 C++ 語言支持的最基礎(chǔ)的數(shù)據(jù)類型
- 類類型:標準庫以及我們自己定義的各種類、模板類等,如
MyClass、std::vector<T>、std::string、std::unique_ptr<T>...
2. C++ 初始化的 4 種形式
初始化是指在創(chuàng)建對象(為特定類型的變量申請存儲空間)的同時賦予初始值。現(xiàn)代 C++ 中,一共有 4 種初始化形式:
- 等號
=... - 等號+花括號
={...} - 花括號
{...} - 圓括號
(...)
無論是內(nèi)置類型還是類類型,都支持這 4 種形式的初始化:
int i1=0; // (1)
int i2={0}; // (2)
int i3{0}; // (3)
int i4(0); // (4)
std::string s1="hello"; // (1)
std::string s2={"hello"}; // (2)
std::string s3{"hello"}; // (3)
std::string s4("hello"); // (4)
3. 初始化和賦值
前兩種初始化雖然在形式上都用了等號 =,但初始化的等號和賦值的等號具有不同的含義。C++ 中賦值和初始化是兩種完全不同的操作,只是恰巧都用了等號 =。就好比乘法和解引用都用了 *,含義卻完全不同。
- 初始化:為變量申請存儲空間,創(chuàng)建新的變量。如果是類類型,將調(diào)用類的構(gòu)造函數(shù)
-
賦值:把一個現(xiàn)有變量的值用另一個值替代,不創(chuàng)建新的變量。如果是類類型,將調(diào)用類的賦值運算符
operator=()
int a = 1; // 初始化
a = 2; // 賦值
MyClass obj1; // 初始化,調(diào)用 MyClass() 構(gòu)造函數(shù)
MyClass obj2{42, "hello"}; // 初始化,調(diào)用 MyClass(int, string) 構(gòu)造函數(shù)
obj1 = obj2; // 賦值,調(diào)用 operator=(const MyClass&)
4. 拷貝初始化和直接初始化
int i1=0; // (1) 拷貝初始化
int i2={0}; // (2) 拷貝初始化
int i3{0}; // (3) 直接初始化
int i4(0); // (4) 直接初始化
std::string s1="hello"; // (1) 拷貝初始化
std::string s2={"hello"}; // (2) 拷貝初始化
std::string s3{"hello"}; // (3) 直接初始化
std::string s4("hello"); // (4) 直接初始化
C++ 初始化的 4 種形式中,前兩種初始化形式 (1)(2) 使用了等號,叫做拷貝初始化,后兩種 (3)(4) 沒有等號,叫做直接初始化。無論是拷貝初始化,還是直接初始化,都是初始化,不是賦值!對于類類型,都是調(diào)用構(gòu)造函數(shù),不會調(diào)用賦值運算符!在絕大多數(shù)情況下(TODO:補充反例),拷貝初始化和直接初始化除了形式上多一個/少一個等號之外,底層代碼上沒有任何區(qū)別。
注意:雖然叫做拷貝初始化,但構(gòu)造 s1、s2 的過程中,不存在“拷貝”!底層代碼和 s3、s4 完全相同,都是直接調(diào)用 string 的構(gòu)造函數(shù)(不信可以去 cppinsights.io 自行驗證)。
5. 列表初始化
列表初始化(list initialization):使用花括號 {} 形式的初始化。C++ 的 4 種初始化形式中的 (2)(3) 都屬于列表初始化。列表初始化在 C++11 中得到全面應(yīng)用,其最大的特點在于可以防止窄化轉(zhuǎn)換:如果列表初始化存在信息丟失的風(fēng)險, 編譯器將報錯。不僅如此,列表初始化還能用于各種初始化場景,包括類內(nèi)初始值以及 Most Vexing Parse 場景。
a. 防止窄化轉(zhuǎn)換
long double ld = 3.1415;
int a{ld}; // 無法編譯,轉(zhuǎn)換存在信息丟失的風(fēng)險
int b = {ld}; // 無法編譯,轉(zhuǎn)換存在信息丟失的風(fēng)險
int c(ld); // 可以編譯,但信息丟失
int d = ld; // 可以編譯,但信息丟失
b. 避免 Most Vexing Parse
class MyClass {
public:
MyClass();
MyClass(int x);
MyClass(int x, int y);
};
int main() {
MyClass obj1(1); // OK
MyClass obj2{1}; // OK,列表初始化
MyClass obj3(1,2); // OK
MyClass obj4(1,2); // OK,列表初始化
// 錯誤,obj5 被解析為函數(shù)聲明:參數(shù)為空,返回 MyClass
MyClass obj5();
MyClass obj6{}; // OK,列表初始化
MyClass obj7; // OK
}
注意:
obj5并不是創(chuàng)建一個默認構(gòu)造的對象,而是被解析為一個函數(shù)聲明,參數(shù)為空,返回 MyClass。有的編譯期會給出警告 warning: empty parentheses were disambiguated as a function declaration [-Wvexing-parse]?
6. 默認初始化
默認初始化(default initialization):當(dāng)對象未被顯式地賦予初值時執(zhí)行的初始化行為。
默認初始化的例子:
int i;
std::string s;
MyClass* p = new MyClass;
double* pd = new double;
- 類類型:由類的默認(無參)構(gòu)造決定
- 內(nèi)置類型(指針、int、double、float、bool、char 等)及其數(shù)組:
- 全局(包括定義在任何函數(shù)之外、命名空間之內(nèi)的)變量或局部靜態(tài)變量:初始化為 0(這種情況也叫值初始化)
- 局部非靜態(tài)變量或類成員:未定義(未初始化)
如果類沒有默認(無參)構(gòu)造函數(shù),則該類不支持默認初始化。
7. 值初始化
值初始化(value initialization):默認初始化的特殊情況,此時內(nèi)置類型會被初始化為 0。
值初始化的場景:
- STL 容器只指定元素數(shù)量,而不指定初值時,就會執(zhí)行值初始化,如
vector<int> vec(10);:10 個 int,初始化為 0 - 全局(包括定義在任何函數(shù)之外、命名空間之內(nèi)的)變量或局部靜態(tài)變量:初始化為 0
- new 類型,后面帶括號,如:
new int(),new string{} - 初始值列表為空
{},如double d{};、int *p{};
類類型沒必要區(qū)分是默認初始化還是值初始化:類類型的初始化總是由類的構(gòu)造函數(shù)決定,與在函數(shù)內(nèi)/外、全局/局部/類成員、靜態(tài)/非靜態(tài)、默認初始化/值初始化無關(guān)!如果類不含默認(無參)構(gòu)造,則該類無法進行默認初始化/值初始化!
8. new 的初始化
// 對于類類型,有無括號沒區(qū)別
string *ps1 = new string; // 默認初始化為空 string
string *ps2 = new string(); // 值初始化為空 string
string *ps3 = new string{}; // 值初始化為空 string
// 對于內(nèi)置類型,有括號進行值初始化,沒有括號的值未定義!
int *pi1 = new int; // 默認初始化,*pi1 值未定義!
int *pi2 = new int(); // 值初始化,*pi2 為 0
int *pi3 = new int{}; // 值初始化,*pi3 為 0
const int *pci1 = new const int(1024); // 分配并初始化一個 const int
const int *pci2 = new const int{1024}; // 分配并初始化一個 const int
9. 類的初始化
類成員有兩種初始化方式:類內(nèi)初始值(成員初始化器,in-class member initializer)以及構(gòu)造函數(shù)初始值列表(constructor initialize list)。
不要在構(gòu)造函數(shù)體內(nèi)部初始化數(shù)據(jù)成員,因為只有當(dāng)類的所有成員初始化完成之后才開始執(zhí)行構(gòu)造函數(shù)體,此時并不是真正意義上的初始化,而是重新賦值!也正是因為如此,引用成員、const 成員只能通過類內(nèi)初始值或者構(gòu)造函數(shù)初始值列表初始化,而不能在構(gòu)造函數(shù)體內(nèi)部“初始化”。不僅如此,在構(gòu)造函數(shù)體內(nèi)部進行賦值,相比于內(nèi)類初始值/構(gòu)造函數(shù)初始化列表的只調(diào)用一次構(gòu)造函數(shù),多了一次賦值操作,效率更低。
注意:對于內(nèi)置類型的數(shù)據(jù)成員,如果沒有對其進行顯式初始化,其值未定義!
9.1 類內(nèi)初始值/成員初始化器
在類中聲明類的數(shù)據(jù)成員同時提供初始值,初始值可以是字面值、表達式甚至是函數(shù)調(diào)用。形式上可以用等號或者花括號,但是不能用圓括號。C++11 之后首選的初始化類成員方式。
class SalesData {
unsigned unitsSold = 0;
double revenue {0.0};
std::string bookNo{"hello"};
shared_ptr<int> sp={make_shared<int>(5)};
};
9.2 構(gòu)造函數(shù)初始值列表
如果需要根據(jù)傳入構(gòu)造函數(shù)的參數(shù)來初始化類成員,可以使用構(gòu)造函數(shù)初始值列表。構(gòu)造函數(shù)初始值列表的形式是在構(gòu)造函數(shù)的形參列表之后,使用冒號分隔,接著是成員名字,然后使用圓括號或花括號來包裹初始化的表達式,多個成員之間通過逗號分隔。
class SalesData {
public:
SalesData(const std::string &s) : bookNo(s) {}
SalesData(const std::string &s, unsigned n, double p) : bookNo(s), unitsSold(n), revenue(p*n) {}
};
注意:類的數(shù)據(jù)成員初始化順序和構(gòu)造函數(shù)初始化列表中的順序無關(guān),而是由成員在類中聲明的順序決定:
class X {
int x;
int y;
public:
// 先用未初始化的 y 初始化 x,再用 val 初始化 y
X(int val): y(val), x(y){}
};
上述 x 值未定義!一般編譯器會給出警告。
9.3 類成員的初始化順序
類的數(shù)據(jù)成員初始化順序由成員在類中聲明的順序決定,按照聲明的順序,依次構(gòu)造每個成員,所有成員構(gòu)造完成后才執(zhí)行構(gòu)造函數(shù)。
順便說一句,析構(gòu)順序與初始化順序相反:先執(zhí)行析構(gòu)函數(shù),再按照構(gòu)造相反的順序依次析構(gòu)每個成員。
10. 總結(jié)
現(xiàn)代 C++ 4 種初始化形式:
| 序號 | 形式 | 拷貝/直接初始化 | 可用于構(gòu)造函數(shù)初始值列表 | 可用于類內(nèi)初始值 | 備注 |
|---|---|---|---|---|---|
| 1 | 等號 =
|
拷貝初始化 | ? | ? | |
| 2 | 等號+花括號 ={}
|
拷貝初始化 | ? | ? | 列表初始化 |
| 3 | 花括號 {}
|
直接初始化 | ? | ? | 推薦!列表初始化,能用于各種初始化場景! |
| 4 | 圓括號 ()
|
直接初始化 | ? | ? | 存在 Most Vexing Parse 問題、不可用于類內(nèi)初始值及提供多個初始元素值的列表 vector<string> v("a", "an", "the");
|
- 拷貝初始化:使用
=形式的初始化。 - 直接初始化:不使用
=形式的初始化(使用{}或()形式初始化) - 列表初始化:使用
{}形式的初始化,能夠用于各種初始化場景,也被稱為統(tǒng)一初始化 - 默認初始化:未顯式指定初始值的初始化行為。類類型將調(diào)用默認無參構(gòu)造函數(shù);而內(nèi)置類型可能被值初始化為 0,也可能未被初始化(值未定義)!
- 值初始化:默認初始化的特殊情況,對于內(nèi)置類型,其值將被初始化為 0。
- 類內(nèi)初始值/成員初始化器:聲明類成員的同時直接提供初值,C++11 之后的首選初始化類成員的方式。
- 構(gòu)造函數(shù)初始值列表:能夠根據(jù)傳入構(gòu)造函數(shù)的參數(shù)進行初始類成員
11. 最佳實踐/核心指南
-
總是初始化內(nèi)置類型的變量,如
int i{};。最好使用 auto,因為 auto 會強迫初始化:不提供初始值就無法推導(dǎo)類型。 -
推薦使用
{}統(tǒng)一列表初始化,形式統(tǒng)一,能用于各種場景。 -
對于類成員的初始化,優(yōu)先考慮類內(nèi)初始值。如果需要根據(jù)傳入構(gòu)造函數(shù)的參數(shù)來初始化成員,可以使用構(gòu)造函數(shù)初始值列表,不要在構(gòu)造函數(shù)體內(nèi)部對類成員進行賦值。
-
C++核心指南 C.45:如果只是初始化類的數(shù)據(jù)成員, 不需要專門定義構(gòu)造函數(shù),用類內(nèi)初始值。
-
C++核心指南 NR.5:不要兩步初始化,類的構(gòu)造函數(shù)應(yīng)該直接完成類的初始化工作,不要把初始化的任務(wù)轉(zhuǎn)移/強加給類的用戶(例如要求用戶在創(chuàng)建一個類的對象后,再額外調(diào)用一個
Init()之類的函數(shù))。
12. 擴展閱讀
- C++ 何時調(diào)用默認構(gòu)造、拷貝構(gòu)造、移動構(gòu)造、拷貝賦值、移動賦值、析構(gòu)以及對象析構(gòu)順序
- C++ Primer 查漏補缺 —— C++ 中的各種初始化
總結(jié)
以上是生活随笔為你收集整理的一文总结现代 C++ 中的初始化的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Gin 框架之jwt 介绍与基本使用
- 下一篇: 梳理拯救烂怂代码?我是这么做的