1. 程式人生 > 實用技巧 >C++知識樹整理——C++面向物件基礎

C++知識樹整理——C++面向物件基礎

C++ 面向物件基礎

  • [x] 建構函式

    • 類被建立時被呼叫的方法,可以用構造特定的初始化方法type(i)來初始化資料,比如傳統賦值先初始化物件在賦值,這個方式可以初始化的時候就賦初值
    • 父類的建構函式會在子類構造前被呼叫,先建立父類再建立子類
  • [x] 三劍客(類中有指標的時候必須複寫這三個函式否則會造成記憶體洩漏)

    • 複製建構函式

      • 在記憶體中開闢出對應空間之後拷貝一份目標物件存入自身
    • 拷貝賦值函式

      • 先判斷是否是自我賦值,是直接return(如果沒有自我賦值檢測,那麼自身物件的data將被釋放,data指標指向的內容將不存在,所以該拷貝會出問題。)
      • 如果不是先釋放自身的資料後再賦值拷貝目標物件,同時返回引用保證支援連續賦值。
    • 解構函式

      • 在物件被銷燬時呼叫,釋放資料
      • 子類的析構會在父類析構之後才被呼叫
  • [x] 深複製和淺複製

    • 由於編譯系統在我們沒有自己定義拷貝建構函式時,會在拷貝物件時呼叫預設拷貝建構函式,進行的是淺拷貝!即對指標name拷貝後只是簡單的將指標指向目的地址,則會導致兩個指標指向同一個地址,就上面說的類中有指標的時候必須要實現“三劍客”否則假設同一個地址的記憶體釋放兩次,第一次能夠正常釋放,第二次解除安裝的時候因為已經解除安裝了物件再次解除安裝程式就不可控了
    • 所以,在對含有指標成員的物件進行拷貝時,必須要自己定義拷貝建構函式(上面的三劍客),使拷貝後的物件指標成員有自己的記憶體空間,就是深拷貝
  • [x] 智慧指標

    1. 原理:
      • 智慧指標在語法上有三個很關鍵的地方,第一個是儲存的外部指標,在內部宣告一個指標如T* px,這個指標將代替傳入指標進行相關傳入指標的操作
      • 第二個是過載“*”運算子,解引用,返回一個指標所指向的物件
      • 第三個是過載“->”運算子,返回一個指標,對應於上圖就是px
    2. 應用:
      • c++的迭代器就是一種智慧指標,迭代器過載的“ * ”和“->”,"++"運算子,內部維護了一個 _list_node * node的指標
      • 迭代器的智慧指標主要是對指標操作做過載,為什麼要過載呢?因為原來的指標太“蠢”了,普通的++只是對原來的記憶體地址+1,指向的是記憶體的下一個地址,但是迭代器想要的是指向下一個元素的地址,假設是順序陣列就還能做到,但是如果是連結串列list就無法支援了,所以封裝成了智慧指標,在++的操作時其實是獲取了當前節點的next指標的位置,將指標指向了next指標指向的元素位置,完成了智慧的++
  • [x] C++中的explicit

    • C++中, 一個引數的建構函式(或者除了第一個引數外其餘引數都有預設值的多參建構函式), 承擔了兩個角色。 1 是個構造器 ,2 是個預設且隱含的型別轉換操作符。所以, 有時候在我們寫下如 AAA = XXX, 這樣的程式碼, 且恰好XXX的型別正好是AAA單引數構造器的引數型別, 這時候編譯器就自動呼叫這個構造器, 建立一個AAA的物件。 但在某些情況下, 卻違背了我們的本意。 這時候就要在這個構造器前面加上explicit修飾, 指定這個構造器只能被明確的呼叫/使用, 不能作為型別轉換操作符被隱含的使用。
  • [x] 函式的傳值和傳址

    • 傳值傳遞的是資料的值,接收端會用一個新的變數接收這個資料的值,等於拷貝了一份給函式使用
    • 而傳址等於傳遞了這個資料的引用(指標),接收端接收了這個引用對這個引用的修改會直接修改傳遞端的物件本身
  • [x] 虛擬函式和虛表

    • 如果一個物件中有虛擬函式就會再這個物件的記憶體中開闢一個4位的記憶體空間來儲存一個虛指標
    • 虛指標指向一個虛表,這個虛表是一個指標陣列,指標指向了在記憶體中的虛擬函式地址,指標下標取決於在宣告時的函式位置
    • 子類繼承父類也同時繼承了父類的虛指標和虛表,但是裡面所指向的虛擬函式可以通過子類的複寫來修改指向的函式指向新複寫的函式地址
    • 虛擬函式和虛表是面向物件很重要的一個部分,通過他實現了動態的物件和方法的指定,這便是面向物件繼承多型的本質
    • 虛擬函式如果用C語言的程式碼解釋則是:(*p->vptr[n])(p)
  • [x] Const的作用

    • 在函式前宣告代表這個函式體內不會對類中的資料進行操作
    • 在變數前宣告代表接下來的操作不會改變這個變數的值
  • [x] new和delete的使用,和malloc,free的區別

    • new
      • 分配記憶體
      • 轉型(將malloc獲得的物件轉型為目標物件)
      • 呼叫建構函式
    • delete
      • 呼叫解構函式
      • 釋放記憶體
    • 過載new和delete
      • new[] 釋放時就必須 delete[] 否則只會釋放一次(陣列第一個)而其他的資料則會記憶體洩漏
      • new[]時過載的size比實際的物件大小*個數還要 + 4,因為需要一個int位來儲存陣列長度
      • new和delete底層其實就是呼叫了malloc(n)和free(ps)
      • 可以通過操作符過載重新定義new和delete,就可以在單純的malloc之前自定義自己的操作,比如做快取池
      • 在stl庫中,base_string的實現就過載了new和delete的操作符過載,new string物件時其實並不是new了這個物件,而是內部new了一個Rep物件並封裝了自定的物件頭來方便對物件做特定的操作。
    • 過載new()和delete()
      • 可以過載多個版本的new(...),但是前期是每個版本都必須要有明確的引數序列,且其中第一個引數必須要是size_t
      • 也可以過載delete() ,寫出的多個版本並不會被delete所呼叫,只有當new所呼叫的ctor(建構函式)丟擲異常時才會呼叫這些過載的delete()
      • 即使new(...)和delete(...)並未能一一對應,也不會出現任何報錯,程式會認為你放棄處理建構函式所丟擲的異常。
  • [x] 虛解構函式的作用

    • 定義:

      • 總的來說虛解構函式是為了避免記憶體洩露,而且是當子類中會有指標成員變數時才會使用得到的。也就說虛解構函式使得在刪除指向子類物件的基類指標時可以呼叫子類的解構函式達到釋放子類中堆記憶體的目的,而防止記憶體洩露的
    • 單一一個類物件加不加虛擬函式對於解構函式來說看不出區別,虛解構函式的作用體現在繼承上,現在我們假設實現的類如下:

      #include<iostream>
      using namespace std;
      
      class ClxBase
      {
      public:
      ClxBase() {};
      virtual ~ClxBase() { cout<<"delete ClxBase"<<endl; };
      
      virtual void DoSomething() { cout << "Do something in class ClxBase!" << endl; };
      
      };
      
      class ClxDerived : public ClxBase
      {
      public:
      ClxDerived() {};
      ~ClxDerived() { cout << "Output from the destructor of class ClxDerived!" << endl; };
      
      void DoSomething() { cout << "Do something in class ClxDerived!" << endl; };
      
      };
      
      int main(int argc, char const* argv[])
      {
      ClxBase *pTest = new ClxDerived;
      pTest->DoSomething();
      delete pTest;
      return 0;
      }
      
      • 當父類的解構函式加virtual關鍵字,輸出的結果為:

        Do something in class ClxBase!
        Output from the destructor of class ClxDerived!
        delete ClxBase
        

        當父類的解構函式宣告成虛解構函式的時候,當子類繼承父類,父類的指標指向子類時,delete掉父類的指標,先調動子類的解構函式,再調動父類的解構函式。

      • 當父類的解構函式不加virtual關鍵字,輸出的結果為:

        Do something in class ClxBase!
        delete ClxBase
        

        當父類的解構函式不宣告成虛解構函式的時候,當子類繼承父類,父類的指標指向子類時,delete掉父類的指標,只調動父類的解構函式,而不調動子類的解構函式。

      • 當然,並不是要把所有類的解構函式都寫成虛擬函式。因為當類裡面有虛擬函式的時候,編譯器會給類新增一個虛擬函式表,裡面來存放虛擬函式指標,這樣就會增加類的儲存空間。所以,只有當一個類被用來作為基類的時候,才把解構函式寫成虛擬函式。

  • [x] ++的區別

    • ++i是先進行+運算後在進行邏輯運算
    • i++是邏輯運算後才進行邏輯運算
    • 從操作符過載層面來看,前++和後++都是過載++這個運算子,所以怎麼區分呢,c++就定義了,如果是operator++()則是前++,先進行++操作的複寫,如果是operator++(int)則是複寫後置++
  • [x] 引用(reference)和指標的關係

    • 記憶體層面看
      • 引用是被編譯器封裝過的指標
      • 引用的大小和資料的大小一致(編譯器封裝),指標的大小預設為4
      • 引用的地址和資料地址一致(編譯器封裝)
    • 使用層面看
      • 引用可以看做是某個被引用變數的別名
      • 引用不能被提前宣告比如 int& x; 必須要準確給出賦值並且賦值後不可再修改
      • 引用常用於引數的宣告和返回型別的指定
  • [x] 仿函式

    每個仿函式都是某個類過載“()”運算子,然後變成了“仿函式”,實質還是一個類,但看起來具有函式的屬性。每個仿函式其實在背後都集成了一個奇怪的類,這個類不用程式設計師手動顯式宣告

  • [x] namespace

    • 防止類與類之間重名的問題,也控制了方法和變數的作用域
    • 經驗上講,最好每個類都用對應的namespace括起來
  • [x] this指標

    • this指標其實可以認為是指向當前物件記憶體地址的一個指標,如基類和子類中有虛擬函式,一個方法Func中呼叫了虛擬函式的Serialize()方法,則this->Serialize()將動態繫結,等價於(*(this->vptr)[n])(this)。可以結合上虛指標和虛擬函式表來理解。

參考資料: