friend: const Rational // see Item 3 for why the Operator*(const Rational& lhs, // return type is const const Rational& rhs); }; 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; } 你可以立即否決這種方法,因為你的目標是避免調用構造函數,而 result 正像任何其它對象一樣必須被構造。一個更嚴重的問題是這個函數返回一個引向 result 的引用,但是 result 是一個局部對象,而局部對象在函數退出時被銷毀。那么,這個 operator* 的版本不會返回引向一個 Rational 的引用——它返回引向一個前 Rational;一個曾經的 Rational;一個空洞的、惡臭的、腐敗的,從前是一個 Rational 但永不再是的尸體的引用,因為它已經被銷毀了。任何調用者甚至于沒有來得及匆匆看一眼這個函數的返回值就馬上進入了未定義行為的領地。這是事實,任何返回一個引向局部變量的引用的函數都是錯誤的。(對于任何返回一個指向局部變量的指針的函數同樣成立。)
那么,讓我們考慮一下在堆上構造一個對象并返回引向它的引用的可能性。基于堆的對象通過使用 new 而開始存在,所以你可以像這樣寫一個基于堆的 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; } 哦,你還是必須要付出一個構造函數調用的成本,因為通過 new 分配的內存要通過調用一個適當的構造函數進行初始化,但是現在你有另一個問題:誰是刪除你用 new 做出來的對象的合適人選?
即使調用者盡職盡責且一心向善,它們也不太可能是用這樣的方案來合理地預防泄漏:
Rational w, x, y, z;
w = x * y * z; // same as operator*(operator*(x, y), z) 這里,在同一個語句中有兩個 operator* 的調用,因此 new 被使用了兩次,這兩次都需要使用 delete 來銷毀。但是 operator* 的客戶沒有合理的辦法進行那些調用,因為他們沒有合理的辦法取得隱藏在通過調用 operator* 返回的引用后面的指針。這是一個早已注定的資源泄漏。
const Rational& operator*(const Rational& lhs, // warning! yet more const Rational& rhs) // bad code! { static Rational result; // static object to which a // reference will be returned
result = ... ; // multiply lhs by rhs and put the // prodUCt inside result return result; } 就像所有使用了 static 對象的設計一樣,這個也會立即引起我們的線程安全(thread-safety)的混亂,但那是它的比較明顯的缺點。為了看到它的更深層的缺陷,考慮這個完全合理的客戶代碼:
bool operator==(const Rational& lhs, // an operator== const Rational& rhs); // for Rationals
Rational a, b, c, d;
... if ((a * b) == (c * d)) { do whatever’s appropriate when the products are equal; } else { do whatever’s appropriate when they’re not; } 猜猜會怎么樣?不管 a,b,c,d 的值是什么,表達式 ((a*b) == (c*d)) 總是等于 true!
我無法拿出示例代碼來肯定這個設計,但我可以概要說明為什么這個想法應該讓你羞愧得無地自容。首先,你必須選擇一個 n 作為數組的大小。假如 n 太小,你可能會用完存儲函數返回值的空間,與剛剛名譽掃地的 single-static 設計相比,在任何一個方面你都不會得到更多的東西。但是假如 n 太大,就會降低你的程序的性能,因為在函數第一次被調用的時候數組中的每一個對象都會被構造。即使這個我們正在討論的函數僅被調用了一次,也將讓你付出 n 個構造函數和 n 個析構函數的成本。假如“優化”是提高軟件效率的過程,對于這種東西也只能是“悲觀主義”的。最后,考慮你怎樣將你所需要的值放入數組的對象中,以及你做這些需要付出什么。在兩個對象間移動值的最直接方法就是通過賦值,但是一次賦值將要付出什么?對于很多類型,這就大約相當于調用一次析構函數(銷毀原來的值)加上調用一次構造函數(把新值拷貝過去)。但是你的目標是避免付出構造和析構成本!面對的結果就是:這個方法絕對不會成功。(不,用一個 vector 代替數組也不會讓事情有多少改進。)