程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> C++對象模型之編譯器如何處理函數返回一個對象

C++對象模型之編譯器如何處理函數返回一個對象

編輯:關於C++
1、與經驗不符的輸出我們知道,當發生以下三種情況之一時,對象對應的類的復制構造函數將會被調用:1)對一個對象做顯示的初始化操作時
2)當對象被當作參數傳遞給某個函數時3)當函數返回一個類的對象時
所以,當我們設計一個函數(普通或成員函數)時,經驗告訴我們,出於效率的考慮,應該盡可能返回一個對象的指針或引用,而不是直接返回一個對象。因為在直接返回一個對象可能會引起對象的復制構造過程,這意味著會發生一定量的內存復制和對象創建的動作,從而降低了程序的效率。這個設計的想法是正確的,但是實際上,當函數返回一個對象時,上述的復制構造過程一定會發生嗎?
例如,對於如下的代碼:
class X
{
    public:
        X()
        {
            mData = 100;
            cout << "X::X()" << endl;
        }
        X(const X& rhs)
        {
            mData = rhs.mData;
            cout << "X::X(const X&rhs)" << endl;
        }
        void setData(int n)
        {
            mData = n;
        }
        void print()
        {
            cout << "X::mData == " << mData << endl;
        }
    private:
        int mData;
};

X func()
{
    X xx;
    xx.setData(101);
    return xx;
}

int main()
{
    X xx = func();
    return 0;
}

看了上面的代碼片斷,你認為這個程序應該輸出什麼呢?若按照書本上的說法進行分析,在func函數中,定義了類X的一個局部對象xx,所以類X的構造函數會被調用;在func函數返回的返回值是一個對象,那麼該函數將返回對象xx的一個副本,所以類X的復制構造函數會被調用;在main函數中,同樣定義了類X的一個局部對象xx,而該對象是通過函數func返回的對象作為初值進行構造的,所以類X的復制因構造該副本而被調用。也就是說,根據這個分析,輸入結果應該是:X::X()
X::X(const X&rhs)
X::X(const X&rhs)

原本我也認為輸出的應該是上面的三行,但是實際的運行結果如下圖所示,它完全出乎我的意料:\\

從運行結果來看,只輸出了一行“X::X()”,也就是說它只構造出了一個類X對象,而且沒有發生任何的復制構造過程。我們的分析依據都是正確的,程序代碼非常簡單,分析的流程也正確,但是程序的行為究竟為什麼與我們的分析不符呢?其實一切都是編譯器出於優化的考慮,暗中修改了我們程序的代碼。下面先介紹編譯器處理返回對象的一種方法,再介紹編譯器的優化究竟對我們的代碼動了什麼手腳。
2、編譯器處理返回對象的一種方法當函數調用完畢後,會銷毀其局部對象,若函數返回一個局部對象,編譯器如何把這個局部對象復制出來呢?方法如下:1)首先為函數加上一個額外的參數,類型是類對象的引用。這個參數將用來存放被“復制建構”而得的返回值2)在return指令之前安插一個復制構造調用操作,以便將欲傳回的對象的內容當做上述新參數的初值。
該方法的兩個操作會重新改寫函數,使它不用返回任何值。根據這個方法,func函數的操作可能轉換為如下的偽代碼:
// C++偽代碼,模擬構造函數和復制構造函數的調用
void func(X &__result)
{
    X xx;
    xx.X::X(); // 調用類X的默認構造函數
   
    xx.setData(101);
   
    __result.X::X(xx); // 調用類X的復制構造函數
   
    return;
}

現在編譯器必須轉換每一個func()調用操作,以符合其新的定義,即X xx = func();
會被轉換成為下列語句:X xx; // 並不調用類X的構造函數func(xx);
所以,main函數會被轉換成如下偽代碼
// C++偽代碼,模擬構造函數和復制構造函數的調用 
int main()
{
    X xx; // 並不調用類X的構造函數
    func(xx);
    return 0;
}

根據上述編譯器的操作,可以得到如下的輸出:X::X()
X::X(const X&rhs)第一行為func函數中局部對象xx的構造,第二行為為了達到返回的目的而發生的復制構造操作。這個結果雖然與第1節中的分析有所不同,從編譯使用這個方法卻能減少一次復制構造函數的調用,提高了效率,畢竟對同一個對象復制兩次也沒有什麼好處。而且編譯對這個程序還會做進一步的優化,在第3節會詳細講述。
考慮函數func的另一種使用情況,就是直接使用函數func的返回值,而不將其賦給一個變量。把main函數修改成如下所示:
int main()
{
    func().print();
    return 0;
}

當遇到上述情況時,為使代碼正確運行,編譯器可能會進行如下轉換:
// C++偽代碼,模擬編譯器的相關處理操作
int main()
{
    {
        X __temp;
        func(__temp);
        __temp.print();
    }
}

在《深度探索C++對象模型》一書中的例子,對於這種情況,轉換後的代碼沒有放在一個花括號內,但是我個人認為這樣做更合理。因為根據C++的定義,func函數返回的是一個臨時對象,所以當語句func().print();
運行結束後,該臨時變量應該被銷毀。把轉換後的語句放在一對花括號中,當運行完轉換後的語句後,__temp臨時變量就會因出了作用域而被銷毀。但是編譯器是否這樣做,我就不知道了。
同理,如果程序中定義了一個函數指針變量,並指向了該函數,則編譯器還需要改寫該函數指針的定義。
3、NRV優化在第2節中已經分析了編譯器如何處理返回一個對象的函數,但是其結果與我們程序的輸出還是不一樣,這是因為編譯器對程序做了進一步的優化,方法就是以增加的類對象引用的參數(result參數)取代返回值的名字(named return value)。
使用此策略,func函數轉換成如下的偽代碼:
// C++偽代碼,模擬構造函數和復制構造函數的優化
void func(X &__result)
{
    __result.X::X(); // 調用類X的默認構造函數
   
    __result.setData(101);
   
    return;
}

main函數轉換後的偽代碼不變,如下:
int main()
{
    X xx; // 並不調用類X的構造函數
    func(xx);
    return 0;
}

通過這個優化,可以看在在main函數的調用過程中,只會調用一次構造函數,且不會調用復制構造函數。這樣的編譯優化操作,被稱為Named Return Value(NRV)優化。NRV優化如今被視為標准C++編譯器的一個義不容辭的優化操作。所以第1節中產生的出人意料的輸出,正是NRV優化的結果。
雖然NRV優化提供了重要的效率改善,但是優化是由編譯器默默完成的,而它是否真的被完成,並不十分清楚,而且一旦函數變得比較復雜,優化也難以施行。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved