接下來(lái)我將持續(xù)更新“深度解讀《深度探索C++對(duì)象模型》”系列,敬請(qǐng)期待,歡迎關(guān)注!也可以關(guān)注公眾號(hào):iShare愛分享,自動(dòng)獲得推文和全部的文章列表。
假如有這樣的一段代碼,代碼中定義了一個(gè)Object類,類中有一個(gè)成員函數(shù)print,通過以下的兩種調(diào)用方式調(diào)用:
Object b;
Object* p = new Object;
b.print();
p->print();
請(qǐng)問這兩種方式有什么區(qū)別嗎?在效率上一樣嗎?答案是不確定。因?yàn)榈每闯蓡T函數(shù)print的聲明方式,它可能是靜態(tài)的,可能是非靜態(tài)的,也可能是一個(gè)虛函數(shù)。還得看Object類的具體定義,它可能是獨(dú)立的類,也有可能是經(jīng)過多重繼承來(lái)的類,或者繼承的父類中有一個(gè)虛基類。
靜態(tài)成員函數(shù)和非虛成員函數(shù)比較簡(jiǎn)單,我們?cè)谙乱恍」?jié)簡(jiǎn)單介紹一下即可,本文重點(diǎn)講解虛函數(shù)的實(shí)現(xiàn)及其效率。
成員函數(shù)種類
- 非靜態(tài)成員函數(shù)
非靜態(tài)成員函數(shù)和普通的非成員函數(shù)是一樣的,它也是被編譯器放置在代碼段中,且可以像普通函數(shù)那樣可以獲取到它的地址。和普通非成員函數(shù)的區(qū)別是它的調(diào)用必須得經(jīng)由一個(gè)對(duì)象或者對(duì)象的指針來(lái)調(diào)用,而且可以直接訪問類中非公開的數(shù)據(jù)成員。下面的代碼打印出函數(shù)的地址:
#include <cstdio>
class Object {
public:
void print() {
printf("a=%d, b=%d\n", a, b);
}
int a = 1;
int b = 2;
};
void printObject(Object* obj) {
printf("a=%d, b=%d\n", obj->a, obj->b);
}
int main() {
printf("Object::print = %p\n", &Object::print);
printf("printObject = %p\n", &printObject);
return 0;
}
程序的輸出結(jié)果如下,從打印結(jié)果來(lái)看,兩者的地址比較相近,說(shuō)明它們都是一起放在代碼段中的,從生成的匯編代碼也可以看出來(lái)。
Object::print = 0x1007b3f30
printObject = 0x1007b3e70
非靜態(tài)成員函數(shù)和普通非成員函數(shù)的運(yùn)行效率上也是一樣的,普通非成員函數(shù)的實(shí)現(xiàn)上,對(duì)類中成員的訪問看起來(lái)像是要經(jīng)過指針的間接訪問,如obj->a
,非靜態(tài)成員函數(shù)的訪問看起來(lái)更直接一點(diǎn),直接可以對(duì)類中的成員進(jìn)行存取,好像是非靜態(tài)成員函數(shù)的效率更高一些,其實(shí)不然,非靜態(tài)成員函數(shù)的調(diào)用,編譯器會(huì)隱式的把它轉(zhuǎn)換成另一種形式:
Object obj;
obj.print();
// 轉(zhuǎn)換成:
print(&obj);
// print的定義轉(zhuǎn)換成:
print(Object* const this) {
printf("a=%d, b=%d\n", this->a, this->b);
}
兩者在本質(zhì)上是一樣的,查看生成的匯編代碼也是一樣的。另外也說(shuō)明了為什么非靜態(tài)成員函數(shù)要經(jīng)由一個(gè)對(duì)象或?qū)ο蟮闹羔榿?lái)調(diào)用。
- 靜態(tài)成員函數(shù)
上面提到的非靜態(tài)成員函數(shù)的調(diào)用,必須要經(jīng)由類的對(duì)象來(lái)調(diào)用,是因?yàn)樾枰獙?duì)象的地址作為函數(shù)的參數(shù),也就是隱式的this指針,這樣在函數(shù)中訪問類的非靜態(tài)數(shù)據(jù)成員時(shí)將綁定到此地址上,也就是將此地址作為基地址,經(jīng)過偏移得到數(shù)據(jù)成員的地址。但是如果函數(shù)中不需要訪問非靜態(tài)數(shù)據(jù)成員的話,是不需要this指針的,但目前的編譯器并不區(qū)分這種情況。靜態(tài)成員函數(shù)不能訪問類中的非靜態(tài)數(shù)據(jù)成員,所以是不需要this指針的,如Object類中定義了靜態(tài)成員函數(shù)static int static_func()
,通過對(duì)象調(diào)用:
Object obj;
obj.static_func();
或者通過對(duì)象的指針調(diào)用:
Object* pobj = new Object;
pobj->static_func();
最終都會(huì)轉(zhuǎn)換成員如下的形式:
Object::static_func();
通過對(duì)象或者對(duì)象的指針來(lái)調(diào)用只是語(yǔ)法上的便利而已,它并不需要對(duì)象的地址作為參數(shù)(this指針)。
那么靜態(tài)成員函數(shù)存在的意義是什么?靜態(tài)成員函數(shù)在C++誕生之初是不支持的,是在后面的版本中增加進(jìn)去的。假設(shè)不支持靜態(tài)成員函數(shù)時(shí),類中有一個(gè)非公開的靜態(tài)數(shù)據(jù)成員,如果外面的代碼需要訪問這個(gè)靜態(tài)數(shù)據(jù),那么就需要寫一個(gè)非靜態(tài)成員函數(shù)來(lái)存取它,而非靜態(tài)成員函數(shù)需要經(jīng)由對(duì)象來(lái)調(diào)用,但有時(shí)候在這個(gè)時(shí)間點(diǎn)沒有創(chuàng)建一個(gè)對(duì)象或者沒有必要?jiǎng)?chuàng)建一個(gè)對(duì)象,那么就有了以下的變通做法:
// 假設(shè)定義了get_static_var函數(shù)用于返回靜態(tài)數(shù)據(jù)成員
((Object*) 0))->get_static_var();
// 編譯器會(huì)轉(zhuǎn)換成:
get_static_var((Object*) 0));
上面的代碼把0強(qiáng)制轉(zhuǎn)換為Object類型的指針,然后經(jīng)由它來(lái)調(diào)用非靜態(tài)成員函數(shù),編譯器會(huì)把0作為對(duì)象的地址傳遞給函數(shù),但函數(shù)中不會(huì)使用這個(gè)0,所以不會(huì)出現(xiàn)問題。由于有這些需求的存在,C++標(biāo)準(zhǔn)委員會(huì)增加了支持靜態(tài)成員函數(shù),靜態(tài)成員函數(shù)可以訪問類中的非公開的靜態(tài)數(shù)據(jù)成員,且不需要經(jīng)由類的對(duì)象來(lái)調(diào)用。
靜態(tài)成員函數(shù)和非靜態(tài)成員函數(shù)、普通函數(shù)一樣都是存儲(chǔ)在代碼段中的,也可以獲取到它的地址,它是一個(gè)實(shí)際的內(nèi)存的地址,是一個(gè)數(shù)據(jù),如上面定義的static_func函數(shù),它的類型為int (*)()
,就是一個(gè)普通的函數(shù)類型。而非靜態(tài)成員函數(shù),返回的是一個(gè)“指向類成員函數(shù)的指針”,如上面定義的print函數(shù),返回的類型是:
void (Object::*) ();
靜態(tài)成員函數(shù)基本上等同于普通函數(shù),所以和C語(yǔ)言結(jié)合編程時(shí),可以作為回調(diào)函數(shù)傳遞給C語(yǔ)言寫的函數(shù)。
總結(jié)一下,靜態(tài)成員函數(shù)具有以下的特性:
-
- 靜態(tài)成員函數(shù)不能存取類中的非靜態(tài)數(shù)據(jù)成員。
- 靜態(tài)成員函數(shù)不能被聲明為const、volatile或者是virtual。
- 靜態(tài)成員不需要經(jīng)由類的對(duì)象來(lái)調(diào)用。
- 虛函數(shù)
虛函數(shù)是否也可以像非虛函數(shù)那樣獲取到它的地址呢?我們寫個(gè)程序來(lái)測(cè)試一下。
#include <cstdio>
class Object {
public:
virtual void virtual_func1() {
printf("this is virtual function 1\n");
}
virtual void virtual_func2() {
printf("this is virtual function 2\n");
}
};
int main() {
printf("Object::virtual_func1 = %p\n", &Object::virtual_func1);
printf("Object::virtual_func2 = %p\n", &Object::virtual_func2);
return 0;
}
上面程序的輸出:
Object::virtual_func1 = 0x0
Object::virtual_func2 = 0x8
程序的輸出結(jié)果并不是一個(gè)內(nèi)存地址,而是一個(gè)數(shù)字,其實(shí)這是一個(gè)偏移值,對(duì)應(yīng)的是這個(gè)虛函數(shù)在虛函數(shù)表中的位置,一個(gè)位置占用8字節(jié)大小,第一個(gè)是0,第二個(gè)是8,以此類推,每多一個(gè)虛函數(shù),就在這個(gè)表中占用一個(gè)位置??雌饋?lái)像是無(wú)法獲取到虛函數(shù)的地址,其實(shí)不然,虛函數(shù)的地址就存放在虛函數(shù)表中,只是我們無(wú)法直接獲取到它,但是我們記得,如果有虛函數(shù)時(shí),對(duì)象的前面會(huì)被編譯器插入一個(gè)虛函數(shù)表指針,這個(gè)指針就是指向類的虛函數(shù)表,我們可以通過它來(lái)獲取到虛函數(shù)的地址,下面演示一下通過非常規(guī)手段來(lái)調(diào)用虛函數(shù)的做法:
#include <cstdio>
class Object {
public:
virtual void virtual_func1() {
printf("this is virtual function 1\n");
}
virtual void virtual_func2() {
printf("this is virtual function 2\n");
}
};
int main() {
Object* pobj = new Object;
using Fun = void (*)(void);
Fun** ptr = (Fun**)pobj;
printf("vptr = %p\n", *ptr);
for (auto i = 0; i < 2; ++i) {
Fun fp = *(*ptr + i); //取得虛函數(shù)的內(nèi)存地址
printf("vptr[%d] = %p\n", i, fp);
fp(); //此行調(diào)用虛函數(shù)
}
delete pobj;
return 0;
}
程序的輸出結(jié)果:
vptr = 0x100264030
vptr[0] = 0x100263ea4
this is virtual function 1
vptr[1] = 0x100263ecc
this is virtual function 2
可以看到,虛函數(shù)的地址不光可以獲取得到,而且還可以直接調(diào)用它,調(diào)用它的前提是函數(shù)中沒有訪問類的非靜態(tài)數(shù)據(jù)成員,不然就會(huì)出現(xiàn)運(yùn)行錯(cuò)誤。vptr就是寫入到對(duì)象前面的虛函數(shù)表指針,它的值就是虛函數(shù)表在內(nèi)存中的地址,虛函數(shù)表中記錄了兩項(xiàng)內(nèi)容,對(duì)應(yīng)了兩個(gè)虛函數(shù)的地址,即vptr[0]是虛函數(shù)virtual_func1的地址,vptr[1]是虛函數(shù)virtual_func2的地址。把他們強(qiáng)制轉(zhuǎn)換成普通函數(shù)的類型指針,然后可以直接調(diào)用他們,所以這里是沒有對(duì)象的this指針的,也就不能訪問類中的非靜態(tài)數(shù)據(jù)成員了。
虛函數(shù)的實(shí)現(xiàn)
從上一小節(jié)中我們已經(jīng)窺探到虛函數(shù)的一般實(shí)現(xiàn)模型,每一個(gè)類有一個(gè)虛函數(shù)表,虛函數(shù)表中包含類中每個(gè)虛函數(shù)的地址,然后每個(gè)對(duì)象的前面會(huì)被編譯器插入一個(gè)指向虛函數(shù)表的指針,同一個(gè)類的所有對(duì)象都共享同一個(gè)虛函數(shù)表。接下來(lái)的內(nèi)容中將詳細(xì)分析虛函數(shù)的實(shí)現(xiàn)細(xì)節(jié),包括單一繼承、多重繼承和虛繼承的情況。
多態(tài)是C++中最重要的特性之一,也是組成面向?qū)ο缶幊谭妒降幕?,虛函?shù)則是為多態(tài)而生。那么何為多態(tài)?多態(tài)是在基類中定義一組接口,根據(jù)不同的業(yè)務(wù)場(chǎng)景派生出不同的子類,在子類中實(shí)現(xiàn)接口,上層代碼根據(jù)業(yè)務(wù)邏輯調(diào)用接口,不關(guān)心接口的具體實(shí)現(xiàn)。在代碼中,一般是聲明一個(gè)基類的指針,此指針在運(yùn)行期間可能指向不同的派生類,然后通過基類的指針調(diào)用一個(gè)接口,這個(gè)接口在不同的派生類中有不同的實(shí)現(xiàn),所以根據(jù)基類的指針指向哪個(gè)具體的派生類,調(diào)用的就是這個(gè)派生類的實(shí)例。假設(shè)有一個(gè)名稱為print的接口,p是基類類型的指針,那么下面的調(diào)用:
p->print();
是如何識(shí)別出要實(shí)施多態(tài)的行為?以及如何調(diào)用到具體哪個(gè)派生類中的print?如果是在指針類型上增加信息,以指明具體所指對(duì)象的類型,那么會(huì)改變指針原有的語(yǔ)義,造成和C語(yǔ)言的不兼容,而且也不是每個(gè)類型都需要這個(gè)信息,這會(huì)造成不必要的空間浪費(fèi)。如果是在每個(gè)類對(duì)象中增加信息,那么在不需要多態(tài)的對(duì)象中也需要存放這些信息,也會(huì)造成空間上的浪費(fèi)。因此增加了一個(gè)關(guān)鍵字virtual,用于修飾那些需要多態(tài)的函數(shù),這樣的函數(shù)就叫做虛函數(shù),所以識(shí)別一個(gè)類是否支持多態(tài),就看這個(gè)類中是否聲明了虛函數(shù)。只要類中有虛函數(shù),就說(shuō)明需要在類對(duì)象中存儲(chǔ)運(yùn)行期的信息。
那么在對(duì)象中要存儲(chǔ)哪些信息才能夠保證保證上面代碼中print的調(diào)用是調(diào)用到正確的派生類中的實(shí)例呢?要調(diào)用到正確的print實(shí)例,我們需要知道:
- p指向具體的對(duì)象類型,讓我們知道要調(diào)用哪個(gè)print;
- print的位置,以便我們可以正確調(diào)用它。
要如何實(shí)現(xiàn)它,不同的編譯器可能有不同的實(shí)現(xiàn)方法,通常是使用虛函數(shù)表的做法。編譯器在編譯的過程中,收集到哪些是虛函數(shù),然后將這些虛函數(shù)的地址存放一個(gè)表格中,這些虛函數(shù)的地址在編譯期間確定的,運(yùn)行期間是不會(huì)改變的,虛函數(shù)的個(gè)數(shù)也是固定的,在程序的執(zhí)行期間不能刪除或者增加,所以表格的大小也是固定的,這個(gè)過程由編譯器在編譯期間完成。表格中虛函數(shù)的位置按照類中聲明的順序,位置是固定不變的,我們?cè)谏瞎?jié)中通過虛函數(shù)名稱打印出來(lái)的值就是虛函數(shù)在虛函數(shù)表中的位置,即相對(duì)于表格首地址的偏移值。
有了這個(gè)表格,那么如何尋址到這個(gè)表格呢?方法就是編譯器根據(jù)類中是否有虛函數(shù),如果有虛函數(shù),就在類的構(gòu)造函數(shù)里插入一些匯編代碼,在構(gòu)造對(duì)象時(shí),在對(duì)象的前面插入一個(gè)指針,這個(gè)指針指向這個(gè)虛函數(shù)表,所以這個(gè)指針也叫做虛函數(shù)表指針。下面以具體的代碼來(lái)看看虛函數(shù)是怎么調(diào)用的,把上面的例子main函數(shù)修改如下,其它地方不變:
int main() {
Object* pobj = new Object;
pobj->virtual_func1();
pobj->virtual_func2();
delete pobj;
return 0;
}
我們來(lái)看下生成的匯編代碼,首先來(lái)看看虛函數(shù)表長(zhǎng)什么樣:
vtable for Object:
.quad 0
.quad typeinfo for Object
.quad Object::virtual_func1()
.quad Object::virtual_func2()
它是匯編中定義在數(shù)據(jù)段的一組數(shù)據(jù),“vtable for Object”是它的標(biāo)簽,代表了這個(gè)數(shù)據(jù)區(qū)的起始地址,每一行定義一條數(shù)據(jù),第一列.quad表示數(shù)據(jù)的大小,占用8字節(jié),第二列表示數(shù)據(jù)的值,可以是數(shù)字,也可以是標(biāo)簽,標(biāo)簽是地址的引用。其實(shí)這個(gè)完整的表叫做虛表,它包含了虛函數(shù)表、RTTI信息和虛繼承相關(guān)的信息,Clang和Gcc編譯器是把它們合在一起了,其它編譯器可能是分開的。第一行是虛繼承中用到,之前已經(jīng)講過了,第二行是RTTI信息,這個(gè)以后再講。第三、四行是兩個(gè)虛函數(shù)的地址。
接著看看Object類的默認(rèn)構(gòu)造函數(shù)的代碼:
Object::Object() [base object constructor]: # @Object::Object() [base object constructor]
# 略...
lea rcx, [rip + vtable for Object]
add rcx, 16
mov qword ptr [rax], rcx
# 略...
之前已經(jīng)講過,有虛函數(shù)時(shí)編譯器會(huì)為類生成默認(rèn)構(gòu)造函數(shù),在默認(rèn)構(gòu)造函數(shù)里在類對(duì)象的前面設(shè)置了虛函數(shù)表指針。在這個(gè)默認(rèn)構(gòu)造函數(shù)里,主要的代碼就是上面這三行,首先獲取虛表(將上面)的起始地址存放在rcx寄存器,然后加上16的偏移值跳過第一、二行,這時(shí)指向第三行數(shù)據(jù),也就是第一個(gè)虛函數(shù)的位置,然后將這個(gè)地址賦值給[rax],rax是存放的對(duì)象的首地址,這就完成了給對(duì)象設(shè)置虛函數(shù)表指針。
接著看main函數(shù)中對(duì)虛函數(shù)的調(diào)用:
main: # @main
# 略...
# 調(diào)用構(gòu)造函數(shù)
mov rdi, rax
mov qword ptr [rbp - 32], rdi # 8-byte Spill
call Object::Object() [base object constructor]
mov rax, qword ptr [rbp - 32] # 8-byte Reload
mov qword ptr [rbp - 16], rax
# 調(diào)用第一個(gè)虛函數(shù)
mov rdi, qword ptr [rbp - 16]
mov rax, qword ptr [rdi]
call qword ptr [rax]
# 調(diào)用第二個(gè)虛函數(shù)
mov rdi, qword ptr [rbp - 16]
mov rax, qword ptr [rdi]
call qword ptr [rax + 8]
# 略...
上面匯編代碼中的第4行rax是調(diào)用new函數(shù)后返回來(lái)的地址,也就是pobj指針,把它存放到rdi寄存器中作為參數(shù),同時(shí)也保存到??臻grbp - 32中,然后調(diào)用構(gòu)造函數(shù),構(gòu)造完成之后再拷貝這個(gè)地址到棧空間rbp - 16中。接下來(lái)的第10到12行是第一個(gè)虛函數(shù)的調(diào)用,將對(duì)象的首地址加載到rdi寄存器中,然后對(duì)其取內(nèi)容,也就是是相當(dāng)于指針的解引用,即 (*pobj)
,取得的內(nèi)容即是構(gòu)造函數(shù)中設(shè)置的虛函數(shù)表的地址,它是一個(gè)指向第一個(gè)虛函數(shù)的地址,然后第12行對(duì)其取內(nèi)容,也即是對(duì)這個(gè)地址解引用,取得第一個(gè)虛函數(shù)的地址,然后以rdi寄存器(即對(duì)象的首地址)為第一個(gè)參數(shù)調(diào)用它,相當(dāng)于:virtual_func1(pobj)
。第14到16行是對(duì)第二個(gè)虛函數(shù)的調(diào)用,流程和第一個(gè)基本一樣,區(qū)別在于將虛函數(shù)表的地址加上8的偏移量以指向第二個(gè)虛函數(shù)。
如果在一個(gè)虛函數(shù)中調(diào)用另一個(gè)虛函數(shù)又會(huì)怎樣?第一個(gè)虛函數(shù)已經(jīng)決議出是調(diào)用哪個(gè)對(duì)象的實(shí)例了,那么在其中調(diào)用其它虛函數(shù)還需要再動(dòng)態(tài)決議嗎?把main函數(shù)中對(duì)第二個(gè)虛函數(shù)的調(diào)用去掉,在第一個(gè)虛函數(shù)中增加以下代碼:
virtual_func2();
Object::virtual_func2();
來(lái)看下對(duì)應(yīng)生成的匯編代碼,其它代碼都差不多,主要看virtual_func1函數(shù)的代碼:
Object::virtual_func1(): # @Object::virtual_func1()
# 略...
mov rdi, qword ptr [rbp - 16] # 8-byte Reload
mov rax, qword ptr [rdi]
call qword ptr [rax + 8]
mov rdi, qword ptr [rbp - 16] # 8-byte Reload
call Object::virtual_func2()
# 略...
rbp - 16保存的是對(duì)象的首地址,第3到5行對(duì)應(yīng)的是上面C++代碼中第一句的調(diào)用,看起來(lái)在虛函數(shù)中調(diào)用另一個(gè)虛函數(shù),用的還是動(dòng)態(tài)決議的方法,這里編譯器沒有識(shí)別出已經(jīng)決議出具體的對(duì)象了。從匯編代碼的第6、7行看到,通過前面加類名的限定符,是直接調(diào)用到這個(gè)函數(shù),如果你明確調(diào)用的是哪個(gè)函數(shù)的話,可以直接在函數(shù)的前面加上類名,這樣就不需要用多態(tài)的方式去調(diào)用了。
如果不是通過指針類型來(lái)調(diào)用虛函數(shù),而是通過對(duì)象來(lái)調(diào)用,結(jié)果是什么情況?把main函數(shù)改成如下:
int main() {
Object obj;
obj.virtual_func1();
obj.virtual_func2();
return 0;
}
查看main函數(shù)對(duì)應(yīng)的匯編代碼:
main: # @main
# 略...
lea rdi, [rbp - 16]
call Object::virtual_func1()
lea rdi, [rbp - 16]
call Object::virtual_func2()
# 略...
可以看到通過對(duì)象來(lái)調(diào)用虛函數(shù),是直接調(diào)用到這個(gè)對(duì)象的函數(shù)實(shí)例的,沒有使用多態(tài)的方式,所以通過對(duì)象的方式調(diào)用是沒有多態(tài)的行為的,只有通過類的指針或者引用類型來(lái)調(diào)用虛函數(shù),才會(huì)有多態(tài)的行為。
單一繼承下的虛函數(shù)
假設(shè)有以下的類定義及繼承關(guān)系:
class Point {
public:
Point(int x = 0) { _x = x; }
virtual ~Point() = default;
virtual void drawLine() = 0;
int x() { return _x; }
virtual int y() { return 0; }
virtual int z() { return 0; }
private:
int _x;
};
class Point2d: public Point {
public:
Point2d(int x = 0, int y = 0): Point(x) { _y = y; }
virtual ~Point2d() = default;
void drawLine() override { }
virtual void rotate() { }
int y() override { return _y; }
private:
int _y;
};
class Point3d: public Point2d {
public:
Point3d(int x = 0, int y = 0, int z = 0): Point2d(x, y) { _z = z; }
virtual ~Point3d() = default;
void drawLine() override { }
void rotate() override { }
int z() override { return _z; }
private:
int _z;
};
int main() {
Point* p = new Point3d(1, 1, 1);
printf("z = %d\n", p->z());
delete p;
return 0;
}
先來(lái)看看生成的匯編代碼中的虛函數(shù)表:
vtable for Point:
.quad 0
.quad typeinfo for Point
.quad Point::~Point() [base object destructor]
.quad Point::~Point() [deleting destructor]
.quad __cxa_pure_virtual
.quad Point::y()
.quad Point::z()
vtable for Point2d:
.quad 0
.quad typeinfo for Point2d
.quad Point2d::~Point2d() [base object destructor]
.quad Point2d::~Point2d() [deleting destructor]
.quad Point2d::drawLine()
.quad Point2d::y()
.quad Point::z()
.quad Point2d::rotate()
vtable for Point3d:
.quad 0
.quad typeinfo for Point3d
.quad Point3d::~Point3d() [base object destructor]
.quad Point3d::~Point3d() [deleting destructor]
.quad Point3d::drawLine()
.quad Point2d::y()
.quad Point3d::z()
.quad Point3d::rotate()
每個(gè)類都有一個(gè)對(duì)應(yīng)的虛函數(shù)表,虛函數(shù)表中的內(nèi)容主要來(lái)自于三方面:
- 改寫基類中對(duì)應(yīng)的虛函數(shù),用自己實(shí)現(xiàn)的虛函數(shù)的地址寫入到對(duì)應(yīng)表格中的位置;
- 從基類中繼承而來(lái)的虛函數(shù),直接拷貝基類虛函數(shù)的地址添加到虛函數(shù)表中;
- 新增的虛函數(shù),基類中沒有,子類的虛函數(shù)表會(huì)增加一行容納新條目;
基類和子類使用各自的虛函數(shù)表,互不干擾,即使子類中沒有改寫基類的虛函數(shù),也沒有新增虛函數(shù),編譯器也會(huì)為子類新建一個(gè)虛函數(shù)表,內(nèi)容從基類中拷貝過來(lái),內(nèi)容和基類完全一樣。
虛函數(shù)表中的虛函數(shù)的排列順序是固定的,一般是按照在類中的聲明順序,如C++代碼中的這行代碼:
p->z();
要尋址到正確的z函數(shù)實(shí)例的地址,我們首先需要知道p指針?biāo)赶虻木唧w對(duì)象,然后需要知道z函數(shù)在表格中的位置,如上例中,z函數(shù)在第5個(gè)條目,也就是說(shuō)虛函數(shù)表的起始地址加上32的偏移量就可以尋址到它,這個(gè)位置保持不變,無(wú)論p指針指向哪個(gè)對(duì)象,都能找到正確的z函數(shù)。如果子類中有新增的虛函數(shù),新增的虛函數(shù)聲明的位置插在從基類中繼承來(lái)的虛函數(shù)中間,編譯器會(huì)做調(diào)整,把它安排在后面,在原有的順序上再遞增,如上例中的rotate函數(shù)。文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-856633.html
如果您感興趣這方面的內(nèi)容,請(qǐng)?jiān)谖⑿派纤阉鞴娞?hào)iShare愛分享并關(guān)注,以便在內(nèi)容更新時(shí)直接向您推送。
文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-856633.html
到了這里,關(guān)于深度解讀《深度探索C++對(duì)象模型》之C++虛函數(shù)實(shí)現(xiàn)分析(一)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!