先來看兩段代碼執行效率是一樣?
//oa的一系列操作...
OptimizationA GetOpt()
{
OptimizationA oa;
//oa的一系列操作...
return oa;
}
void GetOpt(OptimizationA &_result)
{
// result的一系列操作...
return;
}
思考:效率是一樣的?如果是不一樣的,那麼又是如何不一樣的?那我們如何做效率更好呢?
程序語義的轉化
我們自己寫的代碼,自己看一回事,但是在編譯器的角度來看又是一番風景。所以這次我們換個角度來看待問題,分別從初始化操作、優化、成員列表初始化三個方面探究下編譯器會怎麼翻譯我們的代碼。
1.初始化操作
A.顯式初始化操作
OptimizationA oe;
OptimizationA of(oe);
OptimizationA og = oe;
OptimizationA oh = OptimizationA(oe);
// 編譯器的角度看,分成兩步走,
// 第一步:定義變量(不會調用初始化操作),第二步:調用拷貝構造
// 1.OptimizationA of (注意此時不會調用OptimizationA的默認構造函數)
// 2.of.OptimizationA::OptimizationA(oe) (調用拷貝構造函數)
// 3.og.OptimizationA::OptimizationA(oe) (調用拷貝構造函數)
// 4.oh.OptimizationA::OptimizationA(oe) (調用拷貝構造函數)
B.參數初始化
void Parameter(OptimizationA oa)
{
}
{
OptimizationA tempoa;
Parameter(tempoa);
}
// 編譯器生成的代碼
OptimizationA _tempObj<font>;
// tempObj調用copy構造
tempObj.OptimizationA::OptimizationA(tempoa);
Parameter(tempObj);
// tempObj調用析構函數,銷燬對象
tempObj.OptimizationA::~OptimizationA();
C.返回值初始化
OptimizationA GetOpt()
{
OptimizationA oa;
return oa;
}
// 此爲編譯器的生成的函數,分爲兩步操作
// 第一步:將上面的函數重寫爲下面的帶引用參數的形式
void GetOpt(OptimizationA &_result)
{
OptimizationA oa;
//oa的一系列操作。。。。。。
// 第二步:在return返回之前,調用result的copy 構造函數
result::OptimizationA::OptimizationA(oa);
return;
}
// 下面是編譯器生成的調用代碼
// 1.形式轉換成這樣
OptimizationA result;
GetOpt(result);
// 2.如果用戶調用了類成員函數
GetOpt().GetHello();
// 編譯器則轉換成這樣
(GetOpt(result), result).GetHello();
// 3.如果是用戶定義了函數指針
OptimizationA (*pf)();
pf = GetOpt; // 沒有參數
// 編譯器則轉換成這樣
void (*pf)(OptimizationA &);
(pf(result), result).GetHello();
2.優化
A.用戶層面優化
// 程序員的未優化
OptimizationA GetOpt(const T &y, const T &x)
{
OptimizationA oa(x, y);
// oa其他操作
return oa;
}
// 在linux上測試需要關閉優化選項
// 先是生成了一個臨時對象tempobj,然後調用tempobj的拷貝構造函數,將oa的數據拷貝到
// tempobj中,然後在調用oa的析構函數。
// 這個過程中消耗了一個tempobj的拷貝構造和析構函數
// 程序員優化,這樣做就少了一個臨時對象的生成和銷燬
OptimizationA GetOpt(const T &x, const T &y)
{
return OptimizationA(x, y);
}
未優化代碼 | 優化代碼 |
---|---|
Linux上關閉優化選項結果: compiler:1 level:2 call ctor compiler:2 level:3 call copy ctor compiler:1 level:2 call dtor compiler:3 level:4 call copy ctor compiler:2 level:3 call dtor compiler:3 level:4 call dtor Linux不關閉優化選項: compiler:1 level:2 call ctor compiler:1 level:2 call dtor windows上: compiler:1 level:2 call ctor compiler:2 level:3 call copy ctor compiler:1 level:2 call dtor compiler:2 level:3 call dtor |
Linux: compiler:1 level:2 call ctor compiler:1 level:2 call dtor 在windows上: compiler:1 level:2 call ctor compiler:1 level:2 call dtor |
B.編譯器優化
// 程序員寫的代碼
OptimizationA GetOpt()
{
OptimizationA oa;
return oa;
}
// 編譯器生成的代碼:(named return value (NRV))
// 分爲兩步操作
// 第一步:將上面的函數重寫爲下面的帶引用參數的形式
void GetOpt(OptimizationA &_result)
{
OptimizationA oa;
//oa的一系列操作...
// 第二步:在return返回之前,調用__result的copy 構造函數
__result::OptimizationA::OptimizationA(oa);
return;
}
3.成員列表初始化
先來看段代碼:
class InitialzationB
{
public:
// InitialzationB()
// {}
InitialzationB(int value): m_IA(value), m_a(value), m_b(value)
/*
放在初始化列中……
1.如果是在成員列表初始化,站在編譯器的角度看
m_IA.InitialzationA::InitialzationA(value)
*/
{
/*
放在構造函數中…..
m_IA = value;
2.如果是在函數內部初始化,站在編譯器的角度看
A.先是生成一個臨時對象
InitialzationA oc;
oc.InitialzationA::InitialzationA(value);
B.在m_IA的copy ctor
m_IA.InitialzationA::InitialzationA(oc);
C.臨時對象再去銷燬
oc.InitialzationA::~InitialzationA();
所以成員變量初始化會提高效率,但只針對類類型變量,對基本類型無影響。
在初始化列表中,不要用類成員變量去初始化另外一個成員變量
*/
}
private:
InitialzationA m_IA; // 自定義class
int m_a;
int m_b;
};
A.成員列表初始化含義
InitialzationB(int value): m_IA(value), m_a(value), m_b(value) 這就是初始化列表的調用方法
B.爲什麼需要初始化列表,以及初始化列表調用時機
簡單來說爲了初始化對象時的效率。看上面的代碼第7行放在初始化列中,從編譯器的角度看就是直接調用了InitialzationA的構造函數。但是你如果放在16行,那麼在編譯器的角度看就是先生成了一個InitialzationA臨時對象,在調用m_IA的copy構造函數,然後臨時對象的消亡調用析構函數。所以大費周章的構造對象造成效率的下降。
調用時機:編譯器會在構造函數之前會插入一段額外的代碼,這就是initialization list。然後在執行用戶寫的代碼。
C.注意事項
A.有四種情況必須放到初始化列表中
1. 成員變量是個引用 |
2. 成員變量是const類型 |
3. 成員變量是帶參數的構造函數類類型 |
4. 基類有帶參數的構造函數 |
B.初始化列表的初始化順序
初始化順序是按照在類中的聲明順序的來決定。所以在類的初始化列表中還是嚴格按照類中聲明的順序來複制。
比如:
class InitialzationB
{
public:
// InitialzationB()
// {}
// InitialzationB(int value): m_IA(value) , m_b(value), m_a(m_b)
// 正宗做法
InitialzationB(int value): m_IA(value), m_a(value), m_b(value)
{
}
private:
InitialzationA m_IA;
int m_b;
int m_a;
};
C.在初始化列表中調用成員函數
不要在初始化列表中調用成員函數,因爲你不知道這個函數以後會多麼的依賴當前的對象。
總結:
現在我們開始回答上面提出的問題,第一個方法至少消耗了一個ctor,copy ctor, dtor,同時還要考慮編譯器的實現,中間可能還會temp object的生成,又會增加一個copy ctor,dtor。反過來再看方法二隻消耗了ctor,dtor。效率肯定比方法一高。
知道了編譯器做了什麼,和怎麼做的。這將有助於對C++語言背後的實現細節更瞭若指掌,才能寫出高效的程序。同時也看出來c++爲了追求效率,背後做了很多我們不知道的事情。最後假如我們是編譯器,我們會如何生成代碼的?這是值得我們思考的地方。