c++vol2 讀後感(轉)

《C++編程思想》閱讀筆記(二)

動態對象創建

1C語言中動態內存分配(malloc)只在堆中分配一片內存,返回一個void指針(分配失敗則返回0),並沒有創建一個對象。使用時需要強制轉換成恰當的類型。調用free則只負責釋放一片內存,並沒有析構一個對象。

2、創建一個C++對象,有兩步:a 爲對象分配內存;b 調用構造函數來初始化那片內存。

3C++中把創建一個對象的所有動作都結合在一個稱爲new的運算符裏。當用new創建一個對象時,它在堆裏爲對象分配內存併爲這塊內存調用構造函數。當分配內存失敗時返回0

4、運算符new分配內存是調用malloc實現的,在調用構造函數前,new會檢查內存分配是否成功。

5、與new對應的則是deletedelete首先調用析構函數,然後釋放內存(一般是調用free)

6、如果delete的對象指針是0,將不發生任何事情,因此在刪除一個對象後,應立即將其指針置0。因爲重複刪除同一對象會產生錯誤。

7、定義一個友元函數爲內聯函數不會改變友元狀態,它依然是全局函數,而不是一個類的成員函數。

8、如果對一個void指針進行delete操作,唯一執行的是釋放內存,而不會調用析構函數,因爲void指針沒有,類型信息用來確定調用哪個析構函數。

9、不要將在棧上創建的對象的指針和在堆上創建的對象的指針都放在同一個容器中,否則會產生嚴重的問題。

10、動態創建數組:new會調用默認的構造函數,delete需要“[]”來標識。

11、使指針更像數組:用如下方式:int* const arr = new int[10];,這裏arr即爲一個指針常量,其值不可修改,更像一個數組名。若爲:int const* arr = new int[10]; const int* arr = new int[10]; 這裏的arr爲一個常量指針,其指向的值不可修改。另外,數組名和指針的取值方式不一樣:指針有兩次取值才獲得真正的對象,數組只需一次。

12、當new分配內存失敗時,會調用new-handler函數,該函數默認產生一個bad_alloc的異常。

13、重載newdelete:重載newdelete時,我們只能改變原有的內存分配方法,因爲我們無法控制構造函數和析構函數的調用。

繼承與組合

1、在public繼承中,派生類的對象也是基類的對象,但基類對象不是其派生類的對象;

2、將基類指針強制轉換成派生類指針:derivedPtr = static_cast<baseClass*>(basePtr);

3、名字隱藏:a 在派生類中定義和基類中籤名一樣的函數,這叫普通成員函數重定義;如果基類中該函數是虛函數,則叫重寫(overriding)b 如果簽名不同,則基類中所有與該函數同名字的的函數在派生類中均自動隱藏了。

4、在覆蓋定義基類成員函數時,調用基類中的該函數版本需用作用域運算符,否則實際調用的是該函數自身,會引起無窮遞歸。例:BaseClass::function();

5、繼承類型:

apublic繼承:基類的public成員成爲派生類的public成員,protected成員成爲派生類的protected成員,private成員則隱藏。

bprotected繼承:基類的public成員和protected成員成爲派生類的protected成員,private成員在派生類中不可見,隱藏。

cprivate繼承:基類的public成員和protected成員成爲派生類的private成員,private成員在派生類中不可見,隱藏。

privateprotected繼承不是“is-a”)的關係。繼承默認是private的。

6、類的構造是從類層次的最根處開始,而在每一層,首先會調用基類構造函數,然後調用成員對象構造函數。析構嚴格按照相反的順序。注意:基類的構造在成員對象構造之前。構造函數調用的次序完全不受構造函數的初始化列表中次序的影響。

7、把派生類對象賦給基類對象,然後試圖在該基類對象中引用只在派生類中才有的成員是個語法錯誤。

8、多重繼承:用virtual繼承時,派生類中只出現一個子對象,可解決重複子對象的問題。

9javaC++的區別:java是純粹的(pure)的面嚮對象語言,而C++ 是一種混合的(hybrid)語言。java是基於對象的繼承層次結構模型,所有類都是在一個單一的繼承層次結構中派生的,有一個基類爲根(Object)

10、非自動繼承的函數:構造函數,析構函數,賦值函數(operator=)因爲它完成類似於構造函數的功能。

11、靜態成員函數不可以是虛函數。

12、將私有繼承成員公有化:在派生類中的public部分聲明其名字,例:

       public

using baseClass::memberName;   //給出一個重載函數的名字,將使基類中所有它的重載版本公有化

13、除賦值運算符外,其餘運算符的重載都可以自動地繼承到派生類中。

14、類中編譯器可自動生成的四個函數:構造函數,拷貝構造函數,析構函數和賦值運算符(operator=)

15、確定使用組合(composition)還是繼承(inheritance)的方法是:是否需要從新類向上類型轉換; 確定是否需必須使用多重繼承的方法是:是否需要將一個對象指針向上類型轉換成兩個不同的基類類型。

16、多重繼承分兩種:接口繼承(interface inheritance)和實現繼承(implementation inheritance)。接口繼承僅僅是在派生類接口中加入成員函數的聲明,C++不支持這樣用法,C++僅提供了實現繼承,接口繼承需用特別方的法實現。

17、多重繼承的一個共同用途包括使用混入類(mixin),混入類的存在是爲了通過繼承來增加其他類的功能。(卷二P359)

18菱形繼承:通過使用虛基類(不是抽象基類)來消除重複子對象,使用virtual關鍵字繼承(virtual public)

19、基類必須有虛析構函數,雖然沒有編譯器也可以編譯通過。

20、編譯器對多重繼承的實現原則:一個帶有多重繼承的派生對象必須表現出它好像有多個VPTR,它的每個含有虛函數的直接基類都有一個。

21、由於菱形繼承中基類構造的二義性,規定:最高層派生類(繼承樹最葉子端的類)必須初始化一個虛基類(卷二P367)。而中間層次的派生類對基類的初始化將被忽略。

22、多重繼承名字查找問題:若基類中含有同名的函數,派生類調用該函數將產生編譯錯誤。消除方法:用基類名來限定函數的調用,例:

       在派生類中:       public

                                          using baseClass::function;//注意聲明函數名即可,不需括號

另外,基類成員在派生類中的作用域規則:在繼承層次結構中,離派生類越近的上層類,其成員在派生類中的優先級越高。

23、多重繼承的優勢只與虛基類一同存在。

虛函數和多態:(多態--polymorphism,封裝--encapsulation

1、一旦一個函數被聲明爲虛函數,即使重新定義時沒有被聲明爲虛函數,它在該點之後的繼承層次結構中仍都是虛函數。

2、抽象類:有一個或多個純虛函數的類,也稱抽象基類。其唯一的用途是爲其他類提供合適的基類,其他類可從它這繼承或實現接口。抽象類仍然可以提供虛函數的實現。

3、若抽象基類的派生類沒有提供抽象基類中純虛函數的定義,則該派生類仍然是抽象類,因爲繼承了基類的純虛函數。

4、多態性:通過繼承相關的不同類,他們的對象能夠對同一個函數調用作出不同的響應。

5、多態性是通過虛函數實現的。通過基類型的指針或引用請求調用虛函數時,C++會在與對象關聯的派生類中正確地選擇重定義的函數。注意只能是基類型的指針(BaseClass*)或基類型引用(BaseClass&),不能是傳值調用(BaseClass),防止對象切片。

6、不能實例化抽象類的對象,但可以聲明引用抽象類的指針,指向具體類的對象後,這種指針使派生類對象具有多態操作能力。

7、給基類提供一個虛析構函數,這樣刪除該類的派生類時,自動刪除基類部分。

8、將構造函數定義成虛函數是個語法錯誤,構造函數不能是虛函數。但一般需要將基類的析構函數定義成虛函數。

9C++多態的實現原理:多態是通過複雜的數據結構實現的,涉及三層指針。

第一層:C++編譯時,給每個含有虛函數的類建立一個虛函數表(VTABLE),該表中,對每個虛函數都有函數指針指向,而若爲純虛函數,則函數指針設置爲0。因此vtable中有0時爲抽象類(abstract class),沒有0時爲具體類(concrete class)

第二層:對每個實例化的帶虛函數的類對象,編譯器在對象前面添加指向該類vtable的指針(VPTR)。

第三層:接受虛函數調用的對象句柄(指針或引用)

多態實現步驟:

       a 將基類指針或引用指向具體類對象;

       b 通過基類句柄獲得具體類對象,該對象以vtable指針開始;

       c 復引用vtable指針獲得該具體類的vtable

       d 搜索vtable找到需調用的函數指針;

       e 復引用函數指針,調用該函數。

10、虛函數增強了類型概念,它使類概念不僅僅只是在結構內隱藏地封裝代碼。它是面向對象程序設計的核心所在。

11、動態綁定(dynamic binding)(也稱晚綁定,運行時綁定)只對virtual函數起作用,且只在使用含有virtual函數的基類的指針或引用時發生。不能是基類對象的一個原因是傳值會導致對象切片。

12、創造C語言是爲了代替彙編以實現操作系統,發明C++的主要原因之一是讓C程序員的工作效率更高。

13、對只存在於派生類中的函數做虛函數調用,會產生編譯錯誤。

14、編譯器不允許我們在派生類中改變基類中虛函數的返回值,若不是虛函數,則是可以的,但基類中所有同名函數在派生類中都自動隱藏。另外,如果基類中返回值是某基類對象,在派生類中將返回值改成該基類的派生類,則是可以的。

15、虛機制在構造函數和析構函數中均失效。

16、純虛析構函數:唯一的效果是阻止基類的實例化。定義純虛析構函數,我們必須提供一個函數體。

17、單根繼承(single-rooted hierarchy),也稱基於對象的繼承(object-based hierarchy):所有類都直接或間接地從一個公共基類中繼承出來。java等除C++外的面嚮對象語言都是用這種體系。

18、向下類型轉換(downcasting)C++提供了一個特殊的稱爲dynamic_cast的顯式類型轉換(explicit cast)。但使用dynamic_cast時,對象必須含有虛函數,因爲它使用到了VTABLE中的信息。

模板介紹

1、有三種代碼重用的方法:aC方法(繁瑣,已發生錯誤,摒棄)bSmalltalk方法:通過繼承來實現代碼重用,每個容器類都包含通用的基類Object的項目,java中的方法。cC++中的模板:模板實現了參數化類型(parameterized type)的概念。

2、模板語法:template關鍵字告訴編譯器,隨後的類定義將操作一個或多個未指明的類型。

       template<class T, int size>.......

template<...>之後的任何東西都意味着編譯器在當時不爲它分配存儲空間,而是一直處於等待狀態直到被一個模板實例告知。編譯器和連接器中有機制能去掉同一模板的多重定義。

3、懶惰初始化(lazy initialization):數據成員不在構造函數中初始化,而是推遲到第一次訪問時初始化。若創建大量的對象,但不訪問每一個對象,爲節省存儲,可以使用懶惰初始化。

4、容器所有權問題:當容器中包含指向對象的指針,而這些指針很可能用於程序的其他地方,此時需考慮所有權問題。處理所有權問題的最好方法是由客戶程序員來選擇,通常通過構造函數的一個參數來實現。constructor(....bool owner)

5、迭代器(iterator):迭代器是一個對象,它在其它對象的容器上遍歷,每次選擇其中一個,不需要提供對這個容器的實現的直接訪問。迭代器提供了一種訪問元素的標準方法,不管容器是否提供了直接訪問元素的方法。迭代器是一個靈巧指針,其關鍵是:從一個容器元素移動到下一個元素的複雜過程被抽象爲像一個指針一樣。

C++的異常處理機制

1、傳統的錯誤處理:asserterrnoperrorsignal信號處理系統,非局部跳轉(setjmp()longjmp())

問題:C中的信號處理方法和函數setjmp/longjmp並不調用析構函數,所以對象不會被正確的清理。

2、異常匹配:匹配一個異常並不要求異常與其處理器之間完全相關,一個對象或是指向派生類對象的引用都會與其基類處理器匹配。匹配過程中不會將一種異常類型自動轉換成另一種異常類型,轉換構造函數在異常匹配中失效。

3、全能捕獲:catch(...){}

4、重新拋出異常:在一個異常處理器內部,可以使用不帶參數的throw語句重新拋出異常,且這個異常的所有信息都會自動保留,以傳給更高層的異常處理器。

5、異常沒有被捕獲,則庫函數terminate()函數會自動調用。terminate默認調用C庫函數abort()abort被調用時,程序不會執行正常的終止函數,全局對象和靜態對象的析構函數不會執行。當局部對象的析構函數或全局,靜態變量的構造或析構函數拋出異常時,也會調用terminateset_terminate()可設置自己的terminate函數。

6C++異常處理確保當程序執行流程離開一個作用域的時候,對於在這個作用域中所有由構造函數建立起來的對象,它們的析構函數一定會被調用。問題:當在構造函數中發生異常時,析構函數將不會調用,因而申請的資源無法釋放,並且後面的一系列的對象也將不能繼續構造函數。將出現懸掛指針(naked pointer)。解決辦法:a、在構造函數中捕獲異常;b、使用模板來封裝指針(卷二P13)

7、異常規格說明語法:可能拋出的所有可能的異常類型都應該寫在throw後的括號中。

例:void f() throw(exp1exp2,,);

傳統的函數聲明void f()意味着可以跑出任何類型的異常;而void f() thow()則表示不會拋出任何異常。

8、如果函數所拋出的異常沒有列在異常規格說明的異常集中,則會調用unexpected()函數。set_unexpected可設置自己的unexpected函數,該函數不能有參數和返回值。

9、異常規格說明和繼承:派生類中覆蓋函數不能在異常規格說明列表中添加其他異常,可改爲父類中異常類型的派生類型。

10、避免在模板類中使用異常規格說明,因爲無法預料模板參數類所拋出的異常類型。

11、異常安全:棧容器中出棧(pop)和刪除棧頂元素(top)分開實現的原因是爲了保證異常安全。若將兩個動作放在一個函數中,當爲了得到返回值而調用拷貝構造函數時,拷貝構造函數在最後一行拋出異常,此時,棧頂元素丟失了,而返回卻失敗。因此將改變棧狀態和返回值兩個動作分開實現,這也遵守了高內聚設計原則--每個函數只做一件事情。異常安全代碼能使對象保持狀態的一致性而且避免資源泄漏。

12、通過引用而不是值來捕獲異常:a、當異常對象傳遞到異常處理器中的時候,避免進行不必要的對象拷貝;b、當派生類被當做基類對象捕獲時,避免對象切割。

13、不能有異常從析構函數中拋出,因此在拋出其他異常的過程中析構函數會被調用。否則會導致調用terminate函數。析構函數內若可能產生異常,則必須析構函數自行處理。

14、當異常被拋出時,將造成相當多的運行時開銷。

深入理解字符串

1、字符串的內部實現:C語言中字符串就是字符型數組,且總是以二進制零結束;C++stringC字符串最大的不同是它隱藏了字符串的物理表示。極大的減少了C語言中3種常見錯誤:數組越界;野指針訪問;釋放數組空間後產生懸掛指針。

2、創建並初始化C++字符串的方法:

       string sBlank;         string str("hello");         string strcopy = "this is ""a copy";              string copy(str);    

       string s1(str, 0, 4);         string s2 = strcopy + " abc" + str.substr(1,2);              string s3(str.begin(), str.end());     string s4(5, ‘a’); //初始化爲a字符重複5次,即“aaaaa”,第二個參數只能是char字符

3、字符串操作函數:追加: append();插入: insert();長度: size();當前分配的存儲空間大小: capacity()

替換: replace();預留空間: reserve()

查找: find()find_first_of()find_last_of()find_first_not_of()find_last_not_of()rfind()//逆向查找,將字符串反過來;

string類中沒有改變字符串大小寫的函數,可借用C語言的庫函數toupper()tolower()實現;

刪除:erase();返回const char*c_str()//不可作非const參數使用;

字符串的運算符中,沒有邏輯與或非(&&,||,!)運算符,也沒有逐位與或非(&,|,~,^)運算符;

比較:常見的比較運算符(>,<,==)compare()        交換:swap();

C++字符串類中提供一種S[n]表示法的替代方法:at(n)函數。若下標越界,at函數會拋出out_of_range類型異常,多用。

4string類基於basic_string<class T>模板實現

5、寬字符(wchar_t):兩字節(16)

輸入輸出流

1、輸入輸出流:istream對象,ostream對象,iostream對象;文件輸入輸出流:ifstream對象,ofstream對象,fstream對象;string類輸入輸出流:istringstream對象,ostringstream對象,stringstream對象。以上流類實際上都是模板的特化(template specialization)

2、流插入符(inserter)<<;提取符(extractor)>>

3、按行讀取流:成員函數get();成員函數getline();定義在<string>中的全局函數getline()。前兩個函數都有三個參數:指向字符緩衝區的指針;緩衝區大小;結束字符(默認爲'/n')。遇到結束字符時,均會在緩衝區

末尾存儲一個零。兩者區別:get不讀取結束符,而getline讀取結束符但不放入緩衝區。<string>中的getline爲全局函數,兩個非默認參數:輸入流對象和string對象。讀取界定符(delimiter默認'/n')並丟棄。數據存入

string對象中,故不必擔心緩衝區溢出。

4、處理流錯誤:獲取狀態:eofbit置位--eof()返回真,此時failbit也會置位;failbitbadbit置位--fail()返回真;badbit置位--bad()返回真;goodbit置位--good()返回真。clear()清空流狀態標誌位,且置gootbit位。setstate()設置標誌位,clear()也可設置標誌位。

5、文件打開模式:ios::inios::outios::appios::ate(打開一個已存在的文件輸入/輸出,並將指針移至文件尾)ios::truncios::binary(使用read()write()的時候應該以二進制打開,因爲這些函數以字節爲單位操作;若要使用流指針定位命令也應以二進制打開)

6、每個輸入輸出流對象都包含一個指向streambuf的指針。每個輸入輸出流對象都有一個成員函數rdbuf(),用來訪問streambuf,它返回一個指向對象streambuf的指針。例:cout<<in.rdbuf();

7、輸入輸出流中定位:a、絕對定位:tellp()tellg()獲取當前位置,seekp()seekg()定位;b、相對定位:重載版本的seekp()seekg()函數,兩個參數:第一個爲移動的字符數目,第二個爲移動方向(ios::begios::curios::end)

8、創建一個既能讀文件又能寫文件的方法:法一:fstream io

法二:ifstream in("filename"ios::in | ios::out)     ostream out(in.rdbuf);

9、輸出字符串流:成員函數str()可將輸出流格式化爲string對象,每次調用str()都會返回一個新的string對象。

10、輸出流的格式化:格式化標誌;格式化域;操縱算子(cin>>ws可吃掉空格)

通用容器(看得很粗糙)

1、對容器中元素靈活訪問的方法是使用迭代器:迭代器是一個對象,它作爲一個類也提供了一個抽象層,因此可以將容器的內部實現細節與用來訪問容器的代碼分隔開來。通過迭代器,容器可被看作一個序列,迭代器遍歷該序列而無需知道其基本結構。

2STL編程指南網站:http://www.sgi.com/tech//stl/http://www.stlport.org

3C++中容器分三類:

a 序列容器:僅將元素線性的組織起來,是最基本的容器類型,如vectorlistdeque等;

       b 容器適配器:在基本線性序列上添加一些特殊的屬性,如stackqueuepriority_queue等;

       c 關聯式容器:基於關鍵字來組織元素,如setmapmultisetmultimap等。

4、標準庫中所有的容器都持有存入對象的拷貝,所以這些對象必須是可拷貝構造(具有一個可訪問的拷貝構造函數)和可賦值(具有一個可訪問的賦值操作符)的。

10、當容器中持有的是對象時,容器被銷燬時,其持有的對象也會被銷燬,而若持有的是指針,指針不會被銷燬,需要程序員自行銷燬。

6、使用容器一般方式:

typedef std::容器名<對象類型>   Container; //這樣方便更換容器類型,僅將容器名該一下即可

typedef Container::iterator Iter;      //簡化迭代器的寫法

typedef Contianer::const_iterator;    //這種const()容器中的迭代器不允許反向修改所指向的容器中的元素

typedef Container::reverse_iterator;  //這種可逆容器中的迭代器從序列後向前遍歷

typedef Container::const_reverse_iterator; //可逆const容器產生的迭代器

7、所有容器都有begin()end()成員函數,用來產生選擇序列開始端和超越末尾端的迭代器。可逆容器則擁有成員函數rbegin()rend()。如果容器是const的,則均產生const型的迭代器。

8、判斷迭代器是否到達尾部使用不等於end(!=),不要使用<<=,只有!===測試方式有效。通常將循環寫成如下形式:       for(Iter i = ContianerObject.begin(); i != ContianerObject.end(); i++)

9、通過迭代器訪問容器中元素的方法:使用"*"解析迭代器引用,*i返回容器中持有的任何東西,同時迭代器還可以反向給非常容器中元素賦值,如*i = *i + 1

10、容器適配器(棧,隊列,優先隊列)不支持迭代器。

11、所有基本容器都是可逆容器;

       clear()清楚容器中全部內容;

       resize()擴展一個序列,此時新元素調用默認構造函數初始化,內置類型則置0

       insert()插入單個元素或具有相同值的一組元素或由起始和終止迭代器標識的一組來自其他容器的元素;

       erase()清除序列中一個或一組元素,用迭代器指示。

       push_back()pop_back()listdeque還支持push_front()pop_front()

12vector是一種類似數組但可動態擴展的容器,vector的特點是允許快速隨機訪問其中的元素,其實現也是將存儲內容作爲一個連續的對象數組來維護,因此其索引和迭代操作非常快,同時也導致插入新元素非常慢。

13、使用vector最有效的方式是:a 在開始時用reserve()預分配正確數量的空間,此後不必再重新分配空間;b 僅僅在序列後端添加或刪除元素。reserve()只作內存分配操作,不調用構造函數。

14deque(雙端隊列)("deck")的特點是方便在序列的前後端添加新元素;像vector樣也有operator[]操作符,同時,vector       deque還提供了成員函數at()進行元素隨機訪問,但訪問越界時at()會拋出異常,operator[]不會。

15list(雙向鏈表)的特點是在序列的任何地方快速的插入或刪除元素。如果對象較大,較複雜則使用list比較合適。如果需要頻繁的遍歷序列,則不要用list

17、棧(stack):後進先出;隊列(deque):先進先出;優先隊列(priority_queue):總將具有最高優先級的元素先彈出來。堆(heap)就是一個優先隊列。

18、標準C++不支持多線程處理。

19、關聯式容器將關鍵字與值關聯起來(mapmultimap是,setmultiset因結構相似性也歸爲此類)

16set的主要工作是保持獨一無二的元素,同時對元素排序,因爲set的實現是用一顆平衡樹來存儲其元素。

20map中含operator[]操作符,但當用operator[]查尋一個不存在的值時,map會創建一個新的關鍵字-值對(值使用默認構造函數)

21、所有關聯式容器都有count()find()成員函數。count()告知那個關鍵字在容器中重複存在的數目,find()產生一個指向首次出現給定關鍵字的元素的迭代器,找不到則返回超越尾端的迭代器(end())

設計模式(看的很粗略)

1、設計模式的目標是封裝變化(encapsulate change)

2、對象組合優於類繼承,因爲組合更簡單。《極限編程》(extreme programming)的指導原則之一是只要能用,就用最簡單的

3、模式分類:

a 創建型(Creational):用於封裝對象的創建過程。如單件(Singleton)模式、工廠(Factory)模式、構建器(Builder)模式;

       b 結構型(Structural):封裝對象之間的連接方式。如代理(Proxy)模式、適配器(Adapter)模式。

       c 行爲型(Behavioral):封裝特定操作類型的對象。如命令(Command)模式、模板方法(template Method)模式、狀態(State)模式等。

4、單件(Singleton)模式:限制一個類有且只有一個實例的方法。實現方法是聲明所有構造函數爲私有,並防止編譯器隱式生成任何構造函數。另外,拷貝構造函數和賦值操作符都聲明爲私有,以防止該類的任何複製操作。

5、單件模式中唯一實例的創建可採用靜態創建,也可以使用惰性初始化。

6、單件模式可演化成有限個對象的對象池。

7、命令模式直觀的看就是一個函數對象,將函數封裝成對象進行傳遞。

8、工廠模式:封裝對象的創建。如果想在構造函數中調用虛函數,可用特殊的技術來模擬虛構造函數。實現方法見(卷二P407)

9、虛函數的思想是發送一個消息給對象,讓對象決定要做的正確的事情。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章