程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> Effective C++讀書筆記(13)

Effective C++讀書筆記(13)

編輯:C++入門知識

條款21:必須返回對象時,別妄想返回其reference

Don’t try to return a reference when youmust return an object

一旦程序員抓住對象傳值的效率隱憂,很多人就會一心一意根除傳值的罪惡。他們不屈不撓地追求傳引用的純度,但他們全都犯了一個致命的錯誤:他們開始傳遞並不存在的對象的引用。考慮一個用以表現有理數的類,包含一個函數計算兩個有理數的乘積:

class Rational {
public:
Rational(int numerator = 0, int denominator = 1);

    ...

private:
int n, d; // 分子與分母

friend
const Rational operator*(const Rational& lhs, const Rational& rhs);
};

operator* 的這個版本以傳值方式返回它的結果,需要付出對象的構造和析構成本。如果你能用返回一個引用來代替,就不需付出代價。但是,請記住一個引用僅僅是一個名字,一個實際存在的對象的名字。無論何時只要你看到一個引用的聲明,應該立刻問自己它是什麼東西的別名,因為它必定是某物的別名。以上述operator*為例,如果函數返回一個引用,它必然返回某個既有的而且包含兩個對象相乘產物的Rational對象引用。

當然沒有什麼理由期望這樣一個對象在調用operator*之前就存在。也就是說,如果你有

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

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

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

期望原本就存在一個值為3/10的有理數對象並不合理。如果operator*返回一個reference指向如此數值,它必須自己創建那個Rational對象。

函數創建新對象僅有兩種方法:在棧或在堆上。如果定義一個local變量,就是在棧空間創建對象:

const Rational& operator*(constRational& lhs, const Rational& rhs)
{
Rational result(lhs.n * rhs.n, lhs.d * rhs.d);
return result;
}  //糟糕的代碼!

這個函數返回一個指向result的引用,但是result是一個局部對象,在函數退出時被銷毀了。因此這個operator*的版本不會返回指向一個Rational的引用,它返回指向一個過時的Rational,因為它已經被銷毀了。任何調用者甚至只是對此函數的返回值做任何一點點運用,就立刻進入了未定義行為的領地。這是事實,任何返回一個指向局部變量引用(或指針)的函數都是錯誤的。

考慮一下在堆上構造一個對象並返回指向它的引用的可能性。基於堆的對象通過使用new創建:

const Rational& operator*(constRational& lhs, const Rational& rhs)
{
Rational *result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d);
return *result;
}  //更糟的寫法!

誰該隊你用new創建出來的對象實施delete?

Rational w, x, y, z;

w = x * y * z; // 與operator*(operator*(x, y), z)相同

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

無論是在棧還是在堆上的方法,為了從operator*返回的每一個 result,我們都不得不容忍一次構造函數的調用,而我們最初的目標是避免這樣的構造函數調用。我們可以繼續考慮基於 operator*返回一個指向staticRational對象引用的實現,而這個static Rational對象定義在函數內部:

const Rational& operator*(constRational& lhs, const Rational& rhs)
{
static Rational result; // static對象,此函數返回其reference

    result= ... ; // 將lhs乘以rhs,並將結果置於result內
return result;
}  //又一堆爛代碼!

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

// 一個針對Rational所寫的operator==

Rational a, b, c, d;

...

if ((a * b) == (c * d)) {當乘積相等時,做適當的相應動作;}

else {當乘積不等時,做適當的相應動作}

除了和所有使用static對象的設計一樣可能引起的線程安全(thread-safety)的混亂,上面不管 a,b,c,d 的值是什麼,表達式 ((a*b) == (c*d)) 總是等於 true!如果代碼重寫為功能完全等價的另一種形式,很容易了解出了什麼意外:

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

在operator==被調用前,已有兩個起作用的operator*調用,每一個都返回指向 operator*內部的staticRational對象的引用。兩次operator*調用的確各自改變了staticRational對象值,但由於它們返回的都是reference,因此調用端看到的永遠是static Rational對象的“現值”。

一個必須返回新對象的函數的正確方法就是讓那個函數返回一個新對象。對於Rational的 operator*,這就意味著下面這些代碼或在本質上與其等價的代碼:

inline const Rational operator*(constRational& lhs, const Rational& rhs)
{return Rational(lhs.n * rhs.n, lhs.d * rhs.d);}

當然,你可能付出了構造和析構operator*的返回值的成本,但是從長遠看,這只是為正確行為付出的很小代價。但萬一代價很恐怖,你可以允許編譯器施行最優化,用以改善出碼的效率卻不改變其可觀察的行為。因此某些情況下operator*返回值的構造和析構可被安全的消除。如果編譯器運用這一事實(它們也往往如此),程序將保持應有行為,而執行起來又比預期的更快。

總結:如果需要在返回一個引用和返回一個對象之間做決定,你的工作就是讓那個選擇能提供正確的行為。讓你的編譯器廠商去絞盡腦汁使那個選擇成本盡可能地低廉。

·    絕不要返回一個local棧對象的指針或引用,絕不要返回一個被分配的堆對象的引用,如果存在需要一個以上這樣的對象的可能性時,絕不要返回一個局部 static 對象的指針或引用。

 

條款22:將成員變量聲明為private

Declare data members private

首先,我們將看看為什麼數據成員不應該聲明為 public;

然後,我們將看到所有反對public數據成員的理由同樣適用於protected數據成員。

最後導出了數據成員應該是private的結論。

那麼,為什麼不應該聲明public數據成員?以下有三大理由:

1.語法一致性: 如果數據成員不是public的,客戶訪問一個對象的唯一方法就是通過成員函數。如果在public接口中的每件東西都是函數,客戶就不必絞盡腦汁試圖記住當他們要訪問一個類的成員時是否需要使用圓括號,他們只要使用就可以了,因為每件東西都是一個函數。

2.精確控制成員變量的處理:如果你讓一個數據成員為public,每一個人都可以讀寫訪問它,但是如果你使用函數去得到和設置它的值,你就能實現禁止訪問,只讀訪問和讀寫訪問,甚至只寫訪問:

class AccessLevels {
public:
...
int getReadOnly() const { return readOnly; }
void setReadWrite(int value) { readWrite = value; }
int getReadWrite() const { return readWrite; }
void setWriteOnly(int value) { writeOnly = value; }

private:
int noAccess; // no access
int readOnly; // read-only access
int readWrite; // read-write access
int writeOnly; // write-only access
};

3.封裝:如果你通過一個函數實現對數據成員的訪問,你可以以後改以某個計算來替換這個數據成員,使用你的類的人不會有任何察覺。

例如,假設你正在寫一個自動測速程序,當汽車通過,其速度便被計算並填入一個速度收集器內:

class SpeedDataCollection {
...
public:
void addValue(int speed); // 添加一筆新數據
double averageSoFar() const; // 返回平均速度
...
};

現在考慮成員函數averageSoFar的實現:辦法之一是在類中用一個數據成員來實時變化迄今為止收集到的所有速度數據的平均值。無論何時averageSoFar被調用,它需返回那個數據成員的值。另一個方法是在每次調用averageSoFar時重新計算,通過分析集合中每一個數據值做成這些事情。

誰能說哪一個最好?在內存非常緊張的機器(如,一台嵌入式路邊偵測設裝置)上,或是一個很少需要平均值的應用程序中,每次都計算平均值可能是較好的解決方案;在一個頻繁需要平均值的應用程序中,速度比較重要,且內存不成問題,保持一個實時變化的平均值更為可取。重點在於通過一個成員函數訪問平均值(也就是說將它“封裝”),你能替換這兩個不同的實現(也包括其他你可能想到的)。

封裝可能比它最初顯現出來的更加重要。如果你對你的客戶隱藏你的數據成員(也就是說,封裝它們),你就能確保類的約束條件總能被維持,因為只有成員函數能影響它們。此外,你預留了日後變更實現的權利。如果你不隱藏你將很快發現,即使你擁有類的源代碼,你改變任何一個public的東西的能力也是非常有限的,因為有太多的客戶代碼將被破壞。public意味著沒有封裝,沒有封裝意味著不可改變,尤其是被廣泛使用的類。被廣泛使用的類是最需要封裝的,因為它們可以從一種更好的實現中得益。

l  切記聲明數據成員為private。它為客戶提供了訪問數據的一致,細微劃分的訪問控制,允許約束條件獲得保證,而且為類的作者提供了實現上的彈性。

 

為什麼不應該聲明protected數據成員?

反對protected數據成員的理由是類似的。關於語法一致性和細微劃分之訪問控制等理由顯然也適用於protected數據,就連封裝性上protected數據成員也不比public數據成員更好。

某些東西的封裝性與“當其內容改變時可能造成的代碼破壞量“成反比。所謂改變,也許是從類中移除它(就像上述的averageSoFar)。

假設我們有一個public數據成員,隨後我們移除了它,所有使用了它的客戶代碼,其數量通常大得難以置信,因此public數據成員是完全未封裝的。但是,假設我們有一個protected數據成員,隨後我們移除了它。現在有多少代碼會被破壞呢?所有使用了它的派生類,典型情況下,代碼的數量還是大得難以置信,因此protected數據成員就像public數據成員一樣沒有封裝。在這兩種情況下,如果數據成員發生變化,被破壞的客戶代碼的數量都大得難以置信。一旦你聲明一個數據成員為public或protected,而且客戶開始使用它,就很難再改變與這個數據成員有關的任何事情。有太多的代碼不得不被重寫,重測試,重文檔化,或重編譯。從封裝的觀點來看,實際只有兩個訪問層次:private(提供了封裝)與其他(沒有提供封裝)。

protected並不比 public的封裝性強。



 摘自 pandawuwyj的專欄

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