C++筆記(2):《數據結構和問題求解》part1筆記


 

  前言:

  C++,數據結構,算法,這些知識在互聯網行業求職過程中是必備的,而本科電路硬件出身的我這些基本就沒學過,也用得比較少,為了以后的飯碗,從現在開始還是花點時間來看下這些東西吧。本節是mark allen Weiss數據結構書籍data structures and algorithm analysis in c++ (second edition)中第一大部分的筆記(隨手寫的,供自己以后參考),這部分主要是講解一些C++的知識,比如面向對象概念,繼承,多態,重載,虛函數,模板,設計模式等等。

 

  Chap1:

  本章主要是講一些array,string,struct,pointer的東西,屬於c++的一些非常基本的數據類型。

  64bit Win7+32bit vs2010經過測試,發現不同數據類型占用的內存大小情況如下:

  unsigned char:1, char:1, unsigned int:4, int:4, long:4,unsigned long:4

  32bit的系統只能說明變量的地址是32bit(即4字節)的,與地址里面的內容所占內存大小無關。

  一個指針也被看成是一個指向object的object。

  array里面保存的是相同數據類型的變量,這些變量可以是結構體,而結構體中又可以用不同的數據類型。

  vector代替原始的array,而string代替原始的strings。

  其中vector和string屬於一流的object,因為它們滿足通用的操作。稍微不同的是vector是個類模板,而string是個類。

  vector使用數組下標訪問時不會進行邊界檢查,為了保險,需要保證它訪問的范圍在它邊界內,所以此時的用法和普通的array沒區別。

  參數傳遞過程中采用引用而不是直接的形參一個原因是可以減小copy實參帶來的開銷。不過直接用引用的話有可能改變原來變量的內容,所以一般情況下都是用常量引用(如果不想原來的參數被改變的話)。

  在STL中沒有對應多維數組的object,不過在OpenCV中的Mat貌似有這個功能,二維的。

  stirng中的c_str指返回一個char*類型,它指向的內容和原來的string一樣。

  對指針變量進行declaration操作時必須保證該指針已經正確初始化過了。

  自動內存分配和動態內存分配是2個不同的概念,自動指的是程序自動執行,而動態指的是手動執行,在需要使用和銷毀的時候聲明,對應的關鍵字為new, delete. 如果pt是個指針,執行delete pt只是把pt指向的那一塊內存給收回了,而pt本身這個變量還是存在的。

  使用指針傳遞時屬於c語言風格,而用引用傳遞時為c++語言風格。引用傳遞時,其實內部已經隱式將地址傳遞進去了,系統會自動每時每刻的解引用。使用引用的好處是我們只需在聲明和定義引用時加入符號”&”,在函數內部實現時不要額外加其它符合,直接采用實值變量即可。在調用函數時也無需加入其它等符號。

  因為struct中變量的數據類型不完全相同,所以我們不能像操作array那樣循環操作它的每個變量。Struct里面包含多個變量類型,作為參數傳遞時一般不采用實值傳遞,因為這樣的開銷太大(主要是參數復制方面)。

  如果ps是指向結構體的指針,a為結構體的一個成員變量,則(*ps).a表示訪問結構體中的變量a,但是這樣看起來比較別扭,所以在c++中提供了一個符號”->”帶代替,此時的表達為ps->a.

 

  Chap2:

  類中定義的操作類型不能超過c++內嵌的數據操作類型范圍。

  Object是一個原子單元,不可分割的。類的性質如下:信息隱藏性,封裝性,代碼復用性,模板機制,繼承機制,多態性(作為繼承性的一部分實現)。

  Object-oriented programming:面向對象編程。

  Object-based programming:基於對象編程,該思想也有封裝,信息隱藏,代碼復用等機制,但是不包括多態和繼承性質。

  因為在執行類的構造函數時是先依次進行變量的所有內存分配,然后才進行變量的賦初值。一般類的數據成員都有其默認的賦值機制,如果需要更改其默認的值,可以在構造函數體中實現。但是有些變量,比如const變量,它需要在定義它(即分配內存)的時候給它賦初值,但是代價函數是首先分配完所有的內存,然后再賦值,明顯不符合const變量初始化的要求。因此const變量的初始化只能在構造函數后的冒號初始化列表中進行,該處的優先級比構造函數本體的還要高。另一個例子就是類中有類的數據成員,並且它沒有默認的初始化函數,因此這個類的初始化也應該在初始化列表中進行。

  關鍵字explicit一般在構造函數前使用,且一般是單參數的構造函數,因為在C++中,如果一個類的構造函數是單參數的,它是允許直接給一個類的對象賦數值的,這實際上是系統首先定義了一個類的臨時對象,並用該數值作為類的構造函數參數,然后將這個臨時對象賦值給新的類對象。如果使用了explicit關鍵字后,系統(編譯器)就不再允許直接將數字賦值給類對象了。

  const主要用在3個地方:函數參數傳遞時,函數返回值,函數參數括號后。

  在類中,析構函數,copy構造函數,賦值操作符,稱為big three,這3個函數在系統內部已經有了個默認的實現,一般情況下我們不需要更改它。

  析構函數主要是釋放類中的變量,這些變量包括構造函數中定義的以及其他成員函數定義的,還有就是那些在類中使用new后的所有變量,它也關閉那些打開了的的所有文檔。

  *this表示當前的對象。

  當類的數據成員有指針變量時,一般情況下需要重寫Big Three。其中的析構函數需要用delete收回對應的指針變量。Copy構造函數需要滿足deep copy操作。

  將需要初始化特定值的變量盡量放在初始化列表中初始化,這樣避免額外的賦值開銷,因為那些不再初始化列表的變量的初始化過程都是在0-參數的構造函數中進行初始化的,如果你在自己重載的構造函數體中重新進行初始化,就相當於了2次賦值,並且有可能引起一些內存問題(上面的理解可能有問題)。數據成員的初始化順序不是依照初始化列表進行的,而是依照在類中聲明的次序進行的,因此我們要盡量避免定義那些相互依賴的變量。

  下面的情況下數據成員一定要在初始化列表中進行初始化:常量;引用變量;沒有0-參數構造函數的變量。

  C函數中常犯的一個錯誤是返回一個指針(函數內部的)給一個動態變量,c++函數中常犯的一個錯誤是返回一個引用給動態變量。

  在c++中,下面5個操作符是不能重載的:”.”, ”.*”, ”?:”, ”::”, ”sizeof”.並且操作符重載時只能重載c++中現有的操作符,不能重載新的操作符,且重載時其參數的個數不能夠改變。總的來說重載操作符不能改變原先操作符的語法和優先級。

  本來由於類的信息隱藏特性,其私有變量是不能被類對象調用的,它只能被內部的成員函數調用,但是如果有一個函數是該類的友元函數(一般情況下該函數不是該類的成員函數,而是外部函數),則在該函數內部是可以調用類中所有成員的。實際使用情況如下:當類A的對象B作為一個參數傳入到函數f(A的友元函數)中時,在函數f的內部可以調用B中的所有成員,包括它的private成員(正常情況下,其對象時不能調用私有成員的)。

  一般情況下盡量少用友元函數,可以考慮使用全局函數,類靜態成員變量,類枚舉變量等代替友元函數。

  錯誤處理是c++類的設計中是個難題,一般情況下采用異常處理模式。異常處理常用的是try-catch語句,try有測試的意思,因為在try中實現的代碼有可能有常識性的錯誤,自己在try語句里添加那些不符合邏輯的錯誤並throw一個整數值(其它類型的值也可以),這個整數可以理解為錯誤代號。然后在try語句后直接(不用分號)接catch函數,這個函數帶有參數,其參數為可以為任何數據類型,如果不知道是什么類型,則可以采用通用符號“…”,然后在catch語句中實現對應的錯誤處理程序。

  系統自動調用new[]一般發生在構造函數,賦值操作符=,+=中,系統自定調用delete[]一般發生在析構函數中。

  函數值返回一個引用就是返回一個別名,返回一個常量引用就是表面返回的這個別名(所代表的值)不能被修改。

  操作符重載時的函數名為operator+,operator-等,即operator和操作符一起構成的。

  copy constructor與operator=的不同之處在於,在使用operator=時需要賦值符號兩邊的object都已經創建了,它是一個個元素進行賦值的。而copy constructor機制主要用於構造函數的copy,函數參數以value傳遞,函數返回值也為value的情況。

 

  Chap3:

  類模板的出現主要是為了代碼的重復利用。一般是針對類型獨立的場合。

  模板分為函數模板和類模板。模板中實現的一般是通用的算法,簡單的模板思想是:用typedef定義一個通用類型,然后用通用類型來實現函數內部。

  在使用函數模板時,需要在使用前進行目標的聲明,一般情況下如下所示:template<class anyType>

  其中聲明過程用尖括號,且后面不需要加分號,直接接函數或者類的實現過程,其中的anyType表示模板中可以識別的類型等,且目標中類別的個數一般不會太多,1到2個是最常見的。

  使用類模板的基本原理和函數模板相同,但是有3個地方需要注意:1. 在類的實現前也需加入上面的模板聲明語句,且在類的外面實現類中的成員函數時,如果用到了類模板參數anyType,則每個函數實現前都需要加入類模板聲明。2. 在類外面實現類中的成員函數,除了前面要加入類模板聲明語句外還需要在類的域操作符”::”前也加入類模板參數<anyType>. 3. 在用帶模板的類初始化一個對象時,需要給類模板參數一個確定的參數,也是用尖括號括起來。

  類模板的目的是為了實現一個通用的類,這樣當不同的類型傳入時都可以完成其想要的功能,體現了代碼的重用性。不過還有一種模板叫做特殊模板,即它只適用於某個特定的類型,它的類模板聲明語句為:template<>,里面是空的。這種使用方法一般是跟在一個通用類模板后面,因為它的模板參數為空,所以在類的實現時其模板要傳遞具體的類型。這樣當新建一個目標類時,如果它屬於哪個具體的類型(即特殊模板),則按照特殊模板來處理,否則就屬於通用模板了。

  在老版本的類模板的實現代碼時,類模板的實現也是放在.h文件中的,因為當時分開編譯會出現一些問題,且在對應的cpp文件在實現每個函數之前需要加入類模板聲明。

  類模板有缺省的參數,也可以有多個參數。

 

  Chap4:

  類之間常見的關系有IS-A,HAS-A。其中類的繼承屬於IS-A。

  三種繼承方式如圖所示:

  

  指向基類的指針或者引用是可以指向其子類對象。

  公有繼承時父類的隱藏屬性在子類中是不變的,私有繼承時,父類中所有的成員在子類中都是私有的。即使是公有繼承,其父類的構造函數和析構函數是不能夠被子類所繼承的,但是在子類進行對象化時,由於用到了父類,所以其父類必須先建立,這時候父類的構造函數也會被使用,但子類銷毀時,先執行的是子類的析構函數,然后才執行父類的析構函數。

  當把父類中的任何一個成員放在了子類的private區時,表示這些成員在子類中是不能再被使用的,如果將其放在public區時,就需要覆蓋重新實現這些成員。

  類的protected成員只對它自己和它的子類開放,對其它類不開放。

  子類構造函數的初始化列表中可以直接通過函數名調用基類的構造函數,如果此時子類的構造函數體內容為空,則表明直接使用父類的構造函數。

  子類的設計一般不需要再考慮父類的那些私有成員,因此父類中私有成員的更改對子類的設計毫無影響。

  重寫分為2種,一種是直接重寫成員函數,一種是重寫虛函數,但是只有重寫虛函數才是體現類的多態性。多態性的目的是為了實現接口重用。

  虛函數有一個好處是可以直接用父類的指針調用對應的子類的函數,而不需要用子類對象去調用。非純虛函數的情況下,如果子類沒有實現對應的虛函數代碼,則子類對象調用的是父類的那個函數內容。

  要真正實現多態(采用指針或引用訪問時),應該將virtual關鍵字放在父類中聲明,放在子類中是無效的。

  即使在父類中,構造函數,析構函數,賦值操作,拷貝構造函數等放在public區,且子類也是public方式繼承父類的,如果子類中沒有重新定義自己的這幾個函數,那么系統會自動為子類這幾個函數采用默認的方法構造,而不是用父類的,因為子類不繼承這些。

  在類的設計中,通常情況下是將構造函數定義為非虛的,而將析構函數定義為虛函數。

  含有至少一個抽象方法(一般叫做純虛函數,后接”=0”來代替函數體)的類叫做抽象類,抽象類是不能用來實例化對象的,它的子類如果沒有實現對應的抽象方法則也不能用來實例化對象,因此它的構造函數平常是用不到的,除非子類調用來初始化父類的一些變量。不過可以定義指向抽象類的指針或者引用來指向它的子類(非抽象的子類)。

  類中函數的默認參數都是靜態綁定的,一般情況下其它的成員函數(虛函數)地址也是靜態綁定的。

  函數重載:在一個類中,函數與函數的函數名相同,參數類型或者參數的個數不同,對函數的返回值不一定需要完全相同,只需兼容即可。

  函數覆蓋:在基類和派生類中,基類的函數必須是虛函數,兩個類中的函數與函數的函數名相同,參數類型和個數也完全相同,函數的返回值類型也要求相同。

  函數隱藏:在基類和派生類中,兩個類中的函數名相同情況下,不滿足函數覆蓋的所有情況都是函數隱藏。

  公有繼承體現的是IS-A關系,而私有繼承體現的是HAS-A關系。一般情況下盡量少使用私有繼承方式。並且默認情況下子類的繼承都是私有繼承,所以在子類繼承為非私有繼承時不要忘了添加對應的關鍵字。

  父類的友函數原則上不是子類的友函數,不過父類的友函數可以訪問子類中從父類公共繼承的那些成員。

  如果要體現類的多態性機制,則一般情況下不要將對象當做值傳遞。

  類應用中的composition機制指的是一個類代碼的實現過程中會調用另一個類對象,最常見的情況就是將一個類對象作為參數傳遞到另一個類的構造函數中。

    多態性指相同對象收到不同消息或不同對象收到相同消息時產生不同的實現動作。C++支持兩種多態性:編譯時多態性,運行時多態性。編譯時多態性:通過重載函數實現;運行時多態性:通過虛函數實現。 

 

  Chap5:

  一個設計模式類似於一個菜譜,它有模式的名字,問題的描述,問題的解決方法和結果描述。

  如果我們要實現在一個比較大小的函數中,其參數要滿足各種數據類型,我們可以在每個對應的類中實現比較操作符”<”,這樣每實現一個類就需要重載一次操作符。假設我們不希望在類的內部實現該功能,就應該在類外部實現該操作符,但是一旦這個操作符函數定了,就只能有一個並不能滿足所有的類型的情況了。所以另外一種實現方法就是在類的外面實現多個比較函數,這個函數就叫做functor。Functor的大致的意思就是把一個類對象當函數一樣使用,當然這個類需要重載()操作符。因為他比較像函數,所以得了一個叫Funtor的名字,它來自於commond設計模式。一個functor僅僅包含一個方法,沒有包含數據成員,所以將它當為參數傳遞時需要以值傳遞的方式進行。Functor也就是我們常說的函數句柄,可以用類來實現。這樣傳入完成比較功能的函數的參數為數據集以及這個函數類了。

  C++標准中有一個wrapper指針類名為auto_ptr,這個指針能夠自動的動態刪除內存。一般指針內存的刪除發生在下面3種情況:1. 函數內部new的局部變量。 2. 函數內部new的局部變量指針且作為返回值,該返回指針在另外一個函數中被調用,當調用它的函數結束時,需要釋放new過的內存。 3. New后的一個指針作為參數傳入一個函數中,而在這個函數的內部使用了delete掉這個參數指針。auto_ptr的內部有一個指針以及一個owner的指示變量,如果是這個指針的”主人”, 則析構函數時需要刪除對應的指針內存,如果復制操作發生,則轉移”主人”的身份。因此auto_ptr屬於一個指針wrapper。常量引用wrapper主要是用來處理引用指向NULL的情況。

  wrapper側重於包裝,而adapter側重於接口之間的轉換。

  為了解決類型雙向轉換帶來的編譯問題,我們在類的構造函數中盡量使用explicit關鍵字。

  指針可以指向一個目標或空對象,而引用只能指向一個目標,不能為NULL。

  當兩個類之間相互調用時,需要一個不完全的類聲明。

  Iterator模式是用來解決一個聚合對象的遍歷問題,將對聚合的遍歷封裝到一個類中進行,這樣就避免了暴露這個聚合對象的內部表示的可能。最直接的想法是遍歷一個容器,而不用考慮該容器內部的元素類型。

  迭代器的設計需要滿足自己有hasNext和next方法,並且對應的容器有getIterator的類型轉換方法。

  factory模式出現的原因:1. 一個基類指針可以指向任意它的子類對象,但是每次都需要用new來創建子類,且需知道子類的名字,這樣程序的擴展和維護會變得麻煩。2. 當類A中要用到基類B,且需要初始化B的一個子類,但是此時A並不知道該初始化哪個子類,不過A的子類知道。因此Factory提供了2個重要的功能:定義創建了類的接口,封裝了對象的創建;使得具體類的工作延遲到了子類當中。在factory模式中,每個容器都可以返回一個指向抽象類的迭代器。

  Composite模式主要是當一個函數需要返回多個參數時(2個參數的最為常見)可以采用,比如說pair,pair常用於構造map和dictionary。

  Observer模式主要是解決一對多的依賴關系,當”一”(subject)變化了,則對應的多個依賴(obverser)也相應變化。常見例子就是excel圖表中一組數據可以有多個可視化結果,當其中數據改變了時,則這些可視化結果也跟着改變。一般情況下obverser有更新的功能。

 

  參考資料:

  data structures and algorithm analysis in c++ (second edition),mark allen Weiss.

  23種設計模式(C++)

      從零開始學C++之繼承(一)

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM