深度探索C++物件模型——物件(7)——成員初始化列表
1.何時必須使用成員初始化列表
(1)如果初始化的成員是個引用
這裡拋個問題留作後面補充總結的連結:在類中使用引用成員變數的作用?
#include <iostream> using namespace std; class A { public: int a; int b; int& c; public: A(int& num) :c(num) { a = 0; b = 0; c = 180; } }; int main() { int num=1; A a(num); cout << num << endl; }
執行結果:
(2)如果是個const型別的成員
#include <iostream>
using namespace std;
class A
{
public:
int a;
int b;
const int c;
public:
A(int& num)
//:c(num)
{
c = 0;
a = 0;
b = 0;
}
};
int main()
{
}
編譯結果:
(3)如果這個類是繼承一個基類,並且基類中有建構函式,這個建構函式裡邊還有引數,即沒有預設建構函式
#include <iostream> using namespace std; class Base { public: int aa; int bb; Base(int a, int b) { } }; class A:public Base { public: int a; int b; public: A(int& num) //:Base(num,num) { a = 0; b = 0; } }; int main() { }
編譯結果:
(4)如果你的成員變數型別是某個類型別,而這個類的建構函式帶引數時,也即沒有預設建構函式;
#include <iostream>
using namespace std;
class B
{
public:
int aa;
B(int a)
{
}
};
class A
{
public:
int a;
int b;
B bb;
public:
A(int num)
//:bb(num)
{
a = 0;
b = 0;
}
};
int main()
{
}
編譯結果:
2.使用初始化列表的優勢
除過必須使用初始化列表的場合,使用初始化列表還有其他什麼目的?
使用初始化列表的終極目的:提高程式執行效率
<1>類型別成員函式的初始化寫在函式體中
//類型別成員函式的初始化寫在函式體中
#include <iostream>
using namespace std;
class A
{
public:
int val;
A(int value = 0) :val(value) //型別轉換建構函式
{
printf("this = %p", this);
cout << "A(int)建構函式被呼叫" << endl;
cout << "value = " << value << endl;
}
A(const A& a)
{
printf("this = %p", this);
cout << "A拷貝建構函式被呼叫" << endl;
}
A& operator=(const A &a)
{
printf("this = %p", this);
cout << "A拷貝賦值運算子被呼叫" << endl;
return *this;
}
~A()
{
printf("this = %p", this);
cout << "A解構函式被呼叫" << endl;
}
};
class B
{
public:
A a;
int test;
B(int v) //執行完此句已經完成物件a的建立,此時呼叫了預設建構函式,a.val=0;
{
cout << "B的建構函式被呼叫" << endl;
a = v; //此句相當於重新進行賦值而不是初始化
test = 500;
}
~B()
{
cout << "B的解構函式被呼叫" << endl;
}
};
int main()
{
B b(1000);
}
執行結果:
解釋:
(1)在呼叫B的建構函式的第一行就完成了物件的建立,即已經完成對A的建構函式的呼叫,此時呼叫的是預設建構函式,a.val=0
此時編譯器視角下:
A a;
a.A::A();
(2)在執行a=v;時,先呼叫A的型別轉換建構函式建立臨時物件,然後呼叫a物件的拷貝賦值運算子函式,再析構掉臨時物件
(3)最後執行完main函式先析構物件b,再析構它的成員物件a
編譯器視角下的看a=v;:
A temp; //建立臨時物件
temp.A::A(1000); //呼叫臨時物件的建構函式
a.A::operator=(temp); //呼叫物件a的賦值運算子函式
temp.A::~A(); //呼叫臨時物件的解構函式
<2>類型別成員函式的初始化寫在成員初始化列表中
//類型別成員函式的初始化寫在成員初始化列表中
#include <iostream>
using namespace std;
class A
{
public:
int a;
A(int value = 0) :a(value) //型別轉換建構函式
{
printf("this = %p", this);
cout << "A(int)建構函式被呼叫" << endl;
cout << "value = " << value << endl;
}
A(const A& a)
{
printf("this = %p", this);
cout << "A拷貝建構函式被呼叫" << endl;
}
A& operator=(const A &a)
{
printf("this = %p", this);
cout << "A拷貝賦值運算子被呼叫" << endl;
return *this;
}
~A()
{
printf("this = %p", this);
cout << "A解構函式被呼叫" << endl;
}
};
class B
{
public:
A a;
int test;
B(int v)
:a(v) //執行完此句已經完成物件a的建立,此時呼叫了有參建構函式,a.v=1000;
{
cout << "B的建構函式被呼叫" << endl;
test = 500;
}
~B()
{
cout << "B的解構函式被呼叫" << endl;
}
};
int main()
{
B b(1000);
}
執行結果:
解釋:
直接在成員初始化列表中一步完成了對物件a的初始化,呼叫了A的有參建構函式
此時編譯器視角下:
A a;
a.A::A(1000);
此種初始化省略了在函式體內臨時物件建立時呼叫三個函式的開銷
其實成員初始化列表中的程式碼還是函式體內的一部分,只是這是編譯器幫我們挪到函式體內,新增的程式碼只有上述兩句,而直接在函式體內賦值,新增的程式碼就多了(參見上述<1>中編譯器視角)
結論:類型別物件初始化放在成員初始化列表中比放在函式體中效率更高,而對於內建型別效率提升不明顯,但是為了統一,好的做法是將它們都寫到成員初始化列表中
3.初始化列表探究
(1)初始化列表中的程式碼可以看作是被編譯器安插到建構函式體中的,只是這些程式碼有些特殊;
(2)這些程式碼是在任何使用者自己的建構函式體程式碼之前被執行的。所以要區分開建構函式中的使用者程式碼和編譯器插入的初始化所屬的程式碼。
(3)這些列表中變數的初始化順序是定義順序,而不是在初始化列表中的順序。
程式碼演示:
#include <iostream>
using namespace std;
class B
{
public:
int test1;
int test2;
B(int v)
:test2(v),test1(test2)
{
cout << "test1 = " << test1 << endl;
cout << "test2 = " << test2 << endl;
}
};
int main()
{
B b(100);
}
執行結果:
可以發現test1是一個異常的值
Visual Studio下除錯反彙編:加斷點,F5,Alt+8
可以發現它先ecx+4處即test2的值賦給this開始處的位置也就是test1,最後將v賦給eax+4處即test2
所以不建議在成員初始化列表中用一個成員初始化另一個成員,除非你清楚它們的執行順序,否則在函式體中寫這樣的用一個成員初始化另一個成員的語句(如test1(test2))