假設你正在一個包含矩形的應用程序上工作。每一個矩形都可以用它的左上角和右下角表示出來。為了將一個 Rectangle 對象保持在較小狀態,你可能決定那些點的定義的域不應該包含在 Rectangle 本身之中,更合適的做法是放在一個由 Rectangle 指向的輔助的結構體中:
class Point {
// class for representing points
public:
Point(int x, int y);
...
void setX(int newVal);
void setY(int newVal);
...
};
struct RectData {
// Point data for a Rectangle
Point ulhc; // ulhc = " upper left-hand corner"
Point lrhc; // lrhc = " lower right-hand corner"
};
class Rectangle {
...
private:
std::tr1::shared_ptr<RectData> pData; // see Item 13 for info on
}; // tr1::shared_ptr
由於 Rectangle 的客戶需要有能力操控 Rectangle 的區域,因此類提供了 upperLeft 和 lowerRight 函數。可是,Point 是一個用戶定義類型,所以,在典型情況下,以傳引用的方式傳遞用戶定義類型比傳值的方式更加高效的觀點,這些函數返回引向底層 Point 對象的引用:
class Rectangle {
public:
...
Point& upperLeft() const { return pData->ulhc; }
Point& lowerRight() const { return pData->lrhc; }
...
};
這個
設計可以編譯,但它是錯誤的。實際上,它是自相矛盾的。一方面,upperLeft 和 lowerRight 是被聲明為 const 的成員函數,因為它們被設計成僅僅給客戶提供一個獲得 Rectangle 的點的方法,而不允許客戶改變這個 Rectangle。另一方面,兩個函數都返回引向私有的內部數據的引用——調用者可以利用這些引用修改內部數據!例如: Point coord1(0, 0);
Point coord2(100, 100);
const Rectangle rec(coord1, coord2); // rec is a const rectangle from
// (0, 0) to (100, 100)
rec.upperLeft().setX(50); // now rec goes from
// (50, 0) to (100, 100)!
請注意這裡,upperLeft 的調用者是怎樣利用返回的 rec 的內部 Point 數據成員的引用來改變這個成員的。但是 rec 卻被期望為 const!
這直接引出兩條經驗。第一,一個數據成員被封裝,但是具有最高可訪問級別的函數還是能夠返回引向它的引用。在當前情況下,雖然 ulhc 和 lrhc 被聲明為 private,它們還是被有效地公開了,因為 public 函數 upperLeft 和 lowerRight 返回了引向它們的引用。第二,如果一個 const 成員函數返回一個引用,引向一個與某個對象有關並存儲在這個對象本身之外的數據,這個函數的調用者就可以改變那個數據(這正是二進制位常量性的局限性的一個副作用)。
我們前面做的每件事都涉及到成員函數返回的引用,但是,如果它們返回指針或者迭代器,因為同樣的原因也會存在同樣的問題。引用,指針,和迭代器都是句柄(handle)(持有其它對象的方法),而返回一個對象內部構件的句柄總是面臨危及對象封裝安全的風險。就像我們看到的,它同時還能導致 const 成員函數改變了一個對象的狀態。
我們通常認為一個對象的“內部構件”就是它的數據成員,但是不能被常規地公開訪問的成員函數(也就是說,它是 protected 或 private 的)也是對象內部構件的一部分。同樣地,不要返回它們的句柄也很重要。這就意味著你絕不應該有一個成員函數返回一個指向擁有較小的可訪問級別的成員函數的指針。如果你這樣做了,它的可訪問級別就會與那個擁有較大的可訪問級別的函數相同,因為客戶能夠得到指向這個擁有較小的可訪問級別的函數的指針,然後就可以通過這個指針調用這個函數。
無論如何,返回指向成員函數的指針的函數是難得一見的,所以讓我們把注意力返回到 Rectangle 類和它的 upperLeft 和 lowerRight 成員函數。我們在這些函數中挑出來的問題都只需簡單地將 const 用於它們的返回類型就可以排除:
class Rectangle {
public:
...
const Point& upperLeft() const { return pData->ulhc; }
const Point& lowerRight() const { return pData->lrhc; }
...
};
通過這個修改的設計,客戶可以讀取定義一個矩形的 Points,但他們不能寫它們。這就意味著將 upperLeft 和 upperRight 聲明為 const 不再是一句空話,因為他們不再允許調用者改變對象的狀態。至於封裝的問題,我們總是故意讓客戶看到做成一個 Rectangle 的 Points,所以這是封裝的一個故意的放松之處。更重要的,它是一個有限的放松:只有讀訪問是被這些函數允許的,寫訪問依然被禁止。
雖然如此,upperLeft 和 lowerRight 仍然返回一個對象內部構件的句柄,而這有可能造成其它方面的問題。特別是,這會導致空懸句柄:引用了不再存在的對象的構件的句柄。這種消失的對象的最普通的來源就是函數返回值。例如,考慮一個函數,返回在一個矩形窗體中的 GUI 對象的 bounding box:
class GUIObject { ... };
const Rectangle // returns a rectangle by
boundingBox(const GUIObject& obj); // value; see Item 3 for why
// return type is const
現在,考慮客戶可能會這樣使用這個函數:
GUIObject *pgo; // make pgo point to
... // some GUIObject
const Point *pUpperLeft = // get a ptr to the upper
&(boundingBox(*pgo).upperLeft()); // left point of its
// bounding box
對 boundingBox 的調用會返回一個新建的臨時的 Rectangle 對象。這個對象沒有名字,所以我們就稱它為 temp。於是 upperLeft 就在 temp 上被調用,這個調用返回一個引向 temp 的一個內部構件的引用,特別是,它是由 Points 構成的。隨後 pUpperLeft 指向這個 Point 對象。到此為止,一切正常,但是我們無法繼續了,因為在這個語句的末尾,boundingBox 的返回值—— temp ——被銷毀了,這將間接導致 temp 的 Points 的析構。接下來,剩下 pUpperLeft 指向一個已經不再存在的對象;pUpperLeft 空懸在創建它的語句的末尾!
這就是為什麼任何返回一個對象的內部構件的句柄的函數都是危險的。它與那個句柄是指針,引用,還是迭代器沒什麼關系。它與是否受到 cosnt 的限制沒什麼關系。它與那個成員函數返回的句柄本身是否是 const 沒什麼關系。全部的問題在於一個句柄被返回了,因為一旦這樣做了,你就面臨著這個句柄比它引用的對象更長壽的風險。
這並不意味著你永遠不應該讓一個成員函數返回一個句柄。有時你必須如此。例如,operator[] 允許你從 string 和 vector 中取出單獨的元素,而這些 operator[]s 就是通過返回引向容器中的數據的引用來工作的——當容器本身被銷毀,數據也將銷毀。盡管如此,這樣的函數屬於特例,而不是慣例。
Things to Remember
·避免返回對象內部構件的句柄(引用,指針,或迭代器)。這樣會提高封裝性,幫助 const 成員函數產生 cosnt 效果,並將空懸句柄產生的可能性降到最低。