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