標籤:c++ 讀書筆記
條款五
class Empty { };
這樣的一個類,當C++處理過後,編譯器會為它聲明一個copy建構函式、一個copy assignment操作符、一個解構函式和一個default建構函式,所有這些函數都是public且inline。
class Empty {public: Empty() { ... } Empty(const Empty& rhs) { ... } ~Empty() { ... } Empty& operator=(const Empty& rhs) { ... }};
default建構函式和解構函式主要是編譯器用來放置調用base classes和non-static成員變數的建構函式和解構函式等,編譯器產出的解構函式是個non-static,除非這個class的base class自身聲明有virtual解構函式。
copy建構函式和copy assignment操作符,編譯器只是將來來源物件的每一個non-static成員變數拷貝到目標對象。考慮這樣一個類:
template<typename T>class NamedObject {public: NamedObject(const char* name, const T& value); NamedObject(const std::& name, const T& value); ...private: std::string nameValue; T objectValue;}NamedObject<int> no1("doubi", 2);NamedObject<int> no2(no1);
編譯器產生的copy建構函式會以no1.nameValue和no1.objectValue為初值設定no2.nameValue和no2.objectValue。nameValue的類型是string,而標準的string有copy建構函式,所以no2.nameValue的初始化方式是調用string的copy建構函式並以no1.nameValue為實參,另一個成員NameObject::objectValue的類型是int,是內建類型,直接copy記憶體中的每個bits完成初始化。
copy assignment操作符的產生方式和copy建構函式的產生方式基本一致,但如果產生的程式碼是不合法的,編譯器會拒絕為class產生operator=。例如成員變數是reference或const,或者base class將copy assignment操作符聲明為private。
條款六
通常不希望class支援某一項特定的功能,只要不聲明對應的函數即可,但這個策略對copy建構函式和copy assignment操作符卻不起作用,因為編譯器會預設聲明。那麼你要怎麼讓這些函數失效呢?
所有的編譯器產出的函數都是public,為阻止這些函數被建立出來,需要自行聲明它們,可以聲明為private。一般而言這種做法並不是絕對安全的,因為member函數和friend函數還是可以調用你的private函數。當然你可以不去定義它們,如果某些人不慎調用了任何一個,就會獲得一個連結錯誤(linkage error),這一點C++的iostream程式庫就是這麼做的。
class HomeForSale {public: ...private: ... HomeForSale(const HomeForSale&); HomeForSale& operator=(const HomeForSale&);};
將串連期錯誤轉移到編譯期是可能的(而且是好事,越早偵測出錯誤越好),設計一個專門阻止copying動作的base class類:
class Uncopyable {protected: Uncopyable() {} ~Uncopyable() {}private: Uncopyable(const Uncopyable&); Uncopyable& operator=(const Uncopyable&);};class HomeForSale : private Uncopyable {};
這樣當member函數和friend函數嘗試copy HomeForSale對象,編譯器便試著產生一個copy建構函式和一個copy assignment操作符,編譯器會嘗試調用base class,因為private所以會被拒絕。
Uncopyable class的實現和運用很微妙,不一定要以public繼承它,Uncopyable的析構也不一定得是virtual。
條款七
class TimeKeeper {public: TimeKeeper(); ~TimeKeeper(); ...};class AtomicClock : public TimeKeeper { ... };class WaterClock : public TimeKeeper { ... };class WristWatch : public TimeKeeper { ... };TimeKeeper* ptk = getTimeKeeper();...delete ptk;
getTimeKeeper返回一個derived class對象,而那個對象卻經由一個base class指標被刪除,而目前的base class有個non-virtual解構函式。當derived class對象經由一個base class指標被刪除,而該base class帶著一個non-virtual解構函式,實際執行時通常發生的是對象的derived成分沒被銷毀,容易造成資源泄漏、敗壞之資料結構、在調試器上浪費時間。
解決這個問題的方法就是給base class一個virtual解構函式。
class TimeKeeper {public: TimeKeeper(); virtual ~TimeKeeper(); ...};
考慮下面這個class:
class Point {public: Point(int xCoord, int yCoord); ~Point();private: int x,y;};
任何class只要帶有virtual函數都幾乎確定應該也有一個virtual解構函式,帶有virtual函數通常表明該class被設計為一個virtual函數。因為virtual函數實現使得對象必須攜帶某些資訊來決定哪一個virtual函數應該被調用。這份資訊通常是由vptr指標指出,vptr指向一個由函數指標構成的數組,稱為vtbl。
因為virtual函數的緣故,對象的體積就會增加,32-bit電腦體積結構中將佔用64bits(存放2個ints)至96bits(2個ints加上vptr);在64-bit電腦體繫結構中可能佔用64~128bits,因為指標在這樣的電腦結構中佔64bits。
class AWOV {public: virtual ~AWOV() = 0;};AWOV::~AWOV() { }
對於一個擁有pure virtual解構函式的抽象類別,必須為解構函式提供一個定義式,不然連接器會報錯。
條款八
class Widget {public: ... ~Widget() { ... };};void doSomething(){ std::vector<Widget> v; ...}
當vector v被銷毀,它需要銷毀其內含的所有Widgets,如果解構函式中拋出異常,當兩個異常同時存在的情況下,程式若不是結束執行就是導致不明確行為。這個時候如果需要保證資源回收呢?
class DBConnection {public: ... static DBConnection create(); void close();};
為了確保客戶不忘記在DBConnection對象身上調用close(),通常的做法就是建立一個用來管理DBConnection資源的class,並在解構函式中調用close。
class DBConn {public: ... ~DBConn() { db.close(); }private: DBConnection db;};
這種做法如果close調用失敗還是會在析構中出現問題,拋出難以駕馭的問題。
兩種方法可以解決這個問題:
(1)如果close拋出異常就結束程式:
DBConn::~DBConn(){ try { db.close(); } catch (...) { std::abort(); }}
(2)吞下因調用close而發生的異常:
DBConn::~DBConn(){ try { db.close(); } catch (...) { }}
但這兩種方法都無法真正地解決問題。一個比較好的策略就是重新設計DBConn介面,例如DBConn自己提供一個close函數,追蹤DBConnection是否已經關閉,並在答案為否的情況下由解構函式關閉,可以防止遺失資料庫連接。然而如果DBConnection解構函式調用close失敗,還是需要“結束程式”和“吞下異常”。
由客戶自己調用close並不會對他們帶來負擔,而是給他們一個處理錯誤的機會,否則沒機會響應。
條款九
看下面的代碼:
class Transaction {public: Transaction(); virtual void logTransaction() const = 0; ...};Transaction::Transaction() { ... logTransaction;}class BuyTransaction : public Transaction { public: virtual void logTransaction() const; ...};class SellTransaction : public Transaction {public: virtual void logTransaction() const; ...};BuyTransaction b;
當執行BuyTransaction b的時候,首先Transaction的建構函式會被調用,建構函式的最後一行調用了virtual的logTransaction。這個時候調用logTransaction是Transaction的版本,而不是BuyTransaction內的版本,因為在base class構造期間virtual函數不會下降到derived classes階層。
在derived class對象的base class構造期間,對象的類型是base class而不是derived class。不只virtual函數會被編譯器解析到base class,如果要使用運行期類型資訊(例如dynamic_cast和typeid),也會把對象視為base class類型。同樣的道理也適用於解構函式,進入到base class解構函式後對象就成為一個base class對象。
所以上述的例子在啟動並執行時候就會出現連接器報錯,pure virtual是沒有定義的。那要如何?這樣的機制?每次Transaction繼承體繫上有對象被建立,就會調用正確的logTransaction函數?
(1)將logTransaction函數改為non-virtual,然後要求derived class建構函式傳遞必要的資訊給Transaction建構函式:
class Transaction {public: explicit Transaction(const std::string& logInfo); void logTransaction(const std::string& logInfo) const; ...};Transaction::Transaction(const std::string& logInfo){ ... logTransaction(logInfo);}class BuyTransaction : public Transaction {public: BuyTransaction (parameters) : Transaction(createLogString(parameters)) { ... } ...private: static std::string createLogString(parameters);};
因為無法使用virtual函數從base classes向下調用,在構造期間,可以將derived classes將必要的資訊向上傳遞給base class建構函式。
這裡有個需要注意的小技巧:BuyTransaction內的private static函數createLogString,比起在初始化列表中直接傳遞資料,利用輔助函數往往更加方便,最好令這個函數為static,這樣就不會意外地指向BuyTransaction中未初始化的成員變數。
條款十
賦值可以寫出連鎖的形式:
x = y = z = 15;
因為賦值是採用右結合律的,所以賦值操作符必須要返回一個reference指向操作符的左側實參。
class Widget {public: ... Widget& operator=(const Widget* rhs) { ... return *this; }};
這個協議不僅適用於以上的標準賦值形式,也適用於所有賦值相關運算,如+=,-=,*=等。
條款十一
自我賦值在對象或變數存在別名的情況下很有可能會出現,假設你建立一個class用來儲存一個指標指向一塊動態分配的位元影像:
class Bitmap { ... };class Widget { ...private: Bitmap* pb;};Widget& Widget::operator=(const Widget& rhs) { delete pb; pb = new Bitmap(*rhs.pb); return *this;}
這裡的自我賦值問題是this如果和rhs是同一個對象,delete刪除的就是當前對象的bitmap,想要阻止這種錯誤,傳統的做法是藉由operator=最前面的一個“認同測試”達到“自我賦值”的檢驗目的:
Widget* Widget::operator=(const Widget& rhs) { if (this == &rhs) return *this; delete pb; pb = new Bitmap(*rhs.pb); return *this;}
這樣的做法確實可以解決自我賦值的問題,但是正如上一個條款所說,並不具備異常安全性。如果new Bitmap拋出異常,Widget持有的指標指向了一塊刪除的Bitmap。很多時候其實只要注意語句的順序就可以解決異常安全的問題。
Widget& Widget::operator=(const Widget& rhs) { Bitmap* pOrig = pb; pb = new Bitmap(*rhs.pb) delete pOrig; return *this;}
現在如果“new Bitmap”拋出異常,pb依然不變,即使沒有認同測試這裡copy了一份bitmap,刪除了原先的bitmap,效率上是降低的。然而如果將認同測試加入到函數起始處同樣需要成本,因為通常來說“自我賦值”發生的機率非常低,認同測試使得代碼變大,並匯入了一個新的控制流程分支,降低了執行速度,prefetching、caching和pipelining等指令的效率都會因此降低。
另一種方案是使用copy and swap技術,這也是operator=常見的一種撰寫手法。
class Widget { ... void swap(Widget& rhs); ...};Widget& Widget::operator=(const Widget& rhs) { Widget temp(rhs); swap(temp); return *this;}
這個方法的另一種實現方式基於的事實是:
(1)某class的copy assignment操作符可能被聲明為“以by value方式接受實參”;
(2)以by value方式傳遞東西會造成一份副本。
Widget& Widget::operator=(Widget rhs) { swap(rhs): return *this;}
這種做法用它巧妙的修補而犧牲了清晰性,而且將copying的動作從函數的本體移至函數參數構造階段可以令編譯器產生更加高效的代碼。
條款十二
一個設計良好的物件導向系統會將對象的內部封裝起來,只留下兩個函數負責對象拷貝,那便是copy建構函式和copy assignment操作符,編譯器會在適當的時候為classes建立copying函數,並將被拷貝的對象的所有成員變數都做一份拷貝。如果聲明自己的copying函數,那麼當實現代碼中出現錯誤的時候,編譯器是不會告訴你的。
考慮這樣一個class:
void logCall(const std::string& funcName);class Customer {public: ... Customer(const Customer& rhs); Customer& operator=(const Customer& rhs); ...private: std::string name;};Customer::Customer(const Customer& rhs) : name(rhs.name){ logCall("Customer copy constructor");}Customer& Customer::operator=(const Customer& rhs) { logCall("Customer copy assignment operator"); name = rhs.name; return *this;}
這種做法帶來的問題就是,每次有新的成員變數加入就必須同時修改copying函數,而且這個時候編譯器是不會報錯的。而且一旦發生了繼承,更是容易出現這樣的情況。如果是重寫繼承類的copying函數就需要確保調用了基類的copying函數。
當編寫一個copying函數時要確保:
(1)複製所有的local成員變數;
(2)調用所有的base classes內的適當的copying函數。
很多時候自己的構造一個類的時候要思考清除類擴充的複雜度:
(1)新增成員變數、函數;
(2)繼承多態;
(3)運算子操作;
(4)多線程。
如果發現copy建構函式和copy assignment操作符有相近的代碼,消除重複代碼的做法是,建立一個private且通常被命名為init的函數給兩者調用。
…終於結束了第二章….
讀書筆記_Effective C++_構造,析構,賦值