C 箴言 必須返回物件時別返回引用

2021-06-19 19:04:42 字數 3858 閱讀 7846

一旦程式設計師抓住物件傳值的效率隱憂,很多人就會成為狂熱的聖戰分子,誓要**傳值的罪惡,無論它隱藏多深。他們不屈不撓地追求傳引用的純度,但他們全都犯了乙個致命的錯誤:他們開始傳遞並不存在的物件的引用。這可不是什麼好事。

考慮乙個代表有理數的類,包含乙個將兩個有理數相乘的函式:

class rational ;

operator* 的這個版本以傳值方式返回它的結果,而且如果你沒有擔心那個物件的構造和析構的代價,你就是在推卸你的專業職責。如果你不是迫不得已,你不應該為這樣的乙個物件付出成本。所以問題就在這裡:你是迫不得已嗎?

哦,如果你能用返回乙個引用來作為代替,你就不是迫不得已。但是,請記住乙個引用僅僅是乙個名字,乙個實際存在的物件的名字。無論何時只要你看到乙個引用的宣告,你應該立刻問自己它是什麼東西的另乙個名字,因為它必定是某物的另乙個名字。在這個 operator* 的情況下,如果函式返回乙個引用,它必須返回某個已存在的而且其中包含兩個物件相乘的產物的 rational 物件的引用。

rational a(1, 2); // a = 1/2

rational b(3, 5); // b = 3/5

rational c = a * b; // c should be 3/10

似乎沒有理由期望那裡碰巧已經存在乙個值為十分之三的有理數。不是這樣的,如果 operator* 返回這樣乙個數的引用,它必須自己建立那個數字物件。

乙個函式建立乙個新物件僅有兩種方法:在棧上或者在堆上。棧上的生成物通過定義乙個區域性變數而生成。使用這個策略,你可以用這種方法試寫 operator*:

const rational& operator*(const rational& lhs, // warning! bad code!

const rational& rhs)

你可以立即否決這種方法,因為你的目標是避免呼叫建構函式,而 result 正像任何其它物件一樣必須被構造。乙個更嚴重的問題是這個函式返回乙個引向 result 的引用,但是 result 是乙個區域性物件,而區域性物件在函式退出時被銷毀。那麼,這個 operator* 的版本不會返回引向乙個 rational 的引用——它返回引向乙個前 rational;乙個曾經的 rational;乙個空洞的、惡臭的、腐敗的,從前是乙個 rational 但永不再是的屍體的引用,因為它已經被銷毀了。任何呼叫者甚至於沒有來得及匆匆看一眼這個函式的返回值就立刻進入了未定義行為的領地。這是事實,任何返回乙個引向區域性變數的引用的函式都是錯誤的。(對於任何返回乙個指向區域性變數的指標的函式同樣成立。)

那麼,讓我們考慮一下在堆上構造乙個物件並返回引向它的引用的可能性。基於堆的物件通過使用 new 而開始存在,所以你可以像這樣寫乙個基於堆的 operator*:

const rational& operator*(const rational& lhs, // warning! more bad

const rational& rhs) // code!

哦,你還是必須要付出乙個構造函式呼叫的成本,因為通過 new 分配的

記憶體要通過呼叫乙個適當的建構函式進行初始化,但是現在你有另乙個問題:誰是刪除你用 new 做出來的物件的合適人選?

即使呼叫者盡職盡責且一心向善,它們也不太可能是用這樣的方案來合理地預防洩漏:

rational w, x, y, z;

w = x * y * z; // same as operator*(operator*(x, y), z)

這裡,在同乙個語句中有兩個 operator* 的呼叫,因此 new 被使用了兩次,這兩次都需要使用 delete 來銷毀。但是 operator* 的客戶沒有合理的辦法進行那些呼叫,因為他們沒有合理的辦法取得隱藏在通過呼叫 operator* 返回的引用後面的指標。這是乙個早已注定的資源洩漏。

但是也許你注意到無論是在棧上的還是在堆上的方法,為了從 operator* 返回的每乙個 result,我們都不得不容忍一次建構函式的呼叫。也許你想起我們最初的目標是避免這樣的構造函式呼叫。也許你認為你知道一種方法能避免除一次以外幾乎全部的構造函式呼叫。也許下面這個實現是你做過的,乙個基於 operator* 返回乙個引向 static rational 物件的引用的實現,而這個 static rational 物件定義在函式內部:

const rational& operator*(const rational& lhs, // warning! yet more

const rational& rhs) // bad code!

就像所有使用了 static 物件的設計一樣,這個也會立即引起我們的執行緒安全(thread-safety)的混亂,但那是它的比較明顯的缺點。為了看到它的更深層的缺陷,考慮這個完全合理的客戶**:

猜猜會怎麼樣?不管 a,b,

c,d 的值是什麼,表示式 ((a*b) == (c*d)) 總是等於 true!

如果**重寫為功能完全等價的另一種形式,這一啟示就很容易被理解了:

if (operator==(operator*(a, b), operator*(c, d)))

注意,當 operator== 被呼叫時,將同時存在兩個起作用的對 operator* 的呼叫,每乙個都將返回引向 operator* 內部的 static rational 物件的引用。因此,operator== 將被要求比較 operator* 內部的 static rational 物件的值和 operator* 內部的 static rational 物件的值。如果它們不是永遠相等,那才真的會令人大驚失色了。

這些應該足夠讓你信服試圖從類似 operator* 這樣的函式中返回乙個引用純粹是浪費時間,但是你們中的某些人可能會這樣想「好吧,就算乙個 static 不夠用,也許乙個 static 的陣列是乙個竅門……」

我無法拿出示例**來肯定這個設計,但我可以概要說明為什麼這個想法應該讓你羞愧得無地自容。首先,你必須選擇乙個 n 作為陣列的大小。如果 n 太小,你可能會用完儲存函式返回值的空間,與剛剛名譽掃地的 single-static 設計相比,在任何乙個方面你都不會得到更多的東西。但是如果 n 太大,就會降低你的程式的效能,因為在函式第一次被呼叫的時候陣列中的每乙個物件都會被構造。即使這個我們正在討論的函式僅被呼叫了一次,也將讓你付出 n 個建構函式和 n 個析構函式的成本。如果「優化」是提高軟體效率的過程,對於這種東西也只能是「悲觀主義」的。最後,考慮你怎樣將你所需要的值放入陣列的物件中,以及你做這些需要付出什麼。在兩個物件間移動值的最直接方法就是通過賦值,但是一次賦值將要付出什麼?對於很多態別,這就大約相當於呼叫一次析構函式(銷毀原來的值)加上呼叫一次建構函式(把新值拷貝過去)。但是你的目標是避免付出構造和析構成本!面對的結果就是:這個方法絕對不會成功。(不,用乙個 vector 代替陣列也不會讓事情有多少改進。)

寫乙個必須返回乙個新物件的函式的正確方法就是讓那個函式返回乙個新物件。對於 rational 的 operator*,這就意味著下面這些**或在本質上與其相當的某些東西:

inline const rational operator*(const rational& lhs, const rational& rhs)

當然,你可能付出了構造和析構 operator* 的返回值的成本,但是從長遠看,這只是為正確行為付出的很小的代價。除此之外,這種令你感到恐怖的賬單也許永遠都不會到達。就像所有的程式語言,c++ 允許編譯器的實現者在不改變生成**的可觀察行為的條件下使用優化來提公升它的效能,在某些條件下會產生如下結果:operator* 的返回值的構造和析構能被安全地消除。如果編譯器利用了這一點(編譯器經常這樣做),你的程式還是在它假定的方法上繼續執行,只是比你期待的要快。 全部的焦點在這裡:如果需要在返回乙個引用和返回乙個物件之間做出決定,你的工作就是讓那個選擇能提供正確的行為。讓你的編譯器廠商去絞盡腦汁使那個選擇盡可能地廉價。

###adv###  things to remember

·絕不要返回乙個區域性棧物件的指標或引用,絕不要返回乙個被分配的堆物件的引用,如果存在需要乙個以上這樣的物件的可能性時,絕不要返回乙個區域性 static 物件的指標或引用。

條款21 必須返回物件時,別妄想返回其引用

條款21 必須返回物件時,別妄想返回其引用 includeusing namespace std class rational private int n,d friend const rational operator const rational lhs,const rational rhs c...

C 返回物件和返回引用

最大的區別在於,返回物件的話會在記憶體中根據返回的型別開闢一塊區域,用返回的值對該記憶體進行初始化,如果是返回的物件,利用拷貝構造來初始化這個區域,但是這塊區域並沒有名字,就是說之後使用者沒辦法訪問到這個區域,也成為無名變數,它只能在接下來的 中進行一次性的用途,要不作為引數傳遞,或者將值列印,再之...

C 返回物件和返回引用

我們發現,在c 中,有些成員函式返回的是物件,而有些函式返回的又是引用。返回物件和返回引用的最主要的區別就是函式原型和函式頭。car run const car 返回物件 car run const car 返回引用 返回物件會涉及到生成返回物件的副本。因此,返回物件的時間成本包括了呼叫複製建構函式...