在必須返回乙個物件時,不要去嘗試返回乙個引用

2021-09-06 17:26:31 字數 4010 閱讀 5953

一旦程式設計師把注意力都轉向了物件傳值方式隱含的效率問題(參見第 20 條)時,許多人都變成了極端的「改革運動者」,他們對傳值方法採取斬草除根的態度,在他們不屈不撓追求傳遞引用方式的純粹性的同時,他們也犯下了致命的錯誤:有時候傳遞的引用所指向的物件並不存在。這決不是一件好事情。

請看下面的示例,其中的 rational 類用來表示有理數,其中還包括乙個函式來計算兩個有理數的乘積:

class rational ;

這一版本的 operator* 通過傳值方式返回乙個物件,如果你不去考慮這一物件在構造和析構過程中的開銷,那麼你就是在逃避你的專業職責。如果你並不是非得要為這樣的物件付出代價,那麼你大可不必那樣做。現在問題就是:你必須付出這一代價嗎?

好的,如果此時你可以返回乙個引用作為替代品,那麼就不需要了。但是請記住,乙個引用僅僅是乙個名字,它是乙個已存在的物件的別名。當你看到乙個引用的宣告時,你應該立刻問一下你自己:它的另乙個名字是什麼,因為乙個引用作指向的內容必定有它自己的名字。於是對於上面的 operator* 而言,如果它返回乙個引用,那麼它所引用的必須是乙個已存在的 rational 物件,這個物件中包含著需要進行乘法操作的兩個物件的乘積。

如果你期望在呼叫 operator* 之前這一物件必須存在,那麼你就太不理智了。也就是說,如果你這樣做了:

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

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

rational c = a * b;   // c 的值應該為 3/10

期待存在乙個值為 3/10 的有理數的做法看上去顯得很不理智。其實並不是這樣的,如果 operator* 返回乙個指向這類數值的引用,那麼它必須要自己建立這個數字。

乙個函式只能以兩種方式建立新的物件:在棧上或在堆上。定義乙個區域性變數就是在棧上建立乙個新物件。應用這一策略時,你可能會以這種方式編寫 operator* :

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

// 警告!錯誤的**

你大可以拒絕這樣的實現方法,因為你的目標是防止對建構函式的呼叫,但是此時 result 會像其它物件一樣被初始化。乙個更嚴重的問題是:這個函式會返回乙個指向 result 的引用,但是 result 是乙個區域性物件,而區域性物件在函式退出時就會被銷毀。那麼,這一版本的 operator* ,並不會返回乙個指向 rational 的引用,它返回的引用指向乙個「前期 rational 」,它曾經是 rational 的物件,乙個「空寂的、散發著霉氣的、開始腐爛的、曾是乙個 rational 的屍體」,但它現在與 rational 已經毫無關係,因為它已經被銷毀了。對於所有的呼叫者而言,只要稍稍觸及這一函式的返回值,都會遭遇到無盡的無法預知的行為。事實上,任何返回區域性物件引用的函式都是災難性的。(任何返回指向區域性物件的指標的函式也是如此。)

現在,讓我們考慮下面做法的可行性:在堆上建立乙個物件,然後返回乙個指向它的引用。由於儲存於堆上的物件由 new 來建立,因此你可能會這樣編寫基於堆的 operator* :

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

// 警告!這裡有更多的錯誤!

好的,此時仍然需要付出呼叫建構函式的代 價,這是因為通過 new 分配的記憶體要通過呼叫乙個合適的建構函式來初始化,但是現在你面臨這另乙個問題:誰來確保與 new 相對應的 delete 的執行呢?

即使呼叫者十分認真負責並且抱有良好的初衷,他們也無法保證:下面這樣合理的使用場景下不會出現記憶體洩漏:

rational w, x, y, z; 

w = x * y * z; // 等價於 operator*(operator*(x, y), z)

這裡,在乙個語句中存在著兩次對 operator* 的 呼叫,於是存在兩次 new 操作有待於使用 delete 來清除。但是又沒有任何理由要求 operator* 的客戶端程式員來進行這一操作,這是因為對 operator* 的呼叫返回了乙個引用,沒有理由要求客戶端程式員去取得隱藏在這一引用背後的指標。這勢必會造成資源洩漏。

但是,也許你注意到了,棧方案與堆方案都面臨著同乙個問題:它們都需要為 operator* 的每乙個返回值呼叫一次建構函式。也許你能夠回憶起我們最初的目的就是避免像此類構造函式呼叫。也許你認為你知道某種方法來將此類構造函式呼叫的次數降低到僅有一次。也許你想到了下面的實現方法:讓 operator* 返回乙個指向乙個靜態的 rational 物件的引用,這一靜態物件在函式的內部:

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

// 警告!會出現更多更多的錯誤!

與其它引入靜態物件的設計方法一樣,這種方法很顯著的提高了執行緒的安全性,但是這卻帶來了更明顯的缺陷。下面的客戶端**是無懈可擊的,但是上文中的設計會使其暴露出問題:

bool operator==(const rational& lhs, const rational& rhs);

// 為有理數作比較的 operator==

rational a, b, c, d; 

...if ((a * b) == (c * d))  else   

猜猜會髮深什麼?無論 a 、 b 、 c 或 d 取什麼值,表示式 ((a*b) == (c*d)) 的值永遠為真。

我們為上面函式中的判斷語句更換乙個形式,這個問題就更加淺顯了:

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

請注意,在呼叫 operator== 時,已經存在了兩次活動的對 operator* 的呼叫,每次呼叫時都回返回乙個指向 operator* 內部的靜態 rational 物件的引用。於是編譯器將要求 operator== 去將 operator* 內部的靜態 rational 物件與自身相比較。如果結果並不總是相等的,這才是讓人奇怪的事情。

上面的內容似乎已經足夠讓你確信:為類似於 operator* 這樣的函式返回乙個引用確實是在浪費時間,但是有些時候你會想:「好吧,乙個靜態值不夠,那麼用乙個靜態陣列總可以了吧 … 」

我無法用例項來捍衛我的觀點,但是我可以用非常簡明的推理證明這樣做會讓你多羞愧:首先,你必須確定乙個 n 值,也就是陣列的大小。如果 n 太小了,函式返回值的儲存空間可能會用完,這種情況與剛才否定的單一靜態物件的方案一樣糟糕。但是如果 n 的值太大,那麼你的程式將面臨效能問題,這是因為陣列中的每個物件都應在函式在第一次呼叫時被構造。這會使你付出 n 次建構函式和 n 次析構函式的呼叫,即使我們討論的函式只被呼叫一次。如果將「優化」稱為改善軟體效能的乙個步驟,那麼我們可以把這一做法稱為「劣化」。最後,請考慮一下:你如何將需要的值放入陣列中的物件裡,在放置的過程中你又付出了多大代價呢?在兩個物件之間傳值的最直接的方法就是賦值,但是賦值操作又會帶來多大開銷呢?對於許多態別而言,賦值的開銷類似於呼叫一次析構函式(以銷毀舊數值)加上一次建構函式(以複製新數值)。但是要知道,你的原始目標本來是避免構造和析構過程所帶來的開銷!請面對它:這樣做一定不會得到好結果。(別妄想,用 vector 來代替陣列也不會改善多少。)

編寫必須返回乙個新物件的函式,正確的方法就是:讓這個函式返回乙個新物件。對於 rational 的 operator* 來說,這就意味著下面的**是基本符合要求的:

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

顯然地,這樣做可能會招致對 operator* 的返回值的構造和析構過程的開銷,但是從長遠角度講,付出這小小的代價可以獲得更大的收益。而且,這一恐怖的清單可能永遠不需要你來付賬。就像其它程式語言一樣,c++允許編譯器的具體實現版本通過優化**來提公升效能,同時又不改變其固有的行為,在一些情況下,對 operator* 返回值的構造和析構過程可以被安全的排除。當編譯器利用了這一事實時(編譯器通常都會這樣做),你的程式就可以繼續按預期的行為執行,僅僅是更快了一些。

歸根結底,當選擇是使用引用返回,還是直接返回乙個物件時,你的工作就是:做出正確的抉擇,使程式擁有正確的行為。然後把優化工作留給編譯器製造商,他們會使你的抉擇變得盡可能的經濟實用。

牢記在心

c 返回乙個物件

當函式返回類物件的時候,c 編譯器會進行返回值優化 返回值優化 return value optimization,縮寫為rvo 是c 的一項編譯優化技術。即刪除保持函式返回值的臨時物件。這可能會省略兩次複製建構函式,即使複製建構函式有 1 2 典型地,當乙個函式返回乙個物件例項,乙個臨時物件將被建...

在乙個非套接字上嘗試了乙個操作

今天給客戶做維護的時候碰到乙個怪問題,客戶機不能通地dhcp得到ip位址,啟用禁用網絡卡後本地連線直接顯示受限制,後來我就手動設定了乙個固定ip,執行cmd ping測試了乙個,可以ping 通公網ip 位址,但是ping網域名稱直接顯示網域名稱無效,請檢查。對於這種問題我的第一反應就是dns有問題...

vue系列 data為什麼必須是乙個返回物件的方法

因為物件是引用資料型別,如果你寫成物件,這個元件在多處被引用,只要修改一處的值,那麼另一處的引用的值也會變化,這樣就亂套了。而使用返回物件的函式,由於每次返回的都是乙個新物件 object的例項 引用位址不同,則不會出現這個問題。物件方式 var data var vm1 var vm2 vm1.d...