本文為學(xué)習(xí)《Effective C++》各個(gè)條款之后的一點(diǎn)概要式的總結(jié)。
github博客地址
條款2 盡量以const, enum, inline替代#define
- 寧可用編譯器替代預(yù)處理器。以#define定義的記號是不會記錄到符號表中的;
- #define沒有封裝性可言。
- enum hack。
enum {tmp=5};對應(yīng)的tmp一定在編譯期就可以得到并且不會導(dǎo)致非必要的內(nèi)存分配。
條款3 盡可能使用const
- 調(diào)用const成員函數(shù)以實(shí)現(xiàn)孿生non-const成員函數(shù)。通過使用
const_cast和static_cast來達(dá)到目的,優(yōu)點(diǎn)是避免了代碼重復(fù)。 - 調(diào)用non-const成員函數(shù)實(shí)現(xiàn)const成員函數(shù)是錯誤的。因?yàn)檫@破壞了const的語義約束。
條款5 了解C++默認(rèn)編寫并調(diào)用哪些函數(shù)
- 如果自定義了需要實(shí)參的構(gòu)造函數(shù),則編譯器不會自動生成default ctor
- 如果class內(nèi)部包含有帶有
&引用類型或者const常量類型,則編譯器不會自動生成copy assignment;因?yàn)榫幾g器不知道該怎么處理
條款7 為多態(tài)基類聲明virtual析構(gòu)函數(shù)
- 每一個(gè)帶有virtual函數(shù)的class都擁有一個(gè)指向virtual table的指針,virtual table中包含了所有對應(yīng)virtual函數(shù)的函數(shù)指針
- 不要嘗試?yán)^承任何標(biāo)準(zhǔn)庫容器(比如
std::string),因?yàn)樗鼈兌紱]有virtual dtor。這會導(dǎo)致未定義行為 - 沒有多態(tài)性質(zhì)的base class也不要聲明virtual dtor,比如說
boost::noncopyable,virtual并無必要,且浪費(fèi)空間的 - 如果明確了一個(gè)類具有多態(tài)性質(zhì),且作為base class使用,則應(yīng)該聲明virtual dtor
條款8 別讓異常逃離析構(gòu)函數(shù)
- 絕對不能讓dtor吐出異常,因?yàn)楹芸赡軙斐少Y源泄露。對于有可能在dtor中發(fā)生的異常,應(yīng)該將其吞下或者提前終止程序
- 更合適的做法是為客戶代碼提供一個(gè)接口,使得客戶有機(jī)會去處理可能發(fā)生的異常
條款11 在operator=中處理“自我賦值”
核心其實(shí)就是不能讓指針指向一個(gè)未獲取的資源;存在3類方法,各有各的優(yōu)勢
- 賦值之前先比較lhs和rhs的地址是否相同,如果相同,則直接返回;
- 先記住之前本身的資源(可以設(shè)一個(gè)
pOrigin指針指向舊資源),隨后拷貝一份rhs的資源,并令lhs指向新資源,最后再釋放掉lhs的舊資源(即delete pOrigin)(這其實(shí)就是copy and swap的步驟...); - copy and swap。先拷貝rhs指向的資源,再令lhs指向的資源和這份拷貝之后的資源進(jìn)行交換;
條款12 復(fù)制對象是勿忘其每一個(gè)成分
- 自行編寫copy ctor或者operator=是一項(xiàng)重大的責(zé)任,因?yàn)橐紤]到各種細(xì)節(jié)。而也正是因?yàn)檫@樣的原因,當(dāng)自行編寫時(shí),編譯器會認(rèn)定你是一個(gè)足夠強(qiáng)大的程序員,因此不會對自定義copy ctor和operator=的不好的地方做出任何警告;
- 確保每一個(gè)成員變量都被正確拷貝;
- 當(dāng)目標(biāo)是derived class時(shí),其base class的成員變量也要被正確拷貝。這需要通過調(diào)用base class的copy ctor和operator=來實(shí)現(xiàn);
- 切記,copy ctor和operator=不能相互調(diào)用。這從語義上就行不通
條款14 在資源管理類中小心copying行為
對RAII對象執(zhí)行復(fù)制,是需要萬分小心的行為,因?yàn)樗婕暗降馁Y源的最佳處理方式不甚相同;常見的方式包括:
- 禁止復(fù)制。很多情況下這是比較科學(xué)的做法,因?yàn)樾袨楸憩F(xiàn)的像指針這樣的數(shù)據(jù)類型是不應(yīng)該重復(fù)進(jìn)行delete的;如果不禁止復(fù)制,則必須做到對指涉到的資源也進(jìn)行復(fù)制;
- 引用計(jì)數(shù)。不多說了,就是智能指針那一套;
條款15 在資源管理類中提供對原始資源的訪問
- 諸如
std::shared_ptr和std::unique_ptr都會提供get()成員函數(shù)來訪問其指涉的底層資源;這不是破壞封裝性,而僅僅是一種接口風(fēng)格; - 訪問底層資源的接口,一般而言就兩種:①
get()這樣的成員函數(shù),②隱式轉(zhuǎn)換。一般來說還是①更好一點(diǎn),因?yàn)楦踩?/li>
條款16 以獨(dú)立語句將newed對象置入智能指針
- 本條款在《Effective Modern C++》中也有講述;
- 核心的一點(diǎn)就是在單條語句內(nèi),編譯器是有著重新編排執(zhí)行順序的自由的;
- 因此,諸如
std::shared_ptr<XXX> sp(new XXX);這樣的語句應(yīng)該單獨(dú)成句,而不應(yīng)該嵌入到其他語句中; - 其實(shí)現(xiàn)代C++的話,更好的做法是使用
std::make_shared或者std::make_unique;它們使用完美轉(zhuǎn)發(fā),且很安全;
條款19 設(shè)計(jì)class猶如設(shè)計(jì)type
不多說了,在編寫類代碼的時(shí)候多看看本條款,思考條款中列出的問題;
條款23 寧以non-member、non-friend替換member 函數(shù)
- 要理解這個(gè)條款,就得明確namespace的作用:①可以跨越多個(gè)源碼文件;②在實(shí)現(xiàn)類似于utility所提供的功能時(shí),更具有優(yōu)勢(因?yàn)檎Z義更清晰);③在提供了所需功能基礎(chǔ)上達(dá)到編譯依賴最低,封裝性最好
- 書中所舉的例子:任務(wù)是調(diào)用class中的三個(gè)成員函數(shù)。那么方法大致為兩種:①再寫一個(gè)成員函數(shù),內(nèi)容就是調(diào)用那三個(gè)函數(shù);②將新的函數(shù)放在class的外部(非成員函數(shù)),但位于同一個(gè)namespace中;
- 基于上面所陳述的原因,使用第二個(gè)方法是更好的方式
條款24 若所有參數(shù)皆需要類型轉(zhuǎn)換,請為此采用non-member函數(shù)
- member函數(shù)的反面是non-member,而不是friend;friend在OOP中能避免則避免,因?yàn)樘茐姆庋b性了
- 只有當(dāng)參數(shù)被置于參數(shù)列時(shí),這個(gè)參數(shù)才是隱式類型轉(zhuǎn)換的合格參與者;也就是說,當(dāng)調(diào)用成員函數(shù)時(shí),lhs實(shí)際上沒有被置于參數(shù)列中,而是this
條款26 盡可能延后變量定義式的出現(xiàn)時(shí)間
- 應(yīng)該盡可能在要用到某個(gè)變量的時(shí)候才去定義它(這很顯然嘛)
- 關(guān)于循環(huán)體中的變量的下述兩種定義方式,一般情況下,除非明確知道賦值操作的消耗小于構(gòu)造加析構(gòu)的時(shí)候才使用第一種;因?yàn)榈谝环N方式擴(kuò)大了變量的生命期;
// 第一種
{
...
Weight tmp;
for(int i = 0; i < N; ++i){
tmp = Weight(i);
}
}
// 第二種
{
for(int i = 0; i < N; ++i){
Weight tmp= Weight(i);
...
}
}
條款29 為“異常安全”而努力是值得的
- 所謂的異常安全函數(shù),其實(shí)就是發(fā)生異常也不會導(dǎo)致資源泄露和數(shù)據(jù)敗壞;包括三類:
- 基本保證:如果函數(shù)發(fā)生異常,則對應(yīng)的對象不一定還能還原為調(diào)用前的狀態(tài),但至少保證還是正??捎?/strong>的;
- 強(qiáng)烈保證:即使函數(shù)發(fā)生異常,對象還是能夠還原為原來的狀態(tài),即只有兩種狀態(tài):成功調(diào)用和不調(diào)用;這通常通過copy-and-swap來實(shí)現(xiàn),即先將原來的對象復(fù)制一個(gè)副本,隨后對副本執(zhí)行相應(yīng)的改變,如果執(zhí)行成功,則原對象和副本執(zhí)行swap;如果發(fā)生異常,原對象也未發(fā)生任何改變
- 不拋擲(nothrow)保證:即保證函數(shù)不發(fā)生異常;這通常辦不到。。。只要涉及到了動態(tài)內(nèi)存的分配,都是有可能發(fā)生異常的
- 可以看出,級別越高,其實(shí)實(shí)現(xiàn)是越困難的,并且?guī)淼拈_銷也會越高;因此,應(yīng)該挑選的是現(xiàn)實(shí)可實(shí)施下的最高等級
- 異常安全性是遵循木桶原理的,只要函數(shù)調(diào)用了等級較低的函數(shù),那么它的異常安全性也會降低
條款30 透徹了解inlining的里里外外
-
inline在大多數(shù)C++程序中都是編譯期行為; -
inline僅僅是一個(gè)申請,并不保證一定會內(nèi)聯(lián); - 是否真正內(nèi)聯(lián)還取決于函數(shù)的調(diào)用方式;(如果以函數(shù)指針進(jìn)行調(diào)用,那么就不可能被內(nèi)聯(lián)了);
-
inline的優(yōu)勢是避免調(diào)用開銷,但也存在以下問題:- 代碼膨脹:畢竟,如果在多處都調(diào)用了該函數(shù),那么就會有多份該函數(shù)體的副本;
- 編譯依賴:如果
inline函數(shù)發(fā)生了改變,那么所有客戶代碼都必須重新編譯;反之,如果不是內(nèi)聯(lián)的,那么僅僅重新鏈接一下就行
條款31 將文件間的編譯依存關(guān)系降至最低
C++中降低文件間的編譯依賴,主要就是兩種手段:handle class以及interface class
- 如果客戶代碼所使用的的頭文件中,直接包含的是要使用的class的具體實(shí)現(xiàn)(包括各個(gè)函數(shù)定義),那么就形成了依賴關(guān)系;
- 所謂依賴關(guān)系,就是指,只要一個(gè)class改變了一點(diǎn)點(diǎn)實(shí)現(xiàn),那么所有使用它的客戶代碼都需要重新編譯;
-
handle class:
- 所謂的handle class,實(shí)際上意味著一個(gè)負(fù)責(zé)聲明的class和一個(gè)負(fù)責(zé)具體實(shí)現(xiàn)的class(假設(shè)為
class Widget和class WidgetImpl);兩者的接口全部一致,而客戶代碼使用的是class Widget - class Widget中不對任何方法進(jìn)行具體實(shí)現(xiàn),只聲明類接口;且涉及到非基本類型的自定義類型成員變量(比如此處的
class WidgetImpl),都使用前置聲明和(智能)指針來進(jìn)行指涉; - 標(biāo)準(zhǔn)庫組件無需也不應(yīng)該被前置聲明;直接
#include就行; - 這樣一來,class Widget的頭文件中不會
#include任何其他的頭文件(除了標(biāo)準(zhǔn)庫);而這,也就杜絕了客戶代碼對除了class Widget頭文件之外的文件產(chǎn)生任何依賴; - 至于class Widget的接口實(shí)現(xiàn),則在其.cpp文件中去
#include "WidgetImpl",然后調(diào)用class WidgetImpl的接口即可;
- 所謂的handle class,實(shí)際上意味著一個(gè)負(fù)責(zé)聲明的class和一個(gè)負(fù)責(zé)具體實(shí)現(xiàn)的class(假設(shè)為
-
interface class
- 即類似于Java中的interface,不過實(shí)現(xiàn)方式是定義成虛基類;面向派生譜系的多態(tài)技術(shù);
條款33 避免遮掩繼承而來的名稱
- C++應(yīng)對派生譜系中的函數(shù)調(diào)用,歸根結(jié)底就是以名稱為準(zhǔn)進(jìn)行匹配;
- 無論是變量還是函數(shù),是重載還是重寫,是否是虛函數(shù),甚至也無論函數(shù)的參數(shù)列表是什么形式,都沒有任何關(guān)系;編譯器只要在當(dāng)前的域中找到了對應(yīng)的名稱,就直接結(jié)束匹配;
- 這意味著:如果base class中定義了一組重載函數(shù),而后又在derived class中定義了一個(gè)同名的函數(shù),那么當(dāng)用derived class類型(或引用、指針)來調(diào)用這個(gè)名稱的函數(shù)時(shí),基類的重載函數(shù)統(tǒng)統(tǒng)被覆蓋;
- 克服這個(gè)問題的方法:在派生類中加入using聲明:
class Base{
public:
// 重載函數(shù)
void f(int);
void f();
};
class Derived : public Base {
public:
using Base::f; // OK,基類的重載函數(shù)不會被覆蓋了
void f(int, int);
};
- 如何實(shí)現(xiàn)僅繼承部分基類接口?很簡單,使用private繼承+轉(zhuǎn)接函數(shù);
- 所謂的轉(zhuǎn)接函數(shù)就是派生類中的公共接口,但這些公共接口只是去調(diào)用基類的函數(shù);
- 基類因?yàn)楸籶rivate繼承了,所以其所有接口也就被隱藏了;
條款34 區(qū)分接口繼承和實(shí)現(xiàn)繼承
- 在繼承譜系中,虛函數(shù),純虛函數(shù),普通函數(shù)之間的根本區(qū)別就是對待接口繼承和實(shí)現(xiàn)繼承的方式不同;
- 純虛函數(shù):只繼承接口;
- 虛函數(shù):繼承接口和一份缺省實(shí)現(xiàn)
- 普通函數(shù):繼承接口和一份強(qiáng)制實(shí)現(xiàn)======》
- 這意味著任何derived class都不應(yīng)該重新定義base class中的普通函數(shù);
- 條款36就是在陳述這一點(diǎn);本質(zhì)上就是因?yàn)槠胀ê瘮?shù)實(shí)施的是靜態(tài)綁定,相同的對象會因?yàn)槠渲羔樆蛞玫念愋偷牟煌鴪?zhí)行不同的函數(shù)體(有可能是基類的函數(shù)體,也可能是派生類的函數(shù)體);這造成了不確定性(另一方面,單個(gè)基類指針,即使指向不同類型的派生類,其調(diào)用普通函數(shù)時(shí),也只會執(zhí)行基類函數(shù)體,造成了程序錯誤);
- 注:C++的虛函數(shù)模型在二進(jìn)制兼容性(ABI)方面的負(fù)面影響是極大的。如果一個(gè)程序會設(shè)計(jì)為一個(gè)動態(tài)庫,客戶代碼對其進(jìn)行加載調(diào)用,如果后續(xù)動態(tài)庫進(jìn)行了升級,在某個(gè)類中加入了新的虛函數(shù),那么如果客戶代碼不重新編譯的話,會直接調(diào)用不同的函數(shù),造成錯誤,因?yàn)榭蛻舸a在編譯結(jié)束以后,就直接以虛表指針加偏移的形式去調(diào)用函數(shù),而動態(tài)庫的各個(gè)函數(shù)的偏移可能在升級之后就完全改變了。
條款37 絕不重新定義繼承而來的缺省參數(shù)值
- 雖然虛函數(shù)實(shí)行的是動態(tài)綁定,但虛函數(shù)(實(shí)際上是任何函數(shù))中的參數(shù)缺省值卻是靜態(tài)綁定的;
- 這意味著函數(shù)的參數(shù)缺省值不應(yīng)該被重新定義;理由還是一樣的,這會因?yàn)橹羔樆蛞玫念愋筒煌斐刹淮_定性;
- 如果需要為虛函數(shù)定義參數(shù)缺省值,則更好的做法是:
- 定義一個(gè)普通函數(shù),有缺省值;
- 實(shí)際的虛函數(shù)變?yōu)閜rivate,且無缺省值;
- 使用普通函數(shù)去調(diào)用虛函數(shù);
- 這樣就避免了代碼在派生譜系中的依賴性;
條款38 通過復(fù)合塑膜出has-a或“根據(jù)某物實(shí)現(xiàn)”
- 關(guān)鍵就是理解復(fù)合(Composition)二字;復(fù)合包含應(yīng)用域和實(shí)現(xiàn)域兩種關(guān)系;
- 應(yīng)用域:即把一個(gè)class作為組件;比如說
class People的一個(gè)組件是class PhoneNumber;這就是所謂的has-a關(guān)系; - 實(shí)現(xiàn)域:即某個(gè)class需要通過另一個(gè)class進(jìn)行實(shí)現(xiàn),但兩者并不存在完美的繼承關(guān)系;比如說通過一個(gè)
std::vector<int>來實(shí)現(xiàn)一個(gè)class Stack<int>;這就是所謂的Is-implemented-int-terms-of關(guān)系
條款39 明智而審慎地使用private繼承
- private繼承并不具備“軟件設(shè)計(jì)”層面的意義,其僅僅是一種“軟件實(shí)現(xiàn)”的技術(shù);
- 條款38中已經(jīng)闡述過"Is-implemented-in-terms-of"關(guān)系,事實(shí)上,private繼承也是這種意義;
- “private繼承”和“復(fù)合”的區(qū)別就在于:
- 一般情況下,能使用復(fù)合就使用復(fù)合;
- 只有當(dāng)明確是Is-implemented-in-terms-of關(guān)系的同時(shí),需要重寫基類的虛函數(shù)或者訪問protect變量時(shí),才使用private繼承;因?yàn)檫@是復(fù)合無法做到的;
- EBO(empty-base-optimization):C++中一個(gè)空類的size不等于0,而是1;而繼承一個(gè)空類不會加大size;這就是private的另一個(gè)優(yōu)勢;
條款40 明智而審慎地使用多重繼承
- 總的來說,多重繼承還是有用的,但卻是也存在很多的限制;
- 條款中所涉及的“虛繼承”概念是比較重要的:
- 多重繼承很可能會發(fā)生所謂的菱形繼承:即某一個(gè)基類和某一個(gè)派生類之間存在多條繼承路徑;
- 如果使用非虛繼承的話,派生類將會保存同一個(gè)基類的多個(gè)副本;但實(shí)際上一份副本就足夠了;這造成了空間浪費(fèi);更糟糕的則是因?yàn)槎喾莞北緦?dǎo)致的命名沖突;
- 虛繼承是解決這個(gè)問題的唯一方法;它使得派生類可以只保留基類的一份副本;
- 但虛繼承也有自己的缺點(diǎn):最突出的就是加大了運(yùn)行時(shí)消耗;因?yàn)椴扇√摾^承的話,class的size和內(nèi)存模型就只能在運(yùn)行期才能知曉了;(C++中虛函數(shù)、虛繼承內(nèi)存模型 - 知乎 (zhihu.com))
條款41 了解隱式接口和編譯器多態(tài)
- 基于模板的泛型編程其實(shí)也隱含著“接口”的概念,但是是隱式的。這和派生譜系中的接口機(jī)制有很大不同;
- 隱式接口是基于:必須滿足模板代碼中隱含的一組約束。比如書中給出的例子:
if(w.size() > 10 && w != someNastyWidget){...},w的類型為typename T,那么就必須滿足:if中給出的表達(dá)式能夠轉(zhuǎn)換為bool類型。 - 所謂的編譯器多態(tài)就是:編譯器根據(jù)隱式接口去決定需要(生成)調(diào)用哪一個(gè)重載函數(shù)以及具現(xiàn)化模板。
條款42 了解typename的雙重意義
- 當(dāng)用于模板參數(shù)的時(shí)候,typaname和class沒有區(qū)別;
- 如果某個(gè)名稱是嵌套從屬名稱(nested-dependent-names),即它的性質(zhì)(是變量名還是類型名)需要由模板參數(shù)來決定,那么如果它確實(shí)是一個(gè)類型名的話,就需要加上
typename;(因?yàn)榫幾g器不知道它到底是什么東西); - 萃取器:即traits,通過模板以及模板偏特化技術(shù),將傳遞進(jìn)去的類型的一些相關(guān)特征給萃取出來。比如說
typename std::iterator_traits<iteT>::value_type表示的就是iteT類型的迭代器所指涉的元素類型;萃取器的優(yōu)勢在于任何類型的迭代器(甚至是原生指針)都能萃取出想要的特征;
條款43 學(xué)習(xí)處理模板化基類的名稱
- 模板化基類(templatized-base-class):也就是說繼承來的基類是一個(gè)模板,其具體是哪一個(gè)類暫時(shí)無法確定;
- 當(dāng)模板化類繼承自一個(gè)模板化基類時(shí),編譯器就默認(rèn)基類中的所有名稱是無法得知的;除非顯式指出;
- 編譯器之所以這樣做,是因?yàn)橛捎?strong>模板偏特化以及全特化的存在,使得模板化基類不一定會擁有模板中所寫的所用名稱;
- 顯示指出的方法有3類:使用
this->name;使用using BaseClass<T>::name;;顯式調(diào)用BaseClass<T>::name;其中,第3種方法會喪失動態(tài)綁定特性,因此不是很推薦;
條款44 將與參數(shù)無關(guān)的代碼抽離templates
- 如果模板類中的某些函數(shù)與模板參數(shù)沒有關(guān)系,那么多個(gè)具現(xiàn)化的實(shí)體類則會擁有相同的函數(shù)體,這無疑使得目標(biāo)碼變得冗余;
- 更好的做法是將這些與模板參數(shù)無關(guān)的代碼抽離出來,變成基類代碼或者其他,然后不同的模板的具現(xiàn)化class去共同調(diào)用這些相同的代碼(此時(shí)這些代碼就只有一份實(shí)體了);
- 當(dāng)然,這樣也會存在一定問題。簡而言之,誰好誰壞,還是得由具體的運(yùn)行環(huán)境去決定;
條款45 運(yùn)用成員函數(shù)模板接受所有兼容類型
比如對于如下的一個(gè)模板類,很多時(shí)候,我們可能需要使用TmpDemo<int>去初始化一個(gè)tmpDemo<double>對象。這完全是合理的,但問題是,在模板編程的世界里,TmpDemo<int>和TmpDemo<double>是完全沒有任何關(guān)系的?;蛘呖梢灾苯釉谀0孱愔卸x這樣一個(gè)構(gòu)造函數(shù),但如果遭遇了其他的需求呢?比如說int變?yōu)榱薱har,又或者,現(xiàn)在的typename是一個(gè)繼承譜系中的各種類型。顯然,單一的成員函數(shù)是解決不了問題的。
template <typename T>
class TmpDemo{
// ...
};
- 成員模板函數(shù)是解決這個(gè)問題的唯一方法;在成員函數(shù)中再聲明typename,來讓編譯器來處理各種需求;
- 泛化構(gòu)造函數(shù)是成員模板函數(shù)的一種,它解決的是通過
TmpDemo<U>來初始化TmpDemo<T>的問題; - 即使聲明了泛化構(gòu)造函數(shù),也還是要去自定義拷貝構(gòu)造函數(shù),這一點(diǎn)需要注意;
條款46 需要類型轉(zhuǎn)換時(shí)請為模板定義非成員函數(shù)
- 該條款和條款24的思想是一致的,也就是當(dāng)函數(shù)的所有參數(shù)都涉及隱式轉(zhuǎn)換時(shí),它最好是一個(gè)非成員函數(shù)(因?yàn)閠his是無法轉(zhuǎn)換的);
- 和條款24的不同之處在于,本條款涉及到的是模板類;即,某個(gè)函數(shù)的各個(gè)參數(shù)是模板類型;
- 很顯然,這種函數(shù)也需要定義為非成員函數(shù);
- 不同之處在于:因?yàn)樯婕暗搅四0?,那么在進(jìn)行函數(shù)模板的模板參數(shù)推導(dǎo)時(shí),絕對無法進(jìn)行隱式轉(zhuǎn)換,比如說對于如下的代碼,直接調(diào)用
int ans = addFunc(tmp, 3);是無法通過編譯的,因?yàn)檫@涉及到了從3到TmpDemo<T>(3)的隱式轉(zhuǎn)換;而這在函數(shù)模板參數(shù)推導(dǎo)中是絕對禁止的;
template <typename T>
class TmpDemo{
public:
TmpDemo(const T& num){value = num;}
private:
T num;
};
template <typename T>
const T addFunc(const TmpDemo<T> &t1, const TmpDemo<T> &t2){
return t1.num * t2.num;
}
TmpDemo<int> tmp(2);
- 解決方法就是把非成員函數(shù)定義在模板類的內(nèi)部,并聲明為friend。因?yàn)槟0孱悤ypename信息進(jìn)行硬編碼,就可以直接進(jìn)行轉(zhuǎn)換了。
條款49 了解new-handler的行為
- new-handler:一個(gè)函數(shù)指針類型
typedef void (*new_handler) ( );,并對應(yīng)一個(gè)global的函數(shù)指針,由用戶通過new_handler std::set_new_handler(new_handler p)填充其值(可能會有系統(tǒng)默認(rèn)值);當(dāng)new無法分配出足夠的空間時(shí),系統(tǒng)就會在拋出異常之前先調(diào)用這個(gè)函數(shù); - 通常情況下,擁有以下幾種行為的new-handler是更好的:
- ①可以使得下一次調(diào)用new時(shí)有更大概率成功;這可以通過預(yù)先分配一塊大內(nèi)存,隨后每次調(diào)用new-handler時(shí)歸還部分內(nèi)存;
- ②安裝其他new-handler和卸載本地的new-handler:各個(gè)class有可能會定義自己的new-handler,因此最好的做法是new不同的class的時(shí)候,調(diào)用各自不同的new-handler,并在調(diào)用完畢后將new-handler進(jìn)行恢復(fù);
- ③拋出
std::bad_alloc或者直接退出exit()或std::abort()
- 如何實(shí)現(xiàn)方式②?答:自定義
operator new以及使用基于CRTP(curiously recursive template pattern)的模板技術(shù)- 為一個(gè)需要設(shè)置new-handler的class自定義一個(gè)operator new和set_new_handler,而在operator new內(nèi)部的流程就是:先調(diào)用
std::set_new_handler設(shè)置自己的new-handler,隨后調(diào)用系統(tǒng)的new,再之后就是恢復(fù)new-handler到系統(tǒng)原本的值了; - 由于設(shè)置和恢復(fù)完全適配于一個(gè)RAII,因此更優(yōu)秀的做法便是再設(shè)置一個(gè)資源管理類,在構(gòu)造函數(shù)內(nèi)保存之前的new-handler,并在析構(gòu)函數(shù)內(nèi)恢復(fù)之前的new-handler;
- 接下來就是考慮這樣一個(gè)問題了,如果不同的class都需要自定義new-handler的話,而又由于自定義new-handler其實(shí)是一套完全一致的流程,除了各自的new-handler不一樣;因此CRTP就派上用場了,以下代碼就是完整的實(shí)例。
- 為一個(gè)需要設(shè)置new-handler的class自定義一個(gè)operator new和set_new_handler,而在operator new內(nèi)部的流程就是:先調(diào)用
class HandleHolder{
public:
HandleHolder(const HandleHoldr &) = delete; // 禁止拷貝
HandleHolder &operator=(const HandleHolder &) = delete;
HandleHolder(std::new_handler p): oldHandler(p) {}
~HandleHolder(){std::set_new_handler(oldHandler);}
private:
std::new_handler oldHandler;
};
template <typename T>
class NewHandlerHelper{ // 此處沒有定義自己的set_new_handler了,感覺沒有必要
public:
NewhandlerHelper(std::new_handler p): myHandler(p) {}
static void *operator new(size_t size) throw(std::bad_alloc){ // 每個(gè)class對應(yīng)一個(gè)operator new
HandleHolder tmp(std::set_new_handler(myHandler)); // std::set_new_handler會返回之前的new-handler
return ::operator new(size);
// tmp被析構(gòu),new-handler也就得以恢復(fù)
}
private:
static std::new_handler myHandler; // 每個(gè)class對應(yīng)一個(gè)new_handler
};
template <typename T>
std::new_handler NewHandlerHelper<T>::myHandler = nullptr; // static變量要記得初始化
class Widget : public NewHandlerHelper<Widget> { // 自己繼承自己,雖然看起來很奇怪,但實(shí)際上是行得通的;本質(zhì)上只是讓不同的class擁有不同的myHandler
/**
* ...
* Widge只要在構(gòu)造函數(shù)處給NewHandlerHelper提供自己的new-handler即可
* ...
*/
};
條款52 寫了placement new也要寫placement delete
- 當(dāng)代碼中使用
new表達(dá)式之后,發(fā)生了兩件事情:- ①調(diào)用
void *operator new(size_t size)來獲取一塊原始內(nèi)存(raw memory); - ②調(diào)用class的ctor以構(gòu)造對應(yīng)的對象
- ①調(diào)用
- 因?yàn)橛袃蓚€(gè)步驟的存在,因此,如果在第2個(gè)階段發(fā)生了異常,就有可能產(chǎn)生內(nèi)存泄漏;
- 為了避免可能的內(nèi)存泄漏,當(dāng)發(fā)生上述情況時(shí),由系統(tǒng)來負(fù)責(zé)回收對應(yīng)的內(nèi)存;
- 這就引出了一個(gè)問題,系統(tǒng)如何知道應(yīng)該調(diào)用哪一個(gè)版本的delete呢?系統(tǒng)的原則是,使用和
operator new的參數(shù)列表一致的operator delete - 因此就有了本條條款的原則:定義了一個(gè)placement new,就需要定義對應(yīng)的placement delete;所謂的placement就是參數(shù)列表除了
size_t以外還包括其他的參數(shù);