基礎議題:指針、引用、類型轉換、arrays、constructors
條款1:仔細區分指針和引用
- 引用在某種程度上相當于常量指針,因為其必須給定初始化值,并不能改變指向,所以設為nullptr自然就沒有意義了
- 在使用語法上引用和原始類型保持一致,而指針使用去引用符*和-->來分別獲取原始對象和成員
- 值得注意的是被引用的對象(包括簡單類型)必須是左值(?)
- 沒有null reference意味著references可能更有效率,因為不需要測試其有效性
當你考慮不指向任何對象或者在不同時間內指向不同對象的可能性時使用指針,而當你確定總是會代表某個確切對象,并且不能再改變指向,則使用引用
另外引用在函數傳較大對象的過程中使用較多,在必須返回某種能夠作為左值時使
用引用更加清晰,如數組的[]操作符
條款2:最好使用c++轉型操作符、
- 舊時轉型使用括號語法,難以辨識具體轉型類型
- c++中引入了static_cast、const_cast、dynamic_cast、reinterperet_cast;
- static_cast和c舊式轉型基本一致,改變某個變量的靜態類型
- const_cast改變constness和volatileness
- dynamic_cast:只能用于集成體系中,將指向基類的指針或者引用轉化為指向繼承類的指針或者引用,注意改變的是靜態類型,如果相應的靜態類型和動態類型不一致可能會產生不一致的效果(?);另外一個用途在條款27中講述
- reinterperet_cast,這個操作符基本和編譯平臺有關,所以一般不具有移植性;一般用于函數指針的轉型操作
tips:使用新式轉型的好處:
- 嚴謹語義和易辨識度
- 編譯器容易做轉型錯誤
- 讓轉型變得丑陋未嘗不是一件好事
條款3:絕對不要以多態的方式處理數組
- 繼承最重要的性質:多態(運行時多態),也就是用基類指針和引用去處理派生類對象
- 如果對數組使用多態,由于數組的引用是一個叫做“指針算術表達式”的東西,每一次對其中元素的引用都必須按照指定類型大小來改變當前指針的位置,這就牽扯到了如何確定這個大小,而對于編譯器而言這個大小就是指針靜態類型的大小,而實際存儲的確實派生類對象,這就會造成溢出
- 多態和指針算術不能混用,所以數組和多態不能混用
在條款33中,提出“具體類不要繼承自另外一個具體類”
條款4:非必要不提供default constructor
- 含義:使得類沒有任何信息提供就能初始化
- 不使用默認構造函數的影響:1.無法定義對象數組;2.使用指針數組,但是內存成本較高;3使用placement new預分配空間,操作不便;4. 不適合template-based container classes,比如容器模型是建立的對象數組之上的;5. 虛基類的初始化會因為沒有默認構造函數而一層一層傳遞初值,很繁瑣
- 但是我們任然有足夠的理由在不必要的時候不使用它,因為它會使沒有初值的對象存在進一步我們就需要在其他成員函數中測試當前對象是否有意義,這提高了程序的運行成本。而如果不使用默認構造函數,則程序的運行在預期之中,實現上就更加有效率了。
操作符
條款5:對定制的類型轉換函數保持警覺
- 允許編譯器執行隱式類型轉換的害處大于好處。所以不要提供他們,除非確實需要
- 沒有一個轉換程序能夠內含一個以上的“用戶定制行為”
條款6:區分前置式后置式的++,--
- 前置式(++k)返回一個引用,對調用對象進行自增后返回,返回的是傳入的對象,所以用引用;后置式(k++)返回一個const 對象,后置式是改變對象的內容并返回原有對象的內容,此時返回的是一個局部環境下構建的新對象,所以要返回對象。const的限制是避免對象本身被進一步修改。
- 因為上面的構造方法,我們一般認為前置式效率更佳
- 為了防止前置式和后置式在加法操作上的不一致,我們的原則一般是:后置式操作符的實現以其前置式為基礎,如此一來便能夠保證一致性行為。
條款7:千萬不要重載&&、||和,操作符
- c++對于真假值表達式采取驟死式的評估方式,因此重載比較運算符可能從根本上改變程序的行為,破壞這一規則;
- 另外,函數調用的語義和所謂的驟死式語義有著重大的區別。第一當函數調用動作發生時,所有參數的評估工作已經完成,并且評估的順序沒有被明確規定。
- 逗號操作符擁有其內建類型的行為:表達式如果包含逗號,那么逗號左側會被優先評估;整個表達式的值以右邊的值為代表。而這些行為在你重載之后都會發生改變。
- 將操作符重載函數作為non-member函數將會導致無法確定左右的評估順序。member函數也有同樣的問題
條款8:了解各種意義的new和delete
- new expression:內建的操作符,主要干兩件事,1.分配足夠的內存;2.調用constructor構造對象,初始化。
- 函數void* operator new(size_t size):分配內存,可以重載的函數;
- placement new:在已經分配好的內存上面構建對象,語法上類似帶有兩個參數new operator,new (void*) xxxx(size);頭文件include <new>
- delete用法和new一樣,注意如果使用placement new,應該避免對那塊內存使用delete operator。
異常
條款9:利用destructors避免內存泄露
- 將資源封裝在對象內部,避免因為exception的出現而造成資源泄露
條款10:在constructor中避免內存泄漏
- c++只會析構已經構造完成的對象
- const 成員變量只能通過初始化列表進行初始化
- 如果你以auto_ptr對象取代pointer class members,你便是對你的cosntructors做了強化,免除了資源泄露,再需要destructors內親手釋放那個資源,并允許const members pointers得以和non-const members pointers有著一樣優雅的處理方式。
條款11:禁止異常流出destructors之外
- 這樣做可以避免terminate函數在exception傳播過程中的棧展開(stack-unwinding)機制中被調用;它可以協助確保destructors完成其應該完成的所有事情
條款12:了解“拋出一個異常”和“傳遞一個參數”或“調用一個虛函數”之間的區別
- exceptions總是會被復制(包括以by reference-to-non-const,但是指針不會),如果以by value方式被捕捉,他們甚至會被復制兩次。至于傳遞給函數參數的對象不一定得復制。
- “被拋出異常”的對象,其被允許類型轉換動作,比“被傳遞到函數去”的對象少
- catch子句以其“出現于源代碼的順序”被編譯器檢驗對比,其中第一個匹配成功者便執行;而當我們以某對象調用一個虛函數,被選中執行的是那個“與對象最佳吻合”的函數,不論他是不是源代碼所列的第一個。
條款13:以by reference方式捕捉exceptions
- by pointer會出現拋出異常時,離開當前執行域時會發生對象刪除問題
- by value會復制對象兩次,會出現切割問題,無法調用派生類的虛函數
- 使用by reference可以有效解決上述問題,所以盡量使用引用
條款14:明智的使用exception specifications
- 語法:void myfunc() throw();只捕捉指定類型的異常,如果函數拋出一個未列出的異常,unexpected會被調用;
- unexpected默認會導致程序abort,大量資源可能泄露
- 局部性檢驗,只對當前函數執行拋出的異常進行檢驗。所以不應該將template和其混用
- 在回調函數中,極易可能與主調函數的異常具體化沖突,如果前者和后者指定了異常類型,則沒事,但是大多數時候很難保證;
- typedef中不能有異常具體化,而宏可以
- 調用的程序庫可能產生未指定異常
- 修改unexpected函數以將未預期異常轉化為預期的
- 當一個高層的調用者已經準備好要處理發生的exception時,unexcepted函數卻會被調用
條款15:了解異常處理的成本
- 成本1:為了在運行時處理異常,需要大量的簿記工作
- 大部分編譯器允許你決定是否加入exception的支持能力
- 成本2:try語句塊,代碼大概膨脹5%-%10,這個成本和exception specification差不多
- 成本3:根據82法則,拋出異常不會對程序產生大量的沖擊,但是性能損失也十分可觀
效率
分為兩部分:高效的數據結構和算法;高效的語言實現(結合c++語法特性)
條款16:二八法則
- 定義:一個80%程序資源、執行時間、內存、磁盤訪問、維護成本都歸咎于20%的代碼
- 意義:你所產生的代碼80%的時間不會影響系統的性能,這個法則也暗示如果你的軟件出現性能上的劣勢,那你的問題可能不僅僅出現在當前的瓶頸中,可能有更大的提升空間。而如何找出這個空間所在是一個值得思考的問題
- 可行之道:完全根據觀察或實驗識別那20%的關鍵所在;使用程序分析器(可以從總體上獲取程序運行每一個區間的性能表現)
- 關注語句或函數被調用的頻繁度(比如內存分配相關,系統調用等)
- 需要可重制(實際場景中運用的數據)數據來不斷檢測程序的問題
條款17:lazy evaluation(對應于eager evaluation)
- 策略1:引用計數,保證了數據共享機制的正常運行,比如寫時復制
- 策略2:讀寫區分,比如讀寫鎖機制,數據庫的主從機制
- 策略3:lazy fetching,加載大對象數據時,不進行IO操作,而只是獲取一個數據外殼,只有需要訪問相關數據的時候,才采取行動,比如數據庫。設計一個類,為其各個數據保留一個指針字段,并初始化為nullptr,那么在任何成員函數中都可能被賦值(包括mutable),所以使用mutable(高于const)保證它的可變性,或者使用下列語法
const string& ConstTest::field1() const{
ConstTest *const fakeThis=const_cast<ConstTest* const>(this);
if(fieldValue==0){
賦值操作;IO操作
}
}
- 表達式緩式評估:比如矩陣運算,可以延緩運算,而將表達式相關信息暫時保存,如果后續只需要獲取部分信息,則可以有效避免大量運算
條款18:分期攤還預期的計算成本(over-eager evaluation)
- 如果你預期會經常運用某個操作,那么可以通過降低每次計算的平均成本,辦法是設計一份數據結構以便能夠高效的運行該業務。
- 其中有效的策略是保存可能會用到的數據,也就是caching
- 還有一種策略:prefetching,運用的哲學是locality of reference, 比如系統設計中的磁盤緩存、指令和數據的內存緩存等
- 一種重要用例是:在知道預期會使用更大的內存情況下,一次性分配較大的內存,而不是多吃少餐。這就提醒我們要盡量減少系統調用。這是一種用內存換時間的典型例子。
- 存在問題:prefetching在內存不足的情況下可能加劇內存分配的困難程度,因為很難找到合適大小的虛內存分頁。此時就需要大量的換頁操作,并且這也導致的緩存命中的困難程度(如果緩存可用的話,相反一次性獲取多個對象的內存反而能提高花緩存命中)
條款19:臨時對象
- 當產生non-heap object沒有被命名時,則會產生臨時對象;
- 臨時對象通常發生于兩種情況:一是隱式類型轉換,二是當函數返回對象的時候;
- 特殊情況:當向一個string傳遞c_str時,如果是by value或者reference-to-const方式則轉換會發生,而普通引用不能指向一個右值,也就是不能指向一個臨時對象,所以這里的轉換既不能發生,也不合法
- 有時候,使用復合單操作數運算符比雙操作數運算符更能避免產生返回臨時對象
條款20:返回值優化
- 對于返回對象的情況,c++提供一種優化的方式,在返回語句中創建的對象不做為臨時對象,而是直接構造于其調用空間中
- 為了返回值優化而將非構造類型的變量運算符重載沒有必要,這會改變原本的語義特性
條款21:利用重載技術避免隱式類型轉化
- 為了返回值優化而將非構造類型的變量運算符重載沒有必要,這會改變原本的語義特性
條款22:考慮以操作符復合形式取代獨身形式
- 復合形式可以使用返回值優化或者抹去返回值以避免創建臨時對象
- 當你面臨在臨時對象和命名對象之間選擇時,盡量選擇前者
- 身為一個程序設計者應該提供兩種方式,一種是最自然的實現,另一種是高效的實現,作為軟件開發者應該盡量選擇后者
條款23:考慮使用其他程序庫
- 根據性能,內存消耗,IO訪問等優化選擇第三方程序庫
條款24:virtual functions、multiple inheritance、base classes、RTTI
- 虛函數的實現機制:編譯期提供虛表和虛表指針這兩個數據結構維護動態調用所必需的的信息
- 虛表是建立在類上的,也就是一個類(包括派生類)一個虛表用于存儲虛函數指針信息,這個虛表存儲在第一個包含non-inline,non-pure函數定義的目標文件中;派生類擁有自己的虛表,并且繼承基類的函數指針,同時覆蓋同名的函數指針。如果派生類繼承自多個基類,則維護多個虛表,相應對象也維護多個虛指針。
- 每一個包含虛函數類的對象(動態類型)都會維護一個vptr(指向虛表的指針而不是虛函數),vptr在尋找對象類所在的表時會產生一個offset adjustment。
- 虛函數的調用并不構成性能瓶頸
- 多重繼承使用vitrual base classes來避免數據成員的復制,而其底層也是使用虛類指針的方式實現的
- RTTI:我們有義務讓包含虛函數的類都能獲取其對象的類型信息,而這個信息一般也被維護在虛表里面
- 虛表機制(編譯器提供的服務)在一些場景中應該被回避,如對象的持久化,進程間搬移對象;
- 動態綁定的調用過程是這樣的,首先,基類指針被賦值為派生類對象的地址,那么就可以找到指向這個類的虛函數的隱含指針,然后通過該虛函數的名字就可以在這個虛函數表中找到對應的虛函數的地址。然后進行調用就可以了;
- 虛函數的內存模型
技術
條款25:將構造器和非成員函數虛化
- 虛化構造器:用于返回一個基類指針的generator,或者用于copy當前對象并返回其基類指針的copy函數,這種函數一般會調用真正的復制構造器,與之保持功能一致性。
- 這種機制利用晚些時候加入的寬松點:重寫的虛函數不必保持返回類型一致性,而返回具有繼承關系的指針和引用是設計合理的應用
- 非策成員函數虛化:寫一個虛函數做實際工作,再寫一個什么也不做的非虛函數,只負責調用虛函數,為了避免函數調用的成本可以將非虛函數inline。
條款26:限制某個class所能產生的對象數量
- 如何產生零個或者一個對象:零個對象的做法是將class的構造函數(兩個)放到private中;一個對象的做法是在零個基礎上使用友元函數包含static對象的創建方式保證只有一個對象被創建
- 友元函數首先是一個全局函數,然后加入了類的訪問權限。而全局性往往使我們深惡痛疾的,所以我們可以將友元函數改為靜態成員函數(public)也能完成一個問題,但是這又造成一個問題:;類的靜態對象會從一開始就被創建,而函數的靜態對象只有當函數被調用時才創建,前者違背了c++的哲學基礎:你不應該為你不使用的東西付出代價。
- 另外,class static對于初始化時機的把握也不是很明確,因為不同編譯單元內的static無法保證初始化順序
- static和Inline的互動:inline非成員函數存在內部連接,而帶有內部連接的函數可能會在程序中復制,而相應的local static object也會被復制,這樣就會產生多個static對象的副本,所以應該內聯帶有靜態局部變量的非成員函數
- 限制數量的另外一種通用方式是:計數限制,然而又出現一個新的問題,存在繼承關系的計數可能會違背我們的初衷,所以使用條款33的避免對一個具體類繼承可以幫到我們。
- 然后對象計數限制可能以三種不同的形式呈現出來:(1)它自己(2)派生類的基類成分(3)內嵌于較大對象中,這些形式把追蹤對象的意義搞混亂了。對于第一種限制可以使用private構造函數實現,因為private構造函數避免了被繼承和內嵌;可以使用靜態成員函數的方式(使用new而不是static)實現創建多個對象,同時避免了混亂的情況,同時為了為了避免泄露,我們將使用智能指針;將限制計數和這種情況結合就能實現對象的生生滅滅。
- 如此一來,問題大致得到了很好的解決,但是如果存在多個需要被計數的類,這樣的邏輯就無法避免出現代碼的重復。我們可以通過設計一個私有繼承的方式,將計數的實現過程封裝成一個基類。考慮到每一個類型都得有一個單獨的計數器,所以static變量在多個類型之間不能共享,c++中的template可以幫助避免這樣的問題。關于設計,我們需要注意以下幾個問題:1. 保持私有繼承,因為這是has-a的關系;2. 計數器的值應該對外暴露出來,所以子類的相關數據仍然保持私有,而在派生類中使用using base<derived>::objectCount;3. 關于計數的兩個成員變量objectCount和maxCount,前者在定義(申明的時候不初始化,不分配空間)的時候默認初始化為零,后者應該使用和類型綁定的初始化方式,因為maxCount應該針對不同類型發生變化,這樣的事情應該扔給用戶去做 。
條款27:要求或者禁止對象產生于heap之中
- 考慮這個問題的初衷:保證對象是可以被“delete this”的;保證某一類型的對象絕對不會發生內存泄露,也就是對象絕對不會被分配在heap上。
- heap-based objects:限制new以外隱式構造和析構的使用-->限制constructor和destructors的訪問級別--->限制destructors并為外界提供一個顯示的析構封裝;存在問題:繼承(protected)和包含(object-->pointer),未完待續
- 上述的解決方案能夠階段性的解決當前類型的約束問題,但是不能保證派生類對象的父類部分一定是在heap上分配的,因為析構函數對子類仍然是可見的。這樣我們可以使用子類標識來檢測父類的構造是否是通過new產生的(調用operator new的時候改變標志位)進而得知當前對象的子類部分是否是heap-based;但是對于對象數組又會失效,并且在構造時通過位設立檢測內存是否被分配于堆上并不可靠,有時候多個對象的建立會重排內存分配和構造的順序(一個對象的內存分配可能會在另一個對象的構造之前);進一步考慮堆內存分配的順序(從低地址-->高地址),這樣的標準不具有一致性,并且static變量也可能是個干擾,內存被分配在三個地方的標準就難以掌控了;
- 改變思路:我們的初衷是想判斷delete是否安全,而不是堆內存的判斷(后者比前者更加模糊),而前者可以通過判斷指針是否由new返回。在此基礎上,我們給類型添加一個new的構造列表,并在operator new里面將每一個對象的首地址添加進去。而delete所需要做的就是從這個集合中刪除相應的entry。更進一步:我們需要使用abstract mixin base class來封裝heap track 的工作,具體基類的申明和實現如下:
//declaration
class HeapTracked{
public:
class MissingAddress{};
virtual ~HeapTracked()=0;
static void *operator new(size_t size);
static void *operator delete(void *ptr);
bool isOnHeap() const;
private:
typedef const void* RawAddress;
static list<RawAddress> addresses;
};
//definition
void* HeapTracked::operator new(size_t size){
void *memptr=::operator new(size);
addresses.push_front(memptr);
return memptr;
}
void HeapTracked::operator delete(void* ptr){
list<RawAddress>::iterator it=find(addresses.begin(),addresses.end(),ptr);
if(it!=addresses.end()){
addresses.erase(it);
::operator delete(ptr);
}else{
throw MissingAddress();
}
}
bool HeapTracked::isOnHeap() const{
const void *rawAddress=dynamic_cast<const void*>(this);
list<RawAddress>::iterator it=find(addresses.begin(),addresses.end(),rawAddress);
return it!=addresses.end();
}
// how to use it
class Asset: public HeapTracked{
};
void inventoryAsset(const Asset *ap){
if(ap->isOnHeap()){}
else{}
}
- dynamic_cast是找出某個對象占用內存的起始點,一般轉型為const(或者volatile)void *,但是它只適用于對象有至少一個虛函數的情況,也就是存在多態的繼承關系,對于普通類型int等不適用。
- 禁止對象產生于heap之中有三個含義:(1)對象被直接實例化(2)對象被實例化為derived class objects內的“base 成分”(3)對象被內嵌在其他對象之中
- 解決辦法:將operator new私有化,存在問題是派生類沒有辦法heap-based,可以重寫派生類的new方法,但是無法避免子類對自己的調用(?),關于內嵌問題,私有化的operator new就足以解決。
- 派生類使用new時,先用operator new開辟堆上內存,然后分別使用父類和自己的構造函數在該內存空間中完成初始化(?)
條款28:智能指針
- 使用智能指針你可以獲得如下行為的控制權
- 構造和析構,通過引用計數管理資源
- 復制和賦值,你可以決定深復制還是淺復制等
- 解引用,你可以決定解引用時發生的行為,比如lazy fetching
- 智能指針的實現:智能指針由template產生出來,表明其實際類型,智能指針重載=,->,*等運算符
- 智能指針與RPC:將遠程對象封裝在本地智能指針里面,能夠有效的控制遠程調用的時機
- 構造:確定一個目標物,然后將成員變量指定為它,如果沒有目標物就默認為零
- 復制構造、賦值和析構:在c++里面,auto_ptr規定同一對象只能擁有唯一的控制者,所以賦值操作會改變目標對象的控制權,而將刪除原對象和將原指針置空。因此,我們千萬不能按照pass-by-value的方式傳遞參數(因為這樣復制構造會將原智能指針置空,并且在函數結束時原對象也會被銷毀,返回后的任何引用都會導致災難性的結果)。毫無疑問任何智能指針的傳值都應該按照pass-by-reference-to-const 的方式。
- 當smart pointer對象被復制,或是作為賦值動作的來源端時,它們會被修改
- 解引用:此處返回一個對象的引用,這樣可以賦予其多態屬性,如果是lazy fetching則應該產生一個新對象,至于新對象和遠程對象的拷貝可以延遲滿足。
- 測試smart pointer是否為null:使用隱式類型轉換操作符
operator void*();
,這樣就能使用常規的邏輯判斷了。但是這種重載會導致任何類型的判斷都能通過編譯,所以可以選擇只重載!操作符 - smart-dumb:可以使用隱式類型轉化符:
operator T*(){return pointee;}
但是此時的dumb又直接暴露在用戶面前了,如果涉及到引用計數,這個問題就更加嚴重了。所以,盡量不要提供這種隱式類型轉換。 - smart pointers的繼承:可以使用隱式類型轉換可以實現轉換,但是太過于繁瑣而且不安全。所以采用一種新的語言特性:將non virtual member function聲明為templates,將多種類型的隱式類型轉換用函數模板替代。
nvmf是一項先進的技術,它涵蓋了以下四種重量級的要素:
- 函數調用的自變量匹配規則
- 隱式類型轉換函數
- template functions的暗自實例化
- 成員函數模板等技術
- smart和const:const 天生支持對smart pointer指向的常量化,而對其指向對象的常量化是通過改變模板參數的常量性實現的。但是這樣的改變并沒有增加智能指針的彈性,常量指針(智能)和非常量指針(智能)并不能實現轉換。另外,考慮到類型轉換涉及到常量性和繼承的單向性類似,即non-const->const是安全的,反之不安全。所以可以利用這種性質,令每一個smart pointer-to-T class 公開繼承smart pointer-to-const-T class。實現邏輯如下:
\\base class
template<class T>
class SmartPtrToConst{
...
functions
...
protected:
union{
const T* constPointee;
T* pointee;
}
}
\\ derived class
template<class T>
class SmartPtr: public SmartPtrToConst{
沒有data members
}
- 注意:兩個class的members必須自己決定使用哪一個指針,編譯器不能幫你決定。這個可以使用重載(???)
條款29:引用計數
- 引用計數的作用:1. 簡化heap對象的簿記工作。記錄對象的擁有者,在擁有權發生轉移(而不是增加)時,這顯得很困難;2. 節省空間,一處存儲,多處使用。
- 實現:在private中構建一個struct包含引用計數器和引用值-->重寫構造函數和賦值函數,使得他們支持引用計數
//declarations
class String{
public:
String(const char* initValue="");
String(const String&);
String& operator=(const String&);
~String();
const char& operator[]const(int);
char& operator[](int);
private:
struct StringValue{
int refCount;
char* data;
StringValue(const char*);
~StringValue();
}
StringValue* value;
};
//definitions for StringValue
String::StringValue::StringValue(const char * initValue):refCount(1){
data=new char[strlen(initValue)+1];
strcpy(data,initValue);
}
String::StringValue::~StringValue(){
delete [] data;
}
//definitions for String
String::String(const char * initValue = ""):value(new StringValue(initValue)){//這里本也可以實現真正的按字面量共享愛過你,但是
//太過精細反而會降低效率
}
String::String(const String & rhs):value(rhs.value){
++value->refCount;
}
String& operator=(const String& rhs){
if(value==rhs.value){//這個并不是按照字符串值進行比較,而是按照已創建對象之間是否有共有關系
return *this
}
if(--value->refCount==0) {
delete value;//檢查當前對象狀態,如果計數為零則當前指針有責任釋放
}
value=rhs.value;//指向新的對象,并更新對象的引用計數
++value->refCount++;
return *this;
}
String::~String(){
if(--value->refCount==0) delete value;
}
const char& String::operator[](int index) const{
return value->data[index];
}
//區分寫操作和讀操作(copy-on-write)
char& String::operator[](int index){
if(value->refCount>1) {
--value->refCount;
value=new StringValue(value->data);
}
return value->data[index];
}
- 寫時復制:通過對具體的寫操作,也就是[]操作符的重載,檢查當前計數是否為1,如果處于共享狀態,則需要額外復制一份。另外這也帶來一個問題,返回的可寫單元是沒有辦法被引用計數的,所以它的改寫會產生連鎖效應,進而使得整個共享變得不可控。解決辦法有:降低共享的實值個數,在StringValue中加入一個可否共享的標志位flag,對于被額外引用的對象禁止共享。如果讀寫無法有效區分,那么禁止共享的對象個數會比較多,如果區分開來那么這種機制是相當有效的。
-
reference-counting base class(RCObject):設計架構
image.png - 注意事項:
- RCObect的賦值操作從來都不會發生,因為在一個有reference counting的系統中,實值對象并不會被賦值,只會改變引用次數;
- 這里的實值對象是專屬于應用對象的,所以將其定義為嵌套類,而讓一個嵌套類繼承自另一個類,而后者與外圍類無關,這種繼承關系將越來越司空見慣;
- 如何自動操作(引用計數):使用模板智能指針而不是常規指針來操作實值對象,而關于寫時復制,引用計數(構造、賦值、析構),深度復制等操作都被其自動處理。
- 但是在以下語句時需要注意:
pointee=new T(*pointee);
當你使用智能指針的init產生副本時,它會調用實值對象的copy constructor,而我們知道默認的copy constructor只會對成員指針執行淺復制,所以我們需要為所有內含指針的類重寫copy constructor。- 對于應用對象,我們不用單獨重寫copy 和assignment,因為默認的就足夠了
-
將reference counting泛化:
image.png - 差異:RCPtr是直接指向實值,而后者是通過CountHolder實現;后者重載了operator->和operator*,這樣一來non-const access就會觸發寫時機制(!!!,const函數的神奇)
什么時候適合用引用計數 ?
- 考慮其成本:計數器的創建和更新(CPU)、額外分配的內存(memory)、復雜的底層開發(開發成本)
- 適用于對象常常共享實值的情況:相對多數的對象共享相對少量的實值;對象實值創建和銷毀的成本較高(內存和CPU)
- 其他考慮:自我引用和循環依賴的數據結構可能會導致對象的引用次數總是大于零;“不確定誰被允許刪除什么東西”的問題也可以通過引用計數解決,而這個通常是許多程序員入坑的主要因素。
- 實值對象為new所得: 這和智能指針的要求是一樣的,而應用對象負有確知實值對象共享性并將其實例化的責任。
條款30:proxy classes(替身類和代理類)
- 使用proxy class實現多維數組:二維數組的行可以用一個一維的對象代替,并重載[]運算符;
- 區分operator[]的讀寫操作:1. 使用代理類使得被替換的概念在操作上更加有彈性(能夠支持更多精細的控制)2. 在reference counted string的基礎上,我們討論更復雜的情形:
讀-->右值引用-->operator[]返回代理對象-->賦值操作operator[]
寫-->左值引用-->operator[]返回代理對象-->其他方式使用如隱式類型轉化char()
具體實現如下:
class String{
public:
...
class CharProxy{
public:
CharProxy(String& str,int index);
CharProxy& operator=(const CharProxy& rhs);
CharProxy& operator=(char c);
operator char() const;
private:
String& theString;
int charIndex;
}
const CharProxy operator[](int index)const;
CharProxy operator[](int index);
...
friend class CharProxy;
private:
RCPtr<StringValue> value;
...
};
//使用代理對象延緩請求
const String::CharProxy String::operator[](int index)const{
return CharProxy(const_cast<string&>(*this),index);
}
String::CharProxy String::operator[](int index){
return CharProxy(*this,index);
}
//根據具體操作決定使用哪種方式訪問
String::CharProxy::CharProxy(String& str,int index):theString(str),charIndex(index){}
String::CharProxy::operator char() const{
return theString.value->data[charIndex];
}
String::CharProxy& String::CharProxy::operator=(const CharProxy& rhs){
if(theString.value->isShared()){
theString.value=new StringValue(theString.value->data);//智能指針的隱式構造
}
thsString.value->data[charIndex]=rhs.theString.value->data[rhs.charIndex];
return theString.value->data[rhs.charIndex];
}
- 注意:賦值函數要求CharProxy擁有對String.value的私有訪問權,所以將CharProxy聲明為friend類。
- 壓抑隱式轉換:使用代理對象可能在某些場景下造成多次隱式轉換,而這是不被支持的,所以可以達到壓抑隱式轉換的目的,但是事實上更好的做法是使用explicit。
- 限制:盡管代理對象不能完成一些原生對象的諸功能(雖然可以重載),但是有明確的目標進而使用它仍然可以幫助我們解決大部分問題。
條款31:讓函數根據一個以上的對象類型來決定如何虛化
- 注意到以類型為行事基準建立的函數在C語言中給人的影響是:難以維護,難以擴展,于是在c++里面引入了虛函數,將繁雜的類型判斷邏輯交給編譯器。而RTTI實現double dispatching無疑將我們再次推回了那個時代。
- 使用RTTI的if-else-if等來實現,代碼的復用性和擴展性不強
- 使用虛函數加重載:在基類中擴充多個類型參數的虛函數,然后分別由子類繼承他們,存在同樣的問題——沒有辦法動態擴展
- 自行仿真虛函數表格:
class GameObject{
public:
virtual void collide(GameObject& OtherObject)=0;
};
class SpaceShip:public GameObject{
public:
virtual void collide(GameObject& otherObject){
processCollision(otherOject,*this);
}
private:
static void processCollision(GameObject& o1,GameObject& o2){
lookup(typeid(o1).name()),typeid(o2).name())
}
};
namespace{
void ShipStation(GameObject&){
// the process for collision;
}
void StaionShip(GameObject& g1,GameObject& g2){
return ShipStation(g2,g1);
}
typedef void (*HitFuncPtr)(GameObject&,GameObject&);
typedef map<pair<string,string>,HitFuncPtr> HitMap;
HitMap* initHitMap();
HitFuncPtr lookup(HitMap* m);
}
//封裝
class CollisionMap{
public:
void ShipStation(GameObject&){
// the process for collision;
}
void StaionShip(GameObject& g1,GameObject& g2){
return ShipStation(g2,g1);
}
void addEntry(const string& class1,const string& class2, HitFuncPtr HitFunc,bool symmetric=true);
void remove(const string& class1,const string& class2);
HitMap* initHitMap();
void processCollision(GameObject& o1,GameObject& o2){
HitFuncPtr HitFunc= lookup(typeid(o1).name()),typeid(o2).name());
(*HitFunc)(o1,o2);
}
private:
HitFuncPtr lookup(HitMap* m);
typedef void (*HitFuncPtr)(GameObject&,GameObject&);
typedef map<pair<string,string>,HitFuncPtr> HitMap;
};
- 注意:
- 匿名namespace表明其內容為該文件私有(類似于static的功能)
- 對稱的碰撞函數
- lookup里面對于map的定義使用static的智能指針,便于回收;
- 這里的CollisionMap的類應該是單例的
- 數組和指針類型的判斷:查看定義式中表示的單個元素的意義,比如p[10]表示單個指針指向的內容,所以它是一個指針數組,如果是這樣p,它則表示一個指向指針的指針,可以用以表示二維動態數組,(p)[10]此時指每一個指針都指向一個包含十個元素的數組;
雜項討論
條款32:在未來時態下發展程序
- 好的軟件對于變化應該有良好的適應能力,設計時應盡量使用用戶思維
- 如果派生不會發生就應該從語法上阻止它,而不只是警告
- 如果copying和assignment對于某個class沒有意義,及應該private
- 在不使用虛函數時盡量保持謹慎
- 努力讓class的操作符和函數擁有自然的語法和語義
- 寫出可移植性的代碼
- 盡量采用封裝性質,使你的實現細目private
- 使用namespaces和static對象或者函數
- 盡量避免設計出virtual base classes,因為這種類必須被其每一個derived class初始化
- 避免使用RTTI設計出if-then-else的繁瑣語句
- 提供完整的classes,設計你的接口有利于共同的操作行為,阻止共同的錯誤
- 泛化代碼
條款33:將非尾端類設計成抽象類
- 提供抽象類的一個理由:抽取出對于多個具體類(通常大于或者等于兩個)有用的信息,可以將被抽取的抽象性封裝成一個抽象類和繼承自該抽象類的具體類(保證抽象性的具體實現),并對外隱藏某些接口(protected,既然是隱藏就不用使用virtual聲明了,并且抽象類的純虛函數一般選擇析構函數)(避免派生類之間發生不必要的聯系,比如異形賦值)。
可以理解此處的繼承關系保存了封裝,復用等特性,犧牲了多態的特性
- 設計抽象類的原則:概念本身有用,對于一個或者多個繼承類有用,如果當前并不知道這種概念能不能泛化的運用于多個概念,則提前設計抽象類是沒有意義的
- 當我們萬不得已需要繼承自一個具體類時(比如第三方庫),需要注意剛開始出現異形賦值的問題和多態數組問題
將需要封裝的接口放到protected里面,比如賦值運算符重載,可以避免多態數組的問題
- 一般性的原則:繼承體系中的non-leaf類應該是抽象類。如果使用外界供應的程序庫,你或許可以對此法則做一些變通;但是如果代碼完全掌控在你手中,堅持這個原則,可以提升整個軟件的可靠度、健壯度、精巧度、擴充度。
條款34:如何在同一個程序中結合c和c++
名稱重整
- c++編譯器會將函數名稱重整,獲得一個全局獨一無二的名稱,這為重載,多態提供了實現的可能,實則是對連接器的一個退讓(一般意義上重載并不能被鏈接器兼容);
- 問題:在封閉的c++環境里面無需顧慮,但是當你包含一個c庫,對cku中函數的調用被一如既往的翻譯成重整后的名稱,而此時連接器就會找不到庫函數。
- 解決辦法:要壓抑重整,需要使用extern “C”指令包含調用函數的聲明;
//幾種實現語法:
extern "C" void myfunc();
extern "C"{
void myfunc1();
void myfunc2();
......
}
//被c和c++同時使用的語法,考慮
#ifdef _cplusplus
extern "C"{
#endif
....
#ifdef _cplusplus
}
#endif
- 不同的編譯器重整方式也不一樣
statics的初始化
- static class對象、全局對象、namespace內的對象、文件范圍內的對象總是在main之前初始化,main之后銷毀;
- 在main內static initialization,static destruction,所以盡量為程序編輯main程序
動態內存分配
- 因為new/delete ,malloc/free是嚴格一一匹配的,所以在遇到像strdup這樣的函數時(不知道來自哪個庫)時,就會遇到問題。所以編譯器的不同也會帶來移植性的問題,盡量少使用非標準平臺庫。
數據結構的兼容性
- 在c和c++之間對數據結構做雙向交流,應該是安全的——前提是那些結構的定義式能夠在c和c++中都能編譯。而c++的struct加入非虛函數并不改變內存布局,所以仍然能滿足兼容性要求
- 確定你的c++和c編譯器產生兼容的目標文件
條款35: 讓自己習慣于標準c++
- 支持c標準函數庫
- 支持strings
- 支持國別(字符集、時間、貨幣等)
- IO
- 支持數值應用(complex)
- STL
- 注意:上述程序庫基本都是建立在泛型的基礎上的,包括string