C++箴言:必須返回對(duì)象時(shí)別返回引用

字號(hào):

一旦程序員抓住對(duì)象傳值的效率隱憂,很多人就會(huì)成為狂熱的圣戰(zhàn)分子,誓要根除傳值的罪惡,無論它隱藏多深。他們不屈不撓地追求傳引用的純度,但他們?nèi)挤噶艘粋€(gè)致命的錯(cuò)誤:他們開始傳遞并不存在的對(duì)象的引用。這可不是什么好事。
    考慮一個(gè)代表有理數(shù)的類,包含一個(gè)將兩個(gè)有理數(shù)相乘的函數(shù):
    class Rational {
    public:
    Rational(int numerator = 0, // see Item 24 for why this
    int denominator = 1); // ctor isn’t declared explicit
    ...
    private:
    int n, d; // numerator and denominator
    friend:
    const Rational // see Item 3 for why the
    operator*(const Rational& lhs, // return type is const
    const Rational& rhs);
    };
    operator* 的這個(gè)版本以傳值方式返回它的結(jié)果,而且如果你沒有擔(dān)心那個(gè)對(duì)象的構(gòu)造和析構(gòu)的代價(jià),你就是在推卸你的專業(yè)職責(zé)。如果你不是迫不得已,你不應(yīng)該為這樣的一個(gè)對(duì)象付出成本。所以問題就在這里:你是迫不得已嗎?
    哦,如果你能用返回一個(gè)引用來作為代替,你就不是迫不得已。但是,請(qǐng)記住一個(gè)引用僅僅是一個(gè)名字,一個(gè)實(shí)際存在的對(duì)象的名字。無論何時(shí)只要你看到一個(gè)引用的聲明,你應(yīng)該立刻問自己它是什么東西的另一個(gè)名字,因?yàn)樗囟ㄊ悄澄锏牧硪粋€(gè)名字。在這個(gè) operator* 的情況下,如果函數(shù)返回一個(gè)引用,它必須返回某個(gè)已存在的而且其中包含兩個(gè)對(duì)象相乘的產(chǎn)物的 Rational 對(duì)象的引用。
    當(dāng)然沒有什么理由期望這樣一個(gè)對(duì)象在調(diào)用 operator* 之前就存在。也就是說,如果你有
    Rational a(1, 2); // a = 1/2
    Rational b(3, 5); // b = 3/5
    Rational c = a * b; // c should be 3/10
    似乎沒有理由期望那里碰巧已經(jīng)存在一個(gè)值為十分之三的有理數(shù)。不是這樣的,如果 operator* 返回這樣一個(gè)數(shù)的引用,它必須自己創(chuàng)建那個(gè)數(shù)字對(duì)象。
    一個(gè)函數(shù)創(chuàng)建一個(gè)新對(duì)象僅有兩種方法:在棧上或者在堆上。棧上的生成物通過定義一個(gè)局部變量而生成。使用這個(gè)策略,你可以用這種方法試寫 operator*:
    const Rational& operator*(const Rational& lhs, // warning! bad code!
    const Rational& rhs)
    {
    Rational result(lhs.n * rhs.n, lhs.d * rhs.d);
    return result;
    }
    你可以立即否決這種方法,因?yàn)槟愕哪繕?biāo)是避免調(diào)用構(gòu)造函數(shù),而 result 正像任何其它對(duì)象一樣必須被構(gòu)造。一個(gè)更嚴(yán)重的問題是這個(gè)函數(shù)返回一個(gè)引向 result 的引用,但是 result 是一個(gè)局部對(duì)象,而局部對(duì)象在函數(shù)退出時(shí)被銷毀。那么,這個(gè) operator* 的版本不會(huì)返回引向一個(gè) Rational 的引用——它返回引向一個(gè)前 Rational;一個(gè)曾經(jīng)的 Rational;一個(gè)空洞的、惡臭的、腐敗的,從前是一個(gè) Rational 但永不再是的尸體的引用,因?yàn)樗呀?jīng)被銷毀了。任何調(diào)用者甚至于沒有來得及匆匆看一眼這個(gè)函數(shù)的返回值就立刻進(jìn)入了未定義行為的領(lǐng)地。這是事實(shí),任何返回一個(gè)引向局部變量的引用的函數(shù)都是錯(cuò)誤的。(對(duì)于任何返回一個(gè)指向局部變量的指針的函數(shù)同樣成立。)
    那么,讓我們考慮一下在堆上構(gòu)造一個(gè)對(duì)象并返回引向它的引用的可能性?;诙训膶?duì)象通過使用 new 而開始存在,所以你可以像這樣寫一個(gè)基于堆的 operator*:
    const Rational& operator*(const Rational& lhs, // warning! more bad
    const Rational& rhs) // code!
    {
    Rational *result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d);
    return *result;
    }
    哦,你還是必須要付出一個(gè)構(gòu)造函數(shù)調(diào)用的成本,因?yàn)橥ㄟ^ new 分配的內(nèi)存要通過調(diào)用一個(gè)適當(dāng)?shù)臉?gòu)造函數(shù)進(jìn)行初始化,但是現(xiàn)在你有另一個(gè)問題:誰是刪除你用 new 做出來的對(duì)象的合適人選?
    即使調(diào)用者盡職盡責(zé)且一心向善,它們也不太可能是用這樣的方案來合理地預(yù)防泄漏:
    Rational w, x, y, z;
    w = x * y * z; // same as operator*(operator*(x, y), z)
    這里,在同一個(gè)語句中有兩個(gè) operator* 的調(diào)用,因此 new 被使用了兩次,這兩次都需要使用 delete 來銷毀。但是 operator* 的客戶沒有合理的辦法進(jìn)行那些調(diào)用,因?yàn)樗麄儧]有合理的辦法取得隱藏在通過調(diào)用 operator* 返回的引用后面的指針。這是一個(gè)早已注定的資源泄漏。