章節回顧:
《Effective C++》第1章 讓自己習慣C++-讀書筆記
《Effective C++》第2章 構造/析構/賦值運算(1)-讀書筆記
《Effective C++》第2章 構造/析構/賦值運算(2)-讀書筆記
《Effective C++》第3章 資源管理(1)-讀書筆記
《Effective C++》第3章 資源管理(2)-讀書筆記
《Effective C++》第4章 設計與宣告(1)-讀書筆記
《Effective C++》第4章 設計與宣告(2)-讀書筆記
《Effective C++》第8章 定製new和delete-讀書筆記
條款26:儘可能延後變數定義式的出現時間
你定義了一個類型別的變數,那麼就要耗費一個建構函式和解構函式。如果你最終不使用這個變數,就應該避免這些耗費。
你可能會懷疑:怎麼可能定義一個變數而不去使用呢?考慮下面的程式碼:
std::string encryptPassword(const std::string& password) { using namespace std; string encrypted; if (password.length() < MinimumPasswordLength) { throw logic_error("Password is too short") } ... return encrypted; }
先不去考慮程式碼具體含義。如果if語句為true,就會丟擲異常,這個encrypted物件仍然需要耗費一個建構函式和一個解構函式。所以最好延後encrypted的定義式,直到確實需要它。
std::string encryptPassword(const std::string& password) { using namespace std; if (password.length() < MinimumPasswordLength) { throw logic_error("Password is too short") } string encrypted; //放在了後面 ... return encrypted; }
這段程式碼不夠穠纖合度(不懂這個詞)。因為encrypted物件呼叫的是預設建構函式,後面幾乎一定會對它重新賦值。舉例如下:
void encrypt(std::string& s); std::string encryptPassword(const std::string& password) { ... string encrypted; //放在了後面,考慮到使用時才定義 encrypted = password; //重新賦值 encrypt(encrypted); return encrypted; }
更好的做法是跳過無意義的default建構函式:
std::string encryptPassword(const std::string& password) { ... string encrypted(password); //拷貝建構函式 encrypt(encrypted); return encrypted; }
所以,“儘可能延後”的真正意義是:你不僅要儘可能延後變數的定義直到要使用它,還應該延後這個變數的定義直到給它初值。這樣可以避免沒有必要的構造和析構物件以及沒有意義的default建構函式。
還有一種情形出現在迴圈裡面,分下下面兩種做法A、B哪個更好:
//做法A Widget w; for (int i = 0; i < n; ++i) { w = 取決於i的某個值; } //做法B for (int i = 0; i < n; ++i) { Widget w = 取決於i的某個值; }
做法A的成本:1個建構函式、1個解構函式和n個賦值;做法B的成本:n個建構函式和n個解構函式。
如果賦值成本低於1個構造+1個析構,則做法A效率高一點,否則B的做法好。另外做法A造成Widget物件作用域擴大。所以,給出的建議是:除非你明確知道幾個操作的成本,否則做法B是比較好的。
請記住:儘可能延後變數定義式的出現。這樣做可增加程式的清晰度並改善程式效率。
條款27:儘量少做轉型動作
C風格的轉型(舊式轉型)如下:
(T) expression; //兩者含義相同 T(expression)
C++提供的4種新式轉型如下:
const_cast<T>(expression) dynamic_cast<T>(expression) reinterpret_cast<T>(expression) static_cast<T>(expression)
一般來說新式轉型比較好。可能舊式轉型比較常用的地方是呼叫explicit建構函式傳遞一個物件給函式時。舉例如下:
class Widget { public: explicit Widget(int size); }; void doSomeWork(const Widget& w); doSomeWork(Widget(15)); //C的函式風格 doSomeWork(static_cast<Widget>(15)); //C++新風格
任何一個型別轉換,無論是通過轉型操作進行的顯式轉換或通過編譯器進行的隱式轉換,往往會導致編譯器產生執行期執行的程式碼。
下面有個轉型程式碼比較有迷惑:
class Window { public: Window(int n = 0) : m(n) {} virtual void onResize() { m = 10; } int m; }; class SpecialWindow : public Window { public: virtual void onResize() { static_cast<Window>(*this).onResize(); } }; int main() { SpecialWindow w1; cout << w1.m << endl; //輸出0 w1.onResize(); cout << w1.m << endl; //輸出0 return 0; }
兩份輸出都是0。不要懷疑,static_cast<Window>(*this).onResize();確實呼叫了class Window的onResize()函式,但關鍵是轉型的結果是(*this)的一個副本,而不是物件本身。
如果你仍然需要呼叫class Window版本的onResize()函式,就要拿掉轉型。
class SpecialWindow : public Window { public: virtual void onResize() { Window::onResize(); } };
dynamic_cast的成本很高,之所以需要它的一個原因是:在一個你認定為derived class物件身上執行derived class函式,但你只有一個指向base的指標或引用。
請記住:
(1)如果可以,儘量避免轉型,特別是在注重效率的程式碼中避免dynamic_cast。如果有個設計需要轉型動作,試著發展無需轉型的替代設計。
(2)如果轉型是必要的,試著將它隱藏於某個函式背後,客戶隨後可以呼叫該函式,而不需將轉型放在自己的程式碼內。
(3)優先使用C++風格的轉型,因為它很容易被辨識出來並且有分類。
條款28:避免返回handles指向物件內部成分
handles包括指標、引用和迭代器。直接用例子說明:
class Point //表示一個“點” { public: Point(int x, int y); ... void setX(int newVal); void setY(int newVal); } struct RectData { Point ulhc; //表示左上角座標 Point lrhc; //表示右下角座標 }; class Rectangle { public: Point& upperLeft() const { return pData->ulhc; } //返回左上角座標 Point& lowerRight() const { return pData->lrhc; } //返回右下角座標 };
Rectangle類設計兩個成員函式upperLeft(),lowerRight()返回左上角和右下角座標是必要的。但這兩個函式都是const的,說明它的目的只是給使用者檢視,並不是讓使用者去修改這些座標。但是客戶這樣做:
Point coord1(0, 0); Point coord2(100, 100); const Rectangle rec(coord1, coord2); rec.upperLeft().setX(50); //左上角座標變為(50,0)
確實改變了座標值,儘管point還是private資料。這給我們的啟示是:成員變數的封裝性最多隻等於返回其reference函式的訪問級別。雖然point是private的,但實際效果卻是public的。
修改版本也很簡單:
class Rectangle { public: const Point& upperLeft() const { return pData->ulhc; } //返回左上角座標的const const Point& lowerRight() const { return pData->lrhc; } //返回右下角座標的const };
另外一點handles指向的東西返回後可能不再存在。舉例說明:
class GUIObject { ... }; const Rectangle boundingBox(const GUIObject& obj); //客戶如下這樣呼叫 GUIObject *pgo; const Point *pUpperLeft = &(boundingBox(*pgo).upperLeft());
boundingBox(*pgo)的呼叫將產生一個臨時的Rectangle物件,在此物件上呼叫upperLeft()返回的是臨時物件的左上角座標,然後這個臨時物件析構,這樣pUpperLeft就是指向一個不存在的東西。
請記住:避免返回handles(包括引用、指標和迭代器)指向內部物件。遵守這個條款可增加封裝性、幫助const成員函式的行為像個const,並降低發生handles指向不存在東西的可能性。