[英]Why should I prefer to use member initialization lists?
我偏向於在我的構造函數中使用成員初始化列表......但我早就忘記了這背后的原因......
您是否在構造函數中使用成員初始化列表? 如果是這樣,為什么? 如果沒有,為什么不呢?
對於POD班級成員來說,這沒什么區別,只是風格問題。 對於作為類的類成員,它避免了對默認構造函數的不必要調用。 考慮:
class A
{
public:
A() { x = 0; }
A(int x_) { x = x_; }
int x;
};
class B
{
public:
B()
{
a.x = 3;
}
private:
A a;
};
在這種情況下, B
的構造函數將調用A
的默認構造函數,然后將ax
初始化為 3。更好的方法是B
的構造函數直接調用初始化列表中A
的構造函數:
B()
: a(3)
{
}
這只會調用A
的A(int)
構造函數而不是它的默認構造函數。 在這個例子中,差異可以忽略不計,但想象一下,如果你願意, A
的默認構造函數做了更多的事情,比如分配內存或打開文件。 你不會想不必要地這樣做。
此外,如果一個類沒有默認構造函數,或者你有一個const
成員變量,你必須使用一個初始化列表:
class A
{
public:
A(int x_) { x = x_; }
int x;
};
class B
{
public:
B() : a(3), y(2) // 'a' and 'y' MUST be initialized in an initializer list;
{ // it is an error not to do so
}
private:
A a;
const int y;
};
除了上面提到的性能原因,如果您的類存儲對作為構造函數參數傳遞的對象的引用,或者您的類具有 const 變量,那么除了使用初始化列表之外,您別無選擇。
此處的答案中未提及的使用構造函數初始值設定項列表的一個重要原因是基類的初始化。
按照構造順序,基類應該在子類之前構造。 沒有構造函數初始值設定項列表,如果您的基類具有默認構造函數,該構造函數將在進入子類的構造函數之前被調用,則這是可能的。
但是,如果您的基類只有參數化構造函數,那么您必須使用構造函數初始化列表來確保您的基類在子類之前初始化。
初始化只有參數化構造函數的子對象
效率
使用構造函數初始值設定項列表,您可以將數據成員初始化為您在代碼中需要的確切狀態,而不是首先將它們初始化為默認狀態,然后將它們的狀態更改為您在代碼中需要的狀態。
如果您的類中的非靜態 const 數據成員具有默認構造函數並且您不使用構造函數初始值設定項列表,則您將無法將它們初始化為預期狀態,因為它們將被初始化為默認狀態。
當編譯器進入構造函數時,必須初始化引用數據成員,因為引用不能稍后聲明和初始化。 這僅適用於構造函數初始值設定項列表。
除了性能問題,還有另一個非常重要的問題,我稱之為代碼可維護性和可擴展性。
如果T
是 POD 並且您開始更喜歡初始化列表,那么如果一次T
將更改為非 POD 類型,則您無需更改初始化周圍的任何內容以避免不必要的構造函數調用,因為它已經優化。
如果類型T
確實有默認構造函數和一個或多個用戶定義的構造函數,並且有一次您決定刪除或隱藏默認構造函數,那么如果使用了初始化列表,則不需要更新用戶定義的構造函數的代碼,因為它們已經正確實施。
與const
成員或引用成員相同,假設最初T
定義如下:
struct T
{
T() { a = 5; }
private:
int a;
};
接下來,您決定將a
限定為const
,如果您從一開始就使用初始化列表,那么這是單行更改,但是具有如上定義的T
,它還需要挖掘構造函數定義以刪除賦值:
struct T
{
T() : a(5) {} // 2. that requires changes here too
private:
const int a; // 1. one line change
};
如果代碼不是由“代碼猴子”編寫的,而是由工程師根據對他正在做的事情的更深入的考慮來做出決定的,那么維護會容易得多,而且不容易出錯,這已經不是什么秘密了。
在運行構造函數體之前,先調用其父類的所有構造函數,然后調用其字段的所有構造函數。 默認情況下,調用無參數構造函數。 初始化列表允許您選擇調用哪個構造函數以及構造函數接收哪些參數。
如果您有引用或 const 字段,或者如果使用的類之一沒有默認構造函數,則必須使用初始化列表。
// Without Initializer List
class MyClass {
Type variable;
public:
MyClass(Type a) { // Assume that Type is an already
// declared class and it has appropriate
// constructors and operators
variable = a;
}
};
這里編譯器按照以下步驟創建一個MyClass
類型的對象:
a
”調用Type
的構造函數。Type
”的賦值運算符在MyClass()
構造函數體內調用以進行賦值。variable = a;
Type
”的析構函數被稱為“ a
”,因為它超出了范圍。 現在考慮帶有初始化列表的MyClass()
構造函數的相同代碼:
// With Initializer List
class MyClass {
Type variable;
public:
MyClass(Type a):variable(a) { // Assume that Type is an already
// declared class and it has appropriate
// constructors and operators
}
};
對於 Initializer List,編譯器遵循以下步驟:
調用“ Type
”類的復制構造函數來初始化: variable(a)
。 初始化列表中的參數用於直接復制構造“ variable
”。
“ Type
”的析構函數被稱為“ a
”,因為它超出了范圍。
只是添加一些額外的信息來演示成員初始化列表可以產生多少差異。 在 leetcode 303 Range Sum Query - Immutable, https: //leetcode.com/problems/range-sum-query-immutable/ 中,您需要構造並初始化具有特定大小的向量為零。 這是兩種不同的實現和速度比較。
沒有成員初始化列表,要獲得 AC 花費我大約212 ms 。
class NumArray {
public:
vector<int> preSum;
NumArray(vector<int> nums) {
preSum = vector<int>(nums.size()+1, 0);
int ps = 0;
for (int i = 0; i < nums.size(); i++)
{
ps += nums[i];
preSum[i+1] = ps;
}
}
int sumRange(int i, int j) {
return preSum[j+1] - preSum[i];
}
};
現在使用成員初始化列表,獲取AC的時間約為108毫秒。 通過這個簡單的例子,很明顯,成員初始化列表的效率更高。 所有測量均來自 LC 的運行時間。
class NumArray {
public:
vector<int> preSum;
NumArray(vector<int> nums) : preSum(nums.size()+1, 0) {
int ps = 0;
for (int i = 0; i < nums.size(); i++)
{
ps += nums[i];
preSum[i+1] = ps;
}
}
int sumRange(int i, int j) {
return preSum[j+1] - preSum[i];
}
};
正如 C++ 核心指南C.49:在構造函數中優先初始化而不是賦值,它可以防止對默認構造函數的不必要調用。
句法:
class Sample
{
public:
int Sam_x;
int Sam_y;
Sample(): Sam_x(1), Sam_y(2) /* Classname: Initialization List */
{
// Constructor body
}
};
需要初始化列表:
class Sample
{
public:
int Sam_x;
int Sam_y;
Sample() */* Object and variables are created - i.e.:declaration of variables */*
{ // Constructor body starts
Sam_x = 1; */* Defining a value to the variable */*
Sam_y = 2;
} // Constructor body ends
};
在上面的程序中,當執行類的構造函數時,會創建Sam_x和Sam_y 。 然后在構造函數體中,定義那些成員數據變量。
用例:
在 C 中,必須在創建期間定義變量。 在 C++ 中以同樣的方式,我們必須在對象創建過程中使用初始化列表來初始化 Const 和 Reference 變量。 如果我們在對象創建后進行初始化(在構造函數體內部),我們將得到編譯時錯誤。
沒有默認構造函數的 Sample1(基)類的成員對象
class Sample1 { int i; public: Sample1 (int temp) { i = temp; } }; // Class Sample2 contains object of Sample1 class Sample2 { Sample1 a; public: Sample2 (int x): a(x) /* Initializer list must be used */ { } };
在為派生類創建對象時,它將在內部調用派生類構造函數並調用基類構造函數(默認)。 如果基類沒有默認構造函數,用戶將收到編譯時錯誤。 為了避免,我們必須有
1. Default constructor of Sample1 class
2. Initialization list in Sample2 class which will call the parametric constructor of Sample1 class (as per above program)
類構造函數的參數名和類的數據成員相同:
class Sample3 { int i; /* Member variable name : i */ public: Sample3 (int i) /* Local variable name : i */ { i = i; print(i); /* Local variable: Prints the correct value which we passed in constructor */ } int getI() const { print(i); /*global variable: Garbage value is assigned to i. the expected value should be which we passed in constructor*/ return i; } };
眾所周知,如果兩個變量具有相同的名稱,則局部變量的優先級高於全局變量。 在這種情況下,程序會考慮“i”值{左右兩側變量。 即: i = i} 作為 Sample3() 構造函數中的局部變量,並且類成員變量 (i) 被覆蓋。 為了避免,我們必須使用
1. Initialization list
2. this operator.
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.