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(轉換函數)

上圖中黃色背景的部分即為轉換函數,轉換函數通常的格式為:“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
執行Fraction d2 = f + 4;,可以調用Fraction的構造函數将4轉為Fraction(4, 1);,之後調用operator+将兩個Fraction類加起來。
與上一節的例子相反,上一節是将對象轉為數,這節則是轉為對象。
在類内部添加一個轉換函數Fraction d2 = f + 4;,編譯器會報錯,原因是編譯器不知道是利用轉換函數(double())将f轉換為數還是利用構造函數将4轉換為類對象,産生了二義性。
我們注意到,在上一節中,類内也是有相同的構造函數和轉換函數(double()),為什麼上一節的代碼就能通過呢?原因在于上一節的類内沒有重載operator+,是以類之間不能進行加法操作,也就不能将4轉為類對象,是以隻有一條路可以走,那就是利用轉換函數(double())将f轉為數。
這種将一個數轉換為類對象的操作有時候是我們不需要的,我們需要杜絕這種現象,那就是在構造函數前面加explict關鍵字,這樣就不會把其他類型轉為該類的對象,隻允許通過構造函數調用。
上圖中**Fraction d2 = f + 4;**編譯出錯,原因是4不允許被轉換,隻能f轉換為double類型,那麼f+4為double類型,d2也為double類型,d2不能轉為Fraction類對象,是以編譯出錯。
explict關鍵字通常作用于構造函數前,用于禁止隐式的類型轉換。
上圖為在标準庫中使用轉換函數的例子。
Pointer-like classes,關于智能指針
智能指針就是一個類,類裡面包含了指針,同時也包含了一些其他功能。
Pointer-like classes,關于疊代器
疊代器封裝了指針,相比于智能指針又重載了其他一些操作符。
Function-like classes,所謂仿函數
仿函數,實際上是一個類,執行時像函數,類裡面通常重載了**()**操作符,這些類的對象又稱函數對象,因為它們像函數。
标準庫中的仿函數的奇特模樣
仿函數繼承了一些奇特的類。這些類(下圖所示)沒有資料,隻有一些定義,是以類的大小為1。
namespace經驗談
namespace的意義:
為了把一些東西區分過來,避免類名、函數名和變量名的沖突,取個名字将它們包起來。
下圖就是namespace的定義以及如何使用。
class template,類模闆
function template,函數模闆
 函數模闆可以不用顯示說明參數類型,函數模闆能自動推導,類模闆則不行。為什麼這樣?
若是類模闆沒有指定類型,那就會這樣** complex a**,定義了一個對象,編譯器不知道這個類的類型,不知道是int還是double,是以編譯器不知道它的大小就沒法建立記憶體,也就沒法建立對象,是以類模闆必須顯示的說明類型。
而函數模闆不同,它可以自動推導出類型,并且調用函數是開辟一個棧,不用确定配置設定給它記憶體大小。
member template,成員模闆
成員模闆:在類模闆中,成員函數是一個函數模闆。在上圖中,類模闆pair,類的構造函數又是一個函數模闆。
上圖中,**pari<Derived1, Derived2> p;執行的是類模闆,其中T類型為Derived。接下來pari<Base1, Base2> p2§;**調用拷貝構造函數,這裡T類型變為Base,U的類型為Derived。即将一個鲫魚和麻雀構成的pair§,拷貝到一個由魚類和鳥類構成的pair(p2)中,就是父類指針指向子類對象,這樣是可以的。但反之是不可以的,因為魚類不屬于鲫魚,鳥類也不屬于麻雀。
在智能指針類中,該類也封裝了成員模闆,為的是允許父類指針指向子類對象。
成員模闆通常使用在構造函數中,使用類使用更靈活,更有彈性。
specialization,模闆特化
模闆特化就是把泛華模闆中的類型确定下來。
上圖中的第一個框的内容就是泛華的模闆,第二個框就是确定了類型之後特化的模闆,注意形式:template <>,聲明一下是模闆,但特化後<>内不需要給泛化類型;函數名稱後面加入特化的類型:hash<char>,hash<\int>,hash<long>,函數調用時如果類型在特化模闆中有會直接模闆特化後的類。**hans<long> () (1000);**會直接調用hash<long>模闆,其中第一個小括号表示匿名類型,第二個小括号調用的是類中重載的()操作符。
partial specialization,模闆偏特化——個數的偏
模闆偏特化——個數的偏指的是在模闆中,裡面聲明的類型有多個,其中将某些類型确定下來,例如上圖中将bool類型确定下來,其餘類型依然待确定。其中如果某些類型确定了,必須将确定的類型寫在前面,将不确定的類型寫在後面。
partial specialization,模闆偏特化——範圍的偏
模闆偏特化——範圍的偏指的是原來模闆類型是任意的,現在限制類型的範圍,不讓它再是任意類型了,上圖中将任意類型限制在指針範圍内,其中**C<string> obj1;**調用的是第一個類模闆,**C<string*> obj1;**調用的是第二個類模闆。
template template parameter,模闆模闆參數
模闆模闆參數指的是模闆的參數為模闆。
上圖中,模闆類XCLs第一個模闆參數為類型T,第二個參數為容器類的模闆,在模闆内部定義的對象Container<T> c,T類型就是模闆的第一個參數,Container為第二個參數的容器類型。在定義時XCLs<string, List> mylst1;,表示XCLs為List容器,容器裡面的類型為string,但這樣定義是錯誤的,原因是容器需要好幾個模闆參數,第二種定義是正确的。
以上是關于智能指針的定義。
在上面圖檔的例子中,該類模闆不屬于模闆模闆參數,原因在于定義**stack<int, list<int>>時,其中内部第二個參數list<int>**已經被寫死了,不是模闆類型了。
variadic templates(since C++11)
variadic templates為數量不定的參數模闆,參數不确定個數用**"…"**表示。
上圖中,模闆print含有一個類型T以及不确定個數的類型Types,其中關鍵字typename後面有三個句号,函數内部列印第一個參數,然後遞歸調用自己,讓剩下參數的第一個成為typename T類型,列印它并繼續進行遞歸,直到沒有參數了,print将調用上面重載的函數結束此次運作。
auto(since C++11)
關鍵字auto能夠自動推導出變量類型,當你想少寫些代碼或者不關心它的類型的時候,可以使用auto。但是不能用auto去定義類型,就像上圖中下面為變量ite定義的操作,這樣時錯誤的。
ranged-base for(since C++11)
基于範圍的for循環:
如上圖,for循環的新文法,可以挨個周遊容器,并且可以通過值或者通過引用來周遊容器内的元素。如果需要改變容器内的元素時,可以通過引用周遊來解決,文法是在類型後面加&,如auto&。
reference
建議引用在定義時一定要初始化,它要綁定到一個位址上,并且綁定完之後隻能指向這個位址,不能改變,是一個指針常量。
對象和其引用的大小位址是相同的,這其實是一種假象,引用就是指針,在函數傳遞時也不會傳遞對象對象大小的記憶體,就是指針的大小,4位元組(32bit)或8位元組(64bit)。
上圖是分别以指針、值、引用傳遞時的定義以及調用時的文法。
引用通常不用來聲明變量,而是用于參數傳遞和傳回類型的描述。
在上圖中下方的部分,定義了兩個重載函數,它們傳遞的參數類型不同,一種是引用一種時值,這種重載是不允許的,因為在調用時,它們的文法是一樣的,編譯器不能分辨該使用哪個函數。但重載指針傳遞與值或重載指針傳遞與引用傳遞是可以的。
另外,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
關于虛指針和虛函數表:
如上圖右側,觀察它們的繼承關系,以及它們内部的函數,一共有8個函數,其中四個非虛函數,4個虛函數,它們放在記憶體中的不同地方,在繼承關系中,子類繼承了父類函數的調用權而不是函數的大小。
在含有虛函數的類中,建立的類對象都有一個虛指針,這個虛指針指向虛函數表,虛函數表記錄了虛函數的位址。在對象調用函數的過程中,虛指針指向虛函數表,再通過虛函數表内的位址找到虛函數,完成虛函數的調用。調用是編譯器内部的代碼:(* (p->vptr) [n]) §;或(* p->vptr [n]) §;。
在上圖右側有類的繼承關系。子類繼承了父類并重寫了父類的虛函數。由于子類跟父類大小并不一樣,是以在容器中不能同時存放它們(容器要求存放類型大小一樣的對象),故在容器中存放指針,該指針為父類的指針,父類指針可以指向子類對象,這樣父類和子類都可以存儲。
在通過指針調用虛函數的過程中,整個程式走的路線跟之前一緻。
對象模型(Object Model):關于this
函數調用的路線在part I部分有描述。
對象模型(Object Model):關于Dynamic Binding
動态綁定的三個條件:
1、指針操作
2、向上轉型
3、調用虛函數
對象a調用虛函數,該綁定是靜态綁定,因為a是對象,不是指針。在彙編語言中寫成call xxx。
pa調用虛函數屬于動态綁定,其中pa是指針,它們向父類轉型,且調用虛函數。
為什麼動态綁定要求指針?
編譯器在編譯的時候不清楚pa指針的類型,是以在調用虛函數的時候就不能确定調用的哪個函數,是以隻能晚綁定(指針指向函數的綁定,到底走哪條路?),即動态綁定。對象調用虛函數時,已經确定類型了,是以直到要走哪條路。
為什麼動态綁定要求向上轉型?
父類指針可以指向子類的對象,在編譯時編譯器不清楚指針具體類型,也還是不知道綁定函數走哪條路。
為什麼動态綁定要求調用虛函數?
若不是調用虛函數,非虛函數是類裡面特有的,直接有位址,直接可以找到它,也就是隻有一條路能走。
總之:動态綁定的核心就是:不知道走哪條路。
談談const
上圖所示,非常量對象可以調用常量成員函數也可以調用非常量成員函數;常量對象隻能調用常量成員函數。
上圖右側對operator[]進行了重載,由于const也屬于區分重載的一部分,是以上面兩個函數可以同時存在。當成員函數的常量版本和非常量版本同時存在時,常量對象隻能調用常量成員函數版本,非常量對象隻能調用非常量成員函數版本。
關于new,delete
這一部分在part I中有描述。
重載 ::operator new/new[],::operator delete/[]
::operator new指的是重載全局函數。
operator new傳入參數為記憶體大小,operator delete傳入參數為要删除的指針類型和記憶體大小(可選,可以不傳)。
重載 member operator new/delete
重載成員函數,在調用時會調用重載的成員函數。
重載 member operator new[]/delete[]
示例
類内重載了new、new[]、delete、delete[]。
沒有虛函數的類對象大小是125+4,(4為編譯器記錄數組元素個數);
有虛函數的類對象大小是125+4+4,(4為虛函數指針);
注意:
構造和析構的順序:如圖,建立時,數組中對象從上向下調用構造函數,删除時,數組中對象從下向上調用析構函數。
上圖所示,調用的是全局預設的new[]和delete[]。
重載new(),delete()
示例