天天看點

C++面向對象程式設計Part IIC++程式設計兼談對象模型 Part II

C++程式設計兼談對象模型 Part II

注:此篇文章是根據侯捷老師的課程所做的筆記。

本課程是上一門課程“面向對象程式設計”的續集,将探讨上一門課程未讨論的主題。上一門課程筆記:

 連結: C++面向對象程式設計 (C++Object-Oriented Programming) Part I.

目錄索引

  • C++程式設計兼談對象模型 Part II
    •  Conversion function(轉換函數)
    •  Non-explicit one-argument constructor
    •  Pointer-like classes,關于智能指針
    •  Pointer-like classes,關于疊代器
    •  Function-like classes,所謂仿函數
    •  标準庫中的仿函數的奇特模樣
    •  namespace經驗談
    •  class template,類模闆
    •  function template,函數模闆
    •  member template,成員模闆
    •  specialization,模闆特化
      •   partial specialization,模闆偏特化——個數的偏
      •   partial specialization,模闆偏特化——範圍的偏
    •  template template parameter,模闆模闆參數
    •  variadic templates(since C++11)
    •  auto(since C++11)
    •  ranged-base for(since C++11)
    •  reference
    •  Composition(複合)關系下的構造和析構
    •  Inheritance(繼承)關系下的構造和析構
    •  Inheritance+Composition關系下的構造和析構
    •  對象模型(Object Model):關于vptr和vtbl
    •  對象模型(Object Model):關于this
    •  對象模型(Object Model):關于Dynamic Binding
    •  談談const
    •  關于new,delete
      •   重載 ::operator new/new[],::operator delete/[]
      •   重載 member operator new/delete
      •   重載 member operator new[]/delete[]
      •   重載new(),delete()
      •   basic_string使用new(extra)擴充申請量

 Conversion function(轉換函數)

C++面向對象程式設計Part IIC++程式設計兼談對象模型 Part II

 上圖中黃色背景的部分即為轉換函數,轉換函數通常的格式為:“operator type() const;”,函數沒有參數,也沒有傳回類型,通常轉換函數不會改變資料,函數後面加const。

他是如何被調用的呢?

 上圖下方有**“double d = 4 + f”,首先編譯器會在全局範圍找是否重載了加号操作符“4 + f”,若沒有此重載,則進一步找是否有轉化函數,若轉化類型後能進行此操作,那麼就利用轉換函數進行轉換,上圖中将調用operator double()**将f轉為0.6。

 上圖中存在一些錯誤,函數體内計算double值時會計算錯誤,并不會計算出0.6而是0.0。忽略其錯誤,感受其操作。

 Non-explicit one-argument constructor

C++面向對象程式設計Part IIC++程式設計兼談對象模型 Part II

 執行Fraction d2 = f + 4;,可以調用Fraction的構造函數将4轉為Fraction(4, 1);,之後調用operator+将兩個Fraction類加起來。

 與上一節的例子相反,上一節是将對象轉為數,這節則是轉為對象。

C++面向對象程式設計Part IIC++程式設計兼談對象模型 Part II

 在類内部添加一個轉換函數Fraction d2 = f + 4;,編譯器會報錯,原因是編譯器不知道是利用轉換函數(double())将f轉換為數還是利用構造函數将4轉換為類對象,産生了二義性。

 我們注意到,在上一節中,類内也是有相同的構造函數和轉換函數(double()),為什麼上一節的代碼就能通過呢?原因在于上一節的類内沒有重載operator+,是以類之間不能進行加法操作,也就不能将4轉為類對象,是以隻有一條路可以走,那就是利用轉換函數(double())将f轉為數。

C++面向對象程式設計Part IIC++程式設計兼談對象模型 Part II

 這種将一個數轉換為類對象的操作有時候是我們不需要的,我們需要杜絕這種現象,那就是在構造函數前面加explict關鍵字,這樣就不會把其他類型轉為該類的對象,隻允許通過構造函數調用。

 上圖中**Fraction d2 = f + 4;**編譯出錯,原因是4不允許被轉換,隻能f轉換為double類型,那麼f+4為double類型,d2也為double類型,d2不能轉為Fraction類對象,是以編譯出錯。

 explict關鍵字通常作用于構造函數前,用于禁止隐式的類型轉換。

C++面向對象程式設計Part IIC++程式設計兼談對象模型 Part II

 上圖為在标準庫中使用轉換函數的例子。

 Pointer-like classes,關于智能指針

C++面向對象程式設計Part IIC++程式設計兼談對象模型 Part II

 智能指針就是一個類,類裡面包含了指針,同時也包含了一些其他功能。

 Pointer-like classes,關于疊代器

C++面向對象程式設計Part IIC++程式設計兼談對象模型 Part II
C++面向對象程式設計Part IIC++程式設計兼談對象模型 Part II

 疊代器封裝了指針,相比于智能指針又重載了其他一些操作符。

 Function-like classes,所謂仿函數

C++面向對象程式設計Part IIC++程式設計兼談對象模型 Part II

 仿函數,實際上是一個類,執行時像函數,類裡面通常重載了**()**操作符,這些類的對象又稱函數對象,因為它們像函數。

 标準庫中的仿函數的奇特模樣

C++面向對象程式設計Part IIC++程式設計兼談對象模型 Part II

 仿函數繼承了一些奇特的類。這些類(下圖所示)沒有資料,隻有一些定義,是以類的大小為1。

C++面向對象程式設計Part IIC++程式設計兼談對象模型 Part II

 namespace經驗談

namespace的意義:

 為了把一些東西區分過來,避免類名、函數名和變量名的沖突,取個名字将它們包起來。

 下圖就是namespace的定義以及如何使用。

C++面向對象程式設計Part IIC++程式設計兼談對象模型 Part II

 class template,類模闆

C++面向對象程式設計Part IIC++程式設計兼談對象模型 Part II

 function template,函數模闆

C++面向對象程式設計Part IIC++程式設計兼談對象模型 Part II

 函數模闆可以不用顯示說明參數類型,函數模闆能自動推導,類模闆則不行。為什麼這樣?

 若是類模闆沒有指定類型,那就會這樣** complex a**,定義了一個對象,編譯器不知道這個類的類型,不知道是int還是double,是以編譯器不知道它的大小就沒法建立記憶體,也就沒法建立對象,是以類模闆必須顯示的說明類型。

 而函數模闆不同,它可以自動推導出類型,并且調用函數是開辟一個棧,不用确定配置設定給它記憶體大小。

 member template,成員模闆

C++面向對象程式設計Part IIC++程式設計兼談對象模型 Part II

 成員模闆:在類模闆中,成員函數是一個函數模闆。在上圖中,類模闆pair,類的構造函數又是一個函數模闆。

C++面向對象程式設計Part IIC++程式設計兼談對象模型 Part II

 上圖中,**pari<Derived1, Derived2> p;執行的是類模闆,其中T類型為Derived。接下來pari<Base1, Base2> p2§;**調用拷貝構造函數,這裡T類型變為Base,U的類型為Derived。即将一個鲫魚和麻雀構成的pair§,拷貝到一個由魚類和鳥類構成的pair(p2)中,就是父類指針指向子類對象,這樣是可以的。但反之是不可以的,因為魚類不屬于鲫魚,鳥類也不屬于麻雀。

C++面向對象程式設計Part IIC++程式設計兼談對象模型 Part II

 在智能指針類中,該類也封裝了成員模闆,為的是允許父類指針指向子類對象。

 成員模闆通常使用在構造函數中,使用類使用更靈活,更有彈性。

 specialization,模闆特化

C++面向對象程式設計Part IIC++程式設計兼談對象模型 Part II

 模闆特化就是把泛華模闆中的類型确定下來。

上圖中的第一個框的内容就是泛華的模闆,第二個框就是确定了類型之後特化的模闆,注意形式:template <>,聲明一下是模闆,但特化後<>内不需要給泛化類型;函數名稱後面加入特化的類型:hash<char>,hash<\int>,hash<long>,函數調用時如果類型在特化模闆中有會直接模闆特化後的類。**hans<long> () (1000);**會直接調用hash<long>模闆,其中第一個小括号表示匿名類型,第二個小括号調用的是類中重載的()操作符。

  partial specialization,模闆偏特化——個數的偏

C++面向對象程式設計Part IIC++程式設計兼談對象模型 Part II

 模闆偏特化——個數的偏指的是在模闆中,裡面聲明的類型有多個,其中将某些類型确定下來,例如上圖中将bool類型确定下來,其餘類型依然待确定。其中如果某些類型确定了,必須将确定的類型寫在前面,将不确定的類型寫在後面。

  partial specialization,模闆偏特化——範圍的偏

C++面向對象程式設計Part IIC++程式設計兼談對象模型 Part II

 模闆偏特化——範圍的偏指的是原來模闆類型是任意的,現在限制類型的範圍,不讓它再是任意類型了,上圖中将任意類型限制在指針範圍内,其中**C<string> obj1;**調用的是第一個類模闆,**C<string*> obj1;**調用的是第二個類模闆。

 template template parameter,模闆模闆參數

 模闆模闆參數指的是模闆的參數為模闆。

C++面向對象程式設計Part IIC++程式設計兼談對象模型 Part II

 上圖中,模闆類XCLs第一個模闆參數為類型T,第二個參數為容器類的模闆,在模闆内部定義的對象Container<T> c,T類型就是模闆的第一個參數,Container為第二個參數的容器類型。在定義時XCLs<string, List> mylst1;,表示XCLs為List容器,容器裡面的類型為string,但這樣定義是錯誤的,原因是容器需要好幾個模闆參數,第二種定義是正确的。

C++面向對象程式設計Part IIC++程式設計兼談對象模型 Part II

 以上是關于智能指針的定義。

C++面向對象程式設計Part IIC++程式設計兼談對象模型 Part II

 在上面圖檔的例子中,該類模闆不屬于模闆模闆參數,原因在于定義**stack<int, list<int>>時,其中内部第二個參數list<int>**已經被寫死了,不是模闆類型了。

 variadic templates(since C++11)

C++面向對象程式設計Part IIC++程式設計兼談對象模型 Part II

 variadic templates為數量不定的參數模闆,參數不确定個數用**"…"**表示。

 上圖中,模闆print含有一個類型T以及不确定個數的類型Types,其中關鍵字typename後面有三個句号,函數内部列印第一個參數,然後遞歸調用自己,讓剩下參數的第一個成為typename T類型,列印它并繼續進行遞歸,直到沒有參數了,print将調用上面重載的函數結束此次運作。

 auto(since C++11)

C++面向對象程式設計Part IIC++程式設計兼談對象模型 Part II

 關鍵字auto能夠自動推導出變量類型,當你想少寫些代碼或者不關心它的類型的時候,可以使用auto。但是不能用auto去定義類型,就像上圖中下面為變量ite定義的操作,這樣時錯誤的。

 ranged-base for(since C++11)

C++面向對象程式設計Part IIC++程式設計兼談對象模型 Part II

基于範圍的for循環:

 如上圖,for循環的新文法,可以挨個周遊容器,并且可以通過值或者通過引用來周遊容器内的元素。如果需要改變容器内的元素時,可以通過引用周遊來解決,文法是在類型後面加&,如auto&。

 reference

C++面向對象程式設計Part IIC++程式設計兼談對象模型 Part II

 建議引用在定義時一定要初始化,它要綁定到一個位址上,并且綁定完之後隻能指向這個位址,不能改變,是一個指針常量。

C++面向對象程式設計Part IIC++程式設計兼談對象模型 Part II

 對象和其引用的大小位址是相同的,這其實是一種假象,引用就是指針,在函數傳遞時也不會傳遞對象對象大小的記憶體,就是指針的大小,4位元組(32bit)或8位元組(64bit)。

C++面向對象程式設計Part IIC++程式設計兼談對象模型 Part II

 上圖是分别以指針、值、引用傳遞時的定義以及調用時的文法。

 引用通常不用來聲明變量,而是用于參數傳遞和傳回類型的描述。

 在上圖中下方的部分,定義了兩個重載函數,它們傳遞的參數類型不同,一種是引用一種時值,這種重載是不允許的,因為在調用時,它們的文法是一樣的,編譯器不能分辨該使用哪個函數。但重載指針傳遞與值或重載指針傳遞與引用傳遞是可以的。

 另外,const關鍵字(寫在函數括号外面,上圖中的灰色區域)可以用于重載函數的區分,調用時可以用常量進行區分。

#include<iostream>  
using namespace std;  
   
class Test  
{  
protected:  
    int x;  
public:  
    Test (int i):x(i) { }  
    void fun() const  
    {  
        cout << "fun() const called " << endl;  
    }  
    void fun()  
    {  
        cout << "fun() called " << endl;  
    }  
};  
   
int main()  
{  
    Test t1 (10);  
    const Test t2 (20);  
    t1.fun();  //調用void fun() 函數,列印fun() called
    t2.fun();  //調用void fun() const ,列印fun() const called 
    return 0;  
}
           

 但,const用于修飾函數參數傳遞時不可以重載,即void fun(int a)和void fun(const int a);,這兩個函數實際上沒有差別,因為函數調用的時候,存在形實結合的過程,是以不管有沒有const都不會改變實參的值。

詳細請看:連結: C++中const用于函數重載.

 Composition(複合)關系下的構造和析構

這一部分在part I中有描述,這裡不再較長的描述。

 構造:先複合,後自己

 析構:先自己,後複合

 Inheritance(繼承)關系下的構造和析構

 構造:先繼承,後自己

 析構:先自己,後繼承

 Inheritance+Composition關系下的構造和析構

 構造:先繼承,再複合,後自己

 析構:先自己,再複合,後繼承

 對象模型(Object Model):關于vptr和vtbl

C++面向對象程式設計Part IIC++程式設計兼談對象模型 Part II

關于虛指針和虛函數表:

 如上圖右側,觀察它們的繼承關系,以及它們内部的函數,一共有8個函數,其中四個非虛函數,4個虛函數,它們放在記憶體中的不同地方,在繼承關系中,子類繼承了父類函數的調用權而不是函數的大小。

 在含有虛函數的類中,建立的類對象都有一個虛指針,這個虛指針指向虛函數表,虛函數表記錄了虛函數的位址。在對象調用函數的過程中,虛指針指向虛函數表,再通過虛函數表内的位址找到虛函數,完成虛函數的調用。調用是編譯器内部的代碼:(* (p->vptr) [n]) §;或(* p->vptr [n]) §;。

C++面向對象程式設計Part IIC++程式設計兼談對象模型 Part II

 在上圖右側有類的繼承關系。子類繼承了父類并重寫了父類的虛函數。由于子類跟父類大小并不一樣,是以在容器中不能同時存放它們(容器要求存放類型大小一樣的對象),故在容器中存放指針,該指針為父類的指針,父類指針可以指向子類對象,這樣父類和子類都可以存儲。

 在通過指針調用虛函數的過程中,整個程式走的路線跟之前一緻。

 對象模型(Object Model):關于this

C++面向對象程式設計Part IIC++程式設計兼談對象模型 Part II

 函數調用的路線在part I部分有描述。

 對象模型(Object Model):關于Dynamic Binding

動态綁定的三個條件:

 1、指針操作

 2、向上轉型

 3、調用虛函數

C++面向對象程式設計Part IIC++程式設計兼談對象模型 Part II

 對象a調用虛函數,該綁定是靜态綁定,因為a是對象,不是指針。在彙編語言中寫成call xxx。

C++面向對象程式設計Part IIC++程式設計兼談對象模型 Part II

 pa調用虛函數屬于動态綁定,其中pa是指針,它們向父類轉型,且調用虛函數。

為什麼動态綁定要求指針?

 編譯器在編譯的時候不清楚pa指針的類型,是以在調用虛函數的時候就不能确定調用的哪個函數,是以隻能晚綁定(指針指向函數的綁定,到底走哪條路?),即動态綁定。對象調用虛函數時,已經确定類型了,是以直到要走哪條路。

為什麼動态綁定要求向上轉型?

 父類指針可以指向子類的對象,在編譯時編譯器不清楚指針具體類型,也還是不知道綁定函數走哪條路。

為什麼動态綁定要求調用虛函數?

 若不是調用虛函數,非虛函數是類裡面特有的,直接有位址,直接可以找到它,也就是隻有一條路能走。

總之:動态綁定的核心就是:不知道走哪條路。

 談談const

C++面向對象程式設計Part IIC++程式設計兼談對象模型 Part II

 上圖所示,非常量對象可以調用常量成員函數也可以調用非常量成員函數;常量對象隻能調用常量成員函數。

 上圖右側對operator[]進行了重載,由于const也屬于區分重載的一部分,是以上面兩個函數可以同時存在。當成員函數的常量版本和非常量版本同時存在時,常量對象隻能調用常量成員函數版本,非常量對象隻能調用非常量成員函數版本。

 關于new,delete

C++面向對象程式設計Part IIC++程式設計兼談對象模型 Part II

 這一部分在part I中有描述。

  重載 ::operator new/new[],::operator delete/[]

C++面向對象程式設計Part IIC++程式設計兼談對象模型 Part II

 ::operator new指的是重載全局函數。

 operator new傳入參數為記憶體大小,operator delete傳入參數為要删除的指針類型和記憶體大小(可選,可以不傳)。

  重載 member operator new/delete

C++面向對象程式設計Part IIC++程式設計兼談對象模型 Part II

 重載成員函數,在調用時會調用重載的成員函數。

  重載 member operator new[]/delete[]

C++面向對象程式設計Part IIC++程式設計兼談對象模型 Part II

示例

C++面向對象程式設計Part IIC++程式設計兼談對象模型 Part II

 類内重載了new、new[]、delete、delete[]。

C++面向對象程式設計Part IIC++程式設計兼談對象模型 Part II

 沒有虛函數的類對象大小是125+4,(4為編譯器記錄數組元素個數);

 有虛函數的類對象大小是125+4+4,(4為虛函數指針);

注意:

 構造和析構的順序:如圖,建立時,數組中對象從上向下調用構造函數,删除時,數組中對象從下向上調用析構函數。

C++面向對象程式設計Part IIC++程式設計兼談對象模型 Part II

 上圖所示,調用的是全局預設的new[]和delete[]。

  重載new(),delete()

C++面向對象程式設計Part IIC++程式設計兼談對象模型 Part II

示例

C++面向對象程式設計Part IIC++程式設計兼談對象模型 Part II
C++面向對象程式設計Part IIC++程式設計兼談對象模型 Part II

  basic_string使用new(extra)擴充申請量

C++面向對象程式設計Part IIC++程式設計兼談對象模型 Part II

繼續閱讀