C++中列表初始化,你知多少?
一、何為列表初始化
C++中的列表初始化是一種用一對花括號 {} 來進行對象初始化的語法。它被引入主要是為了提供一種統一的初始化方式,適用于各種不同的數據類型和數據結構,包括基本類型、數組、結構體、類、STL 容器等。列表初始化在 C++11 標準中被引入,是現代 C++ 編程風格的一部分。
基本語法
Type variable = {value1, value2, ...};
- 使用一對花括號 {} 來初始化對象。
- 列表初始化對于類型轉換更為嚴格,不允許縮窄轉換(請看下面何為窄轉化部分)。
示例
- 基本類型:
int x = {42};
double y = {3.14};
- 數組:
int arr[] = {1, 2, 3, 4, 5};
- 結構體:
struct Point {
int x;
int y;
};
Point p = {10, 20};
- 類:
class MyClass {
public:
int data;
double value;
MyClass(int d, double v) : data(d), value(v) {}
};
MyClass obj = {42, 3.14};
- STL 容器:
#include <vector>
#include <map>
std::vector<int> vec = {1, 2, 3, 4, 5};
std::map<int, std::string> myMap = {{1, "one"}, {2, "two"}};
- 自定義類型:
class MyType {
public:
int x;
double y;
MyType(int a, double b) : x(a), y(b) {}
};
MyType myVar = {5, 2.5};
二、何為窄轉化
窄轉化(Narrowing Conversion)指的是將一個具有較大范圍的值轉換為較小范圍的類型時可能丟失信息的情況。這種轉換可能導致截斷或失真,因為目標類型的表示范圍比源類型小。在 C++ 中,窄轉化是一種不安全的類型轉換,因為它可能導致數據丟失或意外的行為。
以下是一些示例說明窄轉化:
- 從浮點數到整數:
double myDouble = 3.14;
int myInt = myDouble; // 窄轉化,可能會截斷小數部分
- 從長整型到整數:
long long myLong = 1000000000000;
int myInt = myLong; // 窄轉化,可能會截斷或溢出
- 從大范圍的整數類型到小范圍的整數類型:
long long myLong = 1000000000000;
int myInt = static_cast<int>(myLong); // 窄轉化,可能會截斷或溢出
窄轉化是需要小心處理的,因為它可能導致數據的損失和不確定的行為。在需要進行類型轉換時,最好使用安全的轉換方式,例如使用 static_cast 并在可能丟失信息的地方進行顯式的檢查和處理。在 C++11 引入的列表初始化中,提供了對縮窄轉換的更嚴格的檢查,不允許在列表初始化時發生縮窄轉換,從而幫助程序員避免潛在的問題。
三、列表初始化規則和特點
列表初始化有一些規則和特點,主要包括以下幾個方面:
1. 不允許縮窄轉換
列表初始化對類型轉換更為嚴格,不允許發生縮窄轉換,即不允許將一個精度更高的類型賦值給一個精度較低的類型。
int x = {3.14}; // 錯誤,嘗試縮窄轉換
2. 對于數組,列表初始化的大小由元素個數決定
int arr[] = {1, 2, 3}; // 合法,數組大小為3
3. 類型不匹配時可能調用構造函數
當列表初始化的類型和目標類型不匹配時,如果存在適當的構造函數,編譯器會嘗試調用構造函數進行初始化。
class MyClass {
public:
int data;
double value;
MyClass(int d, double v) : data(d), value(v) {}
};
MyClass obj = {42, 3.14}; // 合法,調用構造函數
4. 空列表初始化
在某些情況下,可以使用空的花括號 {} 進行初始化,這會被解釋為對應類型的默認值。
int x = {}; // x 被初始化為 0
double y = {}; // y 被初始化為 0.0
5. 對于類類型,構造函數的匹配規則
當進行列表初始化時,編譯器會根據構造函數的參數匹配規則選擇相應的構造函數。
class Example {
public:
Example(int a, double b);
Example(std::string str);
};
Example obj1 = {42, 3.14}; // 調用構造函數 Example(int, double)
Example obj2 = {"Hello"}; // 調用構造函數 Example(std::string)
6. 嵌套初始化
可以使用嵌套的列表初始化來初始化嵌套的數據結構。
std::vector<std::vector<int>> matrix = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
總體來說,列表初始化提供了一種簡潔且直觀的初始化語法,同時對類型匹配和轉換有著更為嚴格的規定,減少了一些初始化時可能發生的錯誤。
四、列表初始化的好處
列表初始化(Uniform Initialization)在 C++ 中引入的好處主要有以下幾點:
- 一致性:列表初始化提供了一種一致的初始化語法,可以用于初始化各種類型的對象,包括基本類型、數組、結構體、類、STL 容器等。這種一致性使得代碼更加清晰和易讀。
int x = {42}; // 初始化基本類型
int arr[] = {1, 2, 3}; // 初始化數組
Point p = {10, 20}; // 初始化結構體
MyClass obj = {42, 3.14}; // 初始化類
std::vector<int> vec = {1, 2, 3}; // 初始化容器
- 防止窄化轉換:列表初始化對類型轉換更為嚴格,不允許發生縮窄轉換,從而減少了一些可能引入 bug 的情況。
int x = {3.14}; // 錯誤,嘗試縮窄轉換
- 構造函數匹配:當進行列表初始化時,如果存在適當的構造函數,編譯器會嘗試調用構造函數進行初始化。這提高了代碼的靈活性,使得用戶定義的類型更容易進行初始化。
class MyClass {
public:
int data;
double value;
MyClass(int d, double v) : data(d), value(v) {}
};
MyClass obj = {42, 3.14}; // 調用構造函數
- 簡潔性:列表初始化的語法相對簡潔,通過一對花括號{}就可以完成初始化,避免了傳統的各種初始化方式可能導致的歧義。
int arr[] = {1, 2, 3}; // 合法,簡潔
- 避免 most vexing parse: 傳統的初始化語法在某些情況下可能會導致 most vexing parse,而列表初始化語法避免了這一問題。
"Most Vexing Parse" 是一個有趣而令人困擾的 C++ 編程問題,它通常發生在類的對象聲明上,導致程序員可能不是按照他們預期的方式初始化對象。這個問題的名字來源于這種情況的令人迷惑和難以理解。
Most Vexing Parse 主要發生在下面這樣的情況:
class MyClass {
public:
MyClass() {}
};
int main() {
MyClass obj(); // 最令人迷惑的解析,聲明了一個函數而不是對象
// ...
return 0;
}
在上述代碼中,MyClass obj(); 被編譯器解釋為聲明一個返回 MyClass 類型的函數而不是創建一個 MyClass 類型的對象。這是因為在 C++ 中,如果聲明一個函數的時候帶有空括號,編譯器會將其解釋為一個函數聲明而不是一個對象定義。
為了避免 most vexing parse,可以使用以下兩種方式之一:
- 使用花括號初始化:
MyClass obj{}; // 使用花括號初始化,避免 most vexing parse
- 使用括號初始化:
MyClass obj(); // 編譯器會將其解釋為函數聲明
MyClass obj{}; // 使用括號初始化,避免 most vexing parse
這個問題是由 C++ 語法規則引起的,對于初學者來說可能會令人困擾。因此,在聲明和初始化對象時,特別是在有可能發生 most vexing parse 的地方,建議使用花括號初始化或括號初始化,以避免潛在的問題。
五、不適用列表初始化的情況
什么是聚合類型
1、類型是一個普通數組,如int[5],char[],double[]等
2、類型是一個類,且滿足以下條件:
- 沒有用戶聲明的構造函數
- 沒有用戶提供的構造函數(允許顯示預置或棄置的構造函數)
- 沒有私有或保護的非靜態數據成員
- 沒有基類
- 沒有虛函數
- 沒有{}和=直接初始化的非靜態數據成員
- 沒有默認成員初始化器
雖然列表初始化是一種很方便和清晰的初始化方式,但有一些情況下不適合或者不能使用列表初始化:
- 不支持聚合初始化的類列表初始化主要用于聚合類型的初始化,而對于不支持聚合初始化的類,不能使用列表初始化。一個類如果有用戶自定義的構造函數、私有/受保護的非靜態數據成員,或者基類沒有默認構造函數,那么該類就不再是聚合類型。
class NotAggregate {
public:
int x;
int y;
NotAggregate(int a, int b) : x(a), y(b) {}
};
NotAggregate obj = {1, 2}; // 錯誤,NotAggregate 不是聚合類型
- 不能進行窄化轉換的地方:列表初始化不允許發生窄化轉換,因此在需要執行窄化轉換的地方不能使用列表初始化。
double myDouble = 3.14;
int myInt = myDouble; // 合法,但列表初始化會報錯
- 需要避免 most vexing parse 的地方:在可能發生 most vexing parse(最令人迷惑的解析)的地方,列表初始化可能不適用。這通常發生在類的默認構造函數被誤解為函數聲明的情況下。
class MyClass {
public:
MyClass() {}
};
MyClass obj(); // 最令人迷惑的解析,聲明了一個函數而不是對象
MyClass obj{}; // 正確的初始化方式
總之,雖然列表初始化是一種很便捷和安全的初始化方式,但在某些情況下,特別是對于非聚合類型和可能導致 most vexing parse 的地方,可能需要考慮其他的初始化方式。