C++幕後故事(三)--程序語義轉化

先來看兩段代碼執行效率是一樣?

//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++爲了追求效率,背後做了很多我們不知道的事情。最後假如我們是編譯器,我們會如何生成代碼的?這是值得我們思考的地方。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章