EffectiveC++编程的50个建议
文章目錄
- 何時調用`copy`構造函數
- 視`C++`為一個語言聯邦
- 盡量以`cosnt、enum、inline`替換`#define`
- 盡可能使用`const`
- 確定對象被使用之前已先被初始化
- 了解`C++`默默編寫并調用哪些函數
- 若不想使用編譯器自動生成的函數,就明確拒絕
- 為多態基類聲明`virtual`析構函數
- 別讓異常逃離析構函數
- 絕對不再構造和析構過程中調用`virtual`函數
- 令`operator=`返回一個`reference to *this`
- 在`operator=`中處理`自我賦值`
- 以對象管理資源
- 在資源管理類中提供對原始資源的訪問
- 成對的使用`new`和`delete`時要采取相同的形式
- 以獨立語句將`newed`對象置入智能指針
- 將成員變量聲明為`private`
- 盡可能延后變量定義式的出現時間
- 盡量少做轉型動作
何時調用copy構造函數
在構造函數調用的時候,有的時候調用默認構造函數,有的時候調用copy構造函數,特別是copy構造函數的調用讓人容易和copy賦值的函數產生混淆。
如下對其進行了測試:
class WidgetOperator { public:WidgetOperator() = default;;~WidgetOperator() = default;;WidgetOperator(const WidgetOperator & wo) {std::cout << "call WidgetOperator ctor" << std::endl;}WidgetOperator& operator=(const WidgetOperator& wo) {std::cout << "call WidgetOperator operator= " << std::endl;return *this;} };void WidgetOperatorTest() {std::cout << "W1" << std::endl;WidgetOperator W1; // 調用無參構造函數std::cout << "W2(W1)" << std::endl;WidgetOperator W2(W1); // 調用copy構造函數std::cout << "W1 = W2" << std::endl;W1 = W2; // 調用 operator=函數std::cout << "WidgetOperator W3 = W1" << std::endl;WidgetOperator W3 = W1; // 調用copy構造函數 }執行輸出結果:
W1 W2(W1) call WidgetOperator ctor W1 = W2 call WidgetOperator operator= W3 = W1 call WidgetOperator ctor通過上述測試的輸出可以看出,當調用=操作符的時候,如果一個新對象被定義。如:WidgetOperator W3 = W1;,一定會有一個構造函數被調用,不可能調用賦值操作,反之,如果沒有一個新的對象被定義,就不會有構造函數被調用,而只會調用賦值操作符。
視C++為一個語言聯邦
一開始C++只是C加上一些面向對象特性,但是隨著這個語言的成熟他變得更加無拘無束,接受不同于C with classes的各種觀念、特性和編程戰略。異常對函數的結構化帶來了不同的做法,templates將我們帶來到新的設計思考方式,STL則定義了一個前所未見的伸展性做法。
今天C++已經是個多重范型編程語言,一個同時支持過程形式、面向對象形式、函數形式、泛型形式、元編程形式的語言。這些能力和彈性使C++成為一個無可匹敵的工具,因此、將C++視為一個語言聯邦。
盡量以cosnt、enum、inline替換#define
因為、宏定義會被預處理器處理,編譯器并未看到宏定義的信息,當出現一個編譯錯誤信息的時候,可能會帶來困惑。
解決之道就是使用一個常量替換宏定義(#define)
const double AspectRatio = 1.653; // 大寫名稱通常代表宏定義,因此這里可以使用首字母大寫的方法表示const全局變量作為一個語言常量,AspectRatio肯定會被編譯器看到,當然就會進入符號表內。另外、使用常量也可以有較小的碼、因為使用預處理會導致預處理器盲目的將宏名稱替換為對應的數值,可能會導致目標碼出現多份宏定義的數值。
基于數個理由enum hack值得我們認識。
class GamePlayer{private:enum {NumTurns = 5}; // enum hack 令NumTurns成為5的一個標記int scores[NumTurns]; // };- enum hack的行為某方面來說比較像#define而不像const,有的時候這正是你想要的,例如取一個const的地址是合法的,但是取一個enum的地址就是不合法的,而取一個#define的地址通常也不合法。如果你不想讓別人獲得一個pointer或者reference指向你的某個整數常量,enum可以幫助你實現這個約束。
- 雖然優秀的編譯器不會為const對象設置存儲空間,但是不夠優秀的編譯器可能會設置另外的儲存空間,enum和#define一樣絕對不會導致非必要的內存分配。
- 出于實用主義考慮,很多代碼特別是模板元編程中用到了它,因此、看到它你必須認識他。
對于單純的常量,最好以const對象或者enums替換#define
對于形似函數的宏(macros),最好改用inline函數替換#define
盡可能使用const
const的一件奇妙的事情是,它允許你指定一個語義約束,而編譯器會強制實施這項約束。它允許你告訴拜你一起和其他程序員某值應該保持不變。
char greeting[] = "Hello"; char *p = greeting; // non-const pointer, non-const data const char* p = greeting; // non-const pointer, const data char* const p = greeting; // const pointer non-const data const char* const p = greeting; // const pointer, const dataconst語法雖然變化多端,但并不是莫測高深,如果關鍵字const出現在型號的左邊,表示被指物是常量,如果出現在星號的右邊,表示指針自身是常量,如果出現在星號兩邊,表示被指物和指針兩者都是常量。
如果被指物是常量,有些程序員會將關鍵字const寫在類型之前,有些人會把它寫在類型之后、星號之前,這兩種寫法的意義相同,所以下列兩個函數的參數類型是一樣的:
void f(const Widget* pw); // 一個指向常量的指針 void f2(Widget const* pw); // 一個指向常量的指針兩種形式都有人使用,是否是指向常量的指針,要看const相對于星號的位置,星號左邊為指向常量的指針,星號右邊為常量指針。
const修飾函數返回值,可以降低編碼出現的低級錯誤
class Rational {}; const Rational operator*(const Rational& lhs, const Rational& rhs); Rational a, b, c; if (a*b = c) // 其實是想做個比較,當operator*返回值聲明為const的時候將會返回錯誤,也就防止了編碼不小心帶來的異常const修飾成員函數
- 可以通過const得知哪些函數可以改動對象內容,哪些函數不可以
- 使得操作const對象成為可能
確定對象被使用之前已先被初始化
關于將變量初始化這件事,C++似乎總是反復無常。但是有一點是可以確定的是,讀取沒有初始化的值會導致不確定行為
了解C++默默編寫并調用哪些函數
什么時候empty class不再是個空類呢?當C++處理過之后,是的,如果你沒有自己聲明,并一起就會為它聲明(編譯器版本)一個copy構造函數、一個copy assignment操作符和一個析構函數。
因此、如果你聲明了一個empty class如下:
class Empty{};編譯器處理之后就好像你寫了如下的代碼:
class Empty { public:Empty() {} // default構造函數Empty(const Empty& rhs) {} // copy構造函數~Empty() {} //析枸函數Empty& operator=(const Empty& rhs) {} // copy assignment 操作符 };唯有當這些函數被需要(被調用),它們才會被編譯器創建出來。
好了,我們知道編譯器會常見這些函數,但這些函數做了什么?default構造函數和析構函數,主要是給編譯器一個地方放置藏在幕后的代碼,像是調用base class和non-static成員變量的構造函數和析構函數。需要注意的是編譯器默認的析構函數是non-virtual的。
若不想使用編譯器自動生成的函數,就明確拒絕
有時你不想讓用戶使用某個函數,不對函數進行聲明就行了。但是這樣做對copy構造函數和copy assignment操作符卻不起作用,因為、如果你不進行聲明,編譯器會聲明一個默認的出來。
這就把你逼到一個困境,如果你不想讓用戶使用copy構造函數和copy assignment函數,你既不能不聲明也不能進行聲明。這個問題的解決方案就是,將函數聲明為私有的函數,這樣你即可以阻止編譯器創建它們,又因為是私有函數,使得別人不能調用。
但是這樣做并不是絕對安全的,因為member函數和friend函數還是可以調用private函數的。除非你足夠聰明不去定義它們,那么如果任何人不慎調用了任何一個函數,將會導致一個鏈接錯誤,將成員函數聲明為私有,而又故意不去實現它們是如此的受歡迎。、
class HomeForSale { public:... private:HomeForSale(const HomeForSale&); // 因為根本沒有人能調用,寫參數名稱也是浪費HomeForSale& operator=(const HomeForSale&); };有了上述的定義之后,當用戶企圖調用拷貝HomeForSale對象的時候,編譯器會阻止他,如果不慎在member或者friend函數中調用,連接器也會發出抱怨。
為了駁回編譯器自動提供的功能,可將相應的成員函數聲明為private并且不予實現。
為多態基類聲明virtual析構函數
如果多條基類沒有聲明虛析構函數,那么當通過基類指針指向一個子類對象,調用delete的時候只會調用基類的析構函數,不會調用子類的,這樣就會造成資源部分釋放的現象。
如果class不含有virtual函數,通常表示它并不意圖被用作基類:
如一個二維空間點坐標的class:
class Point { // 二維空間點(2D point) public:Point(int xCoord, int yCoord);~Point(); private:int x, y; };如果int占32bits那么Point對象可以塞進一個64-bit緩存器中。更有甚者,這個類完全可以作為一個64-bit量,傳遞給其他語言,如C,但是當Point的析構函數是virtual時,形式就會發生變化。
欲實現virtual函數,對象必須攜帶某些信息,主要在運行期間決定哪個virtual函數被調用。這類信息通常由一個vptr虛函數表指針之處。vptr指向一個由函數指針構成的數組,稱為vtbl;每一個帶有虛函數的class都有一個相應的vtbl。
因此、無端的將所有的class的析構函數聲明為virtual,就像從未聲明它們為virtual一樣,都是錯誤的。
因為標準容器都是non-virtual的,不要試圖將其作為base-class。
別讓異常逃離析構函數
C++并不禁止析構函數吐出異常,但它不鼓勵你這樣做。
析枸函數絕對不要吐出任何異常,如果一個被析枸函數調用的函數可能拋出異常,析枸函數應該捕獲任何異常,然后吞下讓夢或結束程序
如果客戶需要對某個操作函數運行期間拋出的異常做出反應,那么class應該提供給一個普通函數執行該操作
絕對不再構造和析構過程中調用virtual函數
你不應該在構造函數和析構函數中調用virtual函數,因為這樣的調用不會帶來你預想的結果。
構造函數調用時,因為derived classes沒有初始化好,會調用base class的虛函數
析構函數調用時,一旦進入析構函數,對象中的derived classes對象便呈現出未定義值,所以C++視它們仿佛不再存在。
在構造和析構期間不要調用virtual函數,因為這類掉用,從不降低derived class
令operator=返回一個reference to *this
關于賦值,有趣的是你可以把它們寫成連鎖的形式:
int x, y, z; x = y = z = 5;同樣有趣的是,賦值采用右結合律,所以上述的連鎖賦值被解析為:
x = (y = (z = 15));為了實現連鎖賦值,賦值操作符必須返回一個reference指向操作符的左側實參
class Widget { public:Widget& operator=(const Widget*rhs) {return *this;} };在operator=中處理自我賦值
自我賦值發生在對象被賦值給自己時:
class Widget {}; Widget w; w = w; // 賦值給自己看起來有點傻,但是它是合法的,所以不要認定客戶不會這樣做,此外賦值動作并不總是那么可以被一眼辨認出來:
a[i] = a[j]; // 潛在的自我賦值一個不安全的operator=使用示例:
class BitMap {}; class Widget {private:BitMap* pb; }Widget& Widget::operator=(const Widget& rhs) {delete pb; // 停止使用當前的bitmappb = new BitMap(*rhs.pb); // 使用rhs's bitmap的副本(復件)return *this; }這里的問題是,當operator=進行自我賦值的時候,delete pb相當于把自己的pb給刪掉了
為了防止這種錯誤,傳統的做法是進行證同測試,達到自我賦值的檢驗的目的:
Widget& Widget::operator=(const Widget& rhs) {if (this == &rhs) return *this;delete pb; // 停止使用當前的bitmappb = new BitMap(*rhs.pb); // 使用rhs's bitmap的副本(復件)return *this; }swap版本的:
Widget& Widget::operator=(const Widget& rhs) {Widget temp(rhs);swap(temp);return *this; } // 或者 Widget& Widget::operator=(const Widget& rhs) {swap(rhs);return *this; }- 確保對象自我賦值時,operator=有良好的行為,其中技術包括比較來源對象和目標對象的地址、精心周到的語句順序、以及copy-and-swap
- 確定任何函數如果操作一個以上的對象,其中多個對象是同一個對象時,其行為仍然正確。
以對象管理資源
許多資源分配后用于單一的區域或者函數內,它們應該在控制流離開那個區塊或函數時被釋放。標準庫auto_ptr正是對這種形勢而設計的特制產品。auto_ptr是個類指針對象,也就是所謂智能指針,其析枸函數自動對其所指向對象調用delete
獲得資源后立即放進管理對象內,實際上以對象管理資源的觀念被稱為資源取得時機便是初始化時機(Resource Acquisitioon Is Initialzation; RAIL)
管理對象利用析枸函數確保資源被釋放
- 為防止資源泄露,請使用RAIL對象,它們在構造函數中獲得資源并在析構函數中釋放資源
在資源管理類中提供對原始資源的訪問
資源管理類很棒,它們是你對抗資源泄露的堡壘。但是這個世界并不是總是那么的完美,許多的APIs直接指涉資源,所以除非你發誓用不錄用這樣的APIs,否則就只能繞過資源管理對象直接訪問原始資源。
- APIs往往要求訪問原始資源,所以每一個RAIL Class應該提供一個取得其所管理之資源的方法
- 對原始資源的訪問可能經由顯示轉換或隱式轉換。一般而言顯式轉換比隱式轉換更加安全,但是隱式轉換對客戶來說比較方便
成對的使用new和delete時要采取相同的形式
一下動作有什么錯?
std::string* stringArray = new std::string[100];delete stringArray;每件事情開起來都是井然有序的,使用了new也搭配了對應的delete。但還是有樣東西完全錯誤,你的程序行為不明確,stringArray所包含的個string對象中的99個不太可能被適當刪除,因為他們的析構函數很可能沒有被調用。
當你使用new有兩件事情發生,第一,內存被分配出來;第二、針對此內存會有一個(或更多)構造函數被調用。當你調用delete也有兩件事情發生,針對此內存會有一個(或更多)析構函數被調用,然后內存才被釋放。delete的最大問題在于:即將刪除的內存究竟存在多少對象,這個問題的答案決定了有多少析構函數必須被調用起來。
因此、為了降低不必要的麻煩,不要對數組形式做typedef等操作
- 如果你在new中使用了[],必須在相應的delete表達式中國捏也使用[]。如果你在new表達式中沒有使用[],一定不要在相應的delete表達式中使用[]。
以獨立語句將newed對象置入智能指針
RAIL風格的代碼也不是什么地方都能使用,如有以下代碼:
processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());雖然這里借助shared_ptr實現了對象管理式資源,但是卻可能造成資源泄露
如果上述processWidget的調用按照如下順序進行:
按照上述過程調用是沒有問題的,但是C++編譯器會以什么樣的次序完成這些事情呢?答案是不一定。這正是C++區別java和C#的不同,那兩種語言總是以特定的次序完成函數參數的核算。
如果C++編譯器按照一下的順序執行:
現在你想象下,如果priority執行出現異常,會發生什么事情?在這種情況下new Widget返回的指針將會遺失,從而造成資源泄露。上述的復合語句正是造成這種資源泄露的元兇。
如果想解決這種問題,可以通過將復合語句拆分進行解決
std::tr1::shared_ptr<Widget> pw(new Widget); // 在單獨語句內以只能指針存儲 newed出來的對象 processWidget(pw, priority()); // 這個調用即使出現異常也不會造成資源泄露- 一獨立語句將newed對象存儲于(置于)智能指針內。如果不這樣做,一旦異常被拋出,有可能導致難以察覺的資源泄露。
將成員變量聲明為private
如果成員變量不是public,客戶唯一能夠訪問對象的辦法就是通過成員函數。如果public內都是成員函數,那么客戶也就不必花費時間糾結調用成員的時候是否需要加()。
使用函數可以讓你對成員變量實現更精確的控制。如果你令成員變量為public那么每個人都可以方位它,而通過函數就可以實現不準訪問、者只讀訪問、只寫訪問和讀寫訪問。
如果你通過函數訪問成員變量,日后可以更改某個計算替換這個成員變量,而class客戶一點也不會知道class的內部實現已經起了變化。
因此、一旦你將一個成員變量聲明為一個public或者protect并且客戶開始使用,那么這個成員變量的去除將會影響所有調用它的地方,所有相關的代碼文檔測試接口都將進行重寫。
- 切記將成員變量聲明為private。這可賦予客戶訪問數據的一致性、可細微劃分訪問控制、允許約束條件獲得保證,并提供class作者以充分的實現彈性。
- protect并不比public更具封裝性。
盡可能延后變量定義式的出現時間
只要你定義了一個變量而其類型帶有一個構造函數或者析構函數,那么程序的控制流到達這個變量定義式時,你就得承受構造成本,當這個變量離開其作用域時,你便得承受析構成本,即使這個變量最終并未被使用,仍需耗費這些成本,所以你應該盡可能避免這種情形。
std::string encryptPassword(const std::string& password) {using namespace std;string encrypted;// 這一旦發生異常,encrypted雖然定義并被釋放,但是卻根本沒有用到if (password.length() < MinimuPasswordLength) {throw logic_error("password is too short");}...return encrypted; }- 盡可能延后變量定義式的出現,這樣做可以增加程序的清晰度并改善程序的效率。
盡量少做轉型動作
C++除了C語言中的強制類型轉換,還新增了如下新的類型轉換:
// 將對象的常量性轉除,也就是去除const限制 const_cast<T>(expression) // 主要用于執行安全向下轉型,也就是用來決定某個對象是否歸屬繼承體系中的某個類型 // 它是唯一無法由舊式語法執行的動作,也是唯一可能耗費重大運行成本的轉型動作 dynamic_cast<T>(expression) // 意圖執行低級轉型實際動作可能取決于編譯器,這也就表示它不可移植,例如將一個pointer to int 轉型為int reinterpret_cast<T>(expression) // 用來強迫隱士轉換,例如將non-const對象轉換為const對象,或將int轉換為double等等,他也可以執行上述操作的反向轉換,例如將 // void * 指針轉換為typed指針,將pointer-to-base轉為pointer-to-derived,但它無法將const轉為non-const這個只有const_cast才辦得到 static_cast<T>(expression) class Base {}; class Derived : public Base {}; Derived d; Base* pd = &d; // 隱喻地將Derived*轉換為Base* // 加入進入一個函數,你只能拿到Base* 但是你想調用Derived的函數 // 你又不確認傳進來的是否是 Derived的對象指針,這個時候可以使用dynamic_cast // 如: if (Derived *pDerived = dynamic_cast<Derived*>pd)這里我們不過是建立一個base class指針指向一個derived class對象,但有時候上述兩個指針的值并不 相同。這種情況下會有一個偏移量(offset)在運行期間被施于Derived*指針上,用意取得正確的Base*指針值。
以上例子說明,單一對象可能擁有一個以上的地址,這種現象C不可能發生,java和C#也不可能發生這種事,但是C++可能!實際上一旦使用多重繼承,這種事幾乎一直發生著,即使單一繼承中也可能發生。雖然這還有其他意涵,但是至少意味著你通常應該避免做出對象在C++中如何如何布局的假設。當然更不應該以此為假設的基礎上執行任何轉型動作。
因此、依賴對象布局方式濟南西給你的地址設計方式轉型,在有的編譯器上行得通,在其他平臺可能就行不通了。
- 如果可以,盡量避免轉型,特別是在注重效率的代碼中避免dynamic_cast,如果有個設計需要轉型操作,試著發展無需轉型的替代設計
- 如果轉型是必須的,試著將它隱藏于某個函數背后。客戶隨后可以調用該函數,而不需將轉型放進它們自己的代碼內
- 寧可使用C++新式風格的轉型,不要使用舊式的轉型。前者容易辨認出來。
總結
以上是生活随笔為你收集整理的EffectiveC++编程的50个建议的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 作者:钱卫宁,华东师范大学数据科学与工程
- 下一篇: 一文读懂什么是C++移动语义《一》