前後端分離開發,後端需要編寫接⼝說明⽂檔,會耗費⽐較多的時間。 swagger 是⼀個⽤於⽣成伺服器接⼝的規範性⽂檔,並且能夠對接⼝進⾏測試的⼯具。 作用 ⽣成接⼝說明⽂檔 對接⼝進⾏測試 使用步驟 添加依賴 <!--swagger--> <dependency> <groupId>io.sprin ...
這篇博客來講一下g++實現的C++對象模型中的虛函數的實現,包括:單一繼承體系下的虛函數,多繼承下的虛函數和虛繼承下的虛函數。其中虛繼承下的虛函數在《深度探索C++對象模型》中只是說很複雜,受限於技術力和查到的資料,這裡我只是對於g++的部分實現進行觀察。
單一繼承體系下的虛函數
在前面的博客中我們已經通過對虛表的探索講了虛函數的一般實現,大體上來說就是編譯器會在適當的時候(在單一繼承體系中就是當類中第一次出現虛函數的時候)添加一個虛表指針,指向屬於該類的虛函數表,而所有虛函數的地址會出現在虛表指針的固定表項,也就是說在繼承體系下的一個虛函數會被賦予固定的虛表下標。當派生類覆寫(override)了基類的虛函數時,新的虛函數的地址會出現在基類虛函數在虛表中的位置,在多態調用虛函數時從虛表中取出虛函數地址來調用,從而實現多態。
一般而言,在單一繼承體系下每一個類都只有一個虛表,在這個虛表中存有所有active virtual functions(中文版《深度探索C++對象模型》沒有翻譯,我這裡也直接使用了,在我的理解里就是派生類所有有效的、能用的虛函數)的地址。這些active virtual functions包括:
- 該類所定義的所有虛函數,包括其覆寫(override)的基類的虛函數;
- 繼承自基類的虛函數,如果派生類不覆寫這些虛函數的話;
- 一個pure_vairtual_called()函數實體,她既可以扮演pure virtual function的空間保衛者角色,也可以當作異常處理函數(有時候會用到)【《深度探索C++對象模型》原話】
// test23.cpp
class Base {
public:
Base(int i)
: m_i(i)
{}
virtual
~Base() {
m_i = 0;
}
virtual
int getInt() {
return m_i;
}
virtual
void increaseInt() {
m_i++;
}
virtual
long getLong() = 0;
private:
int m_i;
};
class Derived: public Base {
public:
Derived(int i, long l)
: Base(i),
m_l(l)
{}
virtual
~Derived() {
m_l = 0;
}
virtual
int getInt() override { // overrid Base::getInt()
return Base::getInt() + 1;
}
virtual
long getLong() override { // overrid Base::getLong(),在Base中是一個純虛函數
return m_l;
}
virtual
void increaseLong() { // new virtual function
++m_l;
}
private:
long m_l;
};
int main() {
Derived* pd = new Derived(1, 2L);
int i = pd->getInt();
pd->increaseInt();
long l = pd->getLong();
pd->increaseLong();
pd->~Derived();
delete pd;
}
另外,在這裡我們可以註意到一個問題,虛表指針指向的空間,前兩個表項都顯示是Derived::~Derived(),也就是都是析構函數,而且地址不一樣,這是怎麼回事?我們看一下這兩處地方的彙編代碼:
可以看到,第一個析構函數就是普通的析構函數它先調用了我們自己定義的析構函數,再調用了基類的析構函數Base::~Base;而第二個虛構函數則是先調用了第一個析構函數,再調用了::operator delete
(_ZdlPvm使用c++filt工具查看可知其就是operator delete(void*, unsigned long)
)。
那是不是就是當我們自己調用Derived::~Derived時調用第一個,使用delete操作符時調用的就是第二個呢?我們看到反彙編:
可以看到確實是這樣的。同時,我們還有一個小發現,就是當delete操作符操作的指針是nullptr時,是不會調用析構函數的,編譯器真是相當費心了(在我的測試下好像是只有delete一個指向有虛析構函數的對象的指針時才會檢查,否則就直接不檢查調用::operator delete
)。
關於最後一個,因為我們無法實例化抽象基類,所以使用-fdump-class-hierarchy
選項查看類信息:
Vtable for Base
Base::_ZTV4Base: 7 entries
0 (int (*)(...))0
8 (int (*)(...))(& _ZTI4Base)
16 0
24 0
32 (int (*)(...))Base::getInt
40 (int (*)(...))Base::increaseInt
48 (int (*)(...))__cxa_pure_virtual
Class Base
size=16 align=8
base size=12 base align=8
Base (0x0x7f24b28e7960) 0
vptr=((& Base::_ZTV4Base) + 16)
Vtable for Derived
Derived::_ZTV7Derived: 8 entries
0 (int (*)(...))0
8 (int (*)(...))(& _ZTI7Derived)
16 (int (*)(...))Derived::~Derived
24 (int (*)(...))Derived::~Derived
32 (int (*)(...))Derived::getInt
40 (int (*)(...))Base::increaseInt
48 (int (*)(...))Derived::getLong
56 (int (*)(...))Derived::increaseLong
Class Derived
size=24 align=8
base size=24 base align=8
Derived (0x0x7f24b277d1a0) 0
vptr=((& Derived::_ZTV7Derived) + 16)
Base (0x0x7f24b28e7de0) 0
primary-for Derived (0x0x7f24b277d1a0)
我們可以看到在Base
類的48偏移處確實有一個__cxa_pure_virtual表項,應該就是所謂的pure_vairtual_called,在結合Derived
類的虛表,在對應位置是Derived::getLong,說明正是使用該函數占位了Base::getLong這個虛函數。
多重繼承下的虛函數
在單一繼承體系下一切都顯得那麼美好,完全不涉及到指針的調整,因為所有的指針轉化都不需要做底層的調整,始終指向類的開頭。你可能現在還不能理解,在看完這一部分後再來看上面這一句話就會感慨:啊,單一繼承是這麼簡單的事!
但在多重繼承下事情開始變得複雜,看下麵的例子:
// test24.cpp
class Base1 {
public:
Base1(int i)
: m_i(i)
{}
virtual
int getInt() {
return m_i;
}
virtual
Base1* clone() {
return new Base1(m_i);
}
private:
int m_i;
};
class Base2 {
public:
Base2(long l)
: m_l(l)
{}
virtual
long getLong() {
return m_l;
}
virtual
Base2* clone() {
return new Base2(m_l);
}
private:
long m_l;
};
class Derived: public Base1, public Base2 {
public:
Derived(int i, long l)
: Base1(i),
Base2(l)
{}
virtual
long getLong() { // override Base2::getLong()
return Base2::getLong() + 1L;
}
virtual
Derived* clone() { // override Base1::clone 和 Base2::clone
return new Derived(getInt(), getLong());
}
private:
};
int main() {
Derived* pd = new Derived(1, 2L);
Base2* pb2 = pd;
long l = pb2->getLong(); // (1)
Base2* p = pb2->clone(); // (2)
}
試想,在(1)這一語句上,我們使用pb2調用getLong這一虛函數,雖然pb2類型是Base2*,但它實際上指向的是類Derived的對象。由前面的知識我們知道,在指針由Dervied*
轉化為Base2*
時,會加上Base2在類Derived內的偏移(為0x10)。那就出問題了,pd2 = (pd2 == nullptr ? nullptr : pd + 0x10),在執行pb2->getLong()時,傳入的是pd2,但實際上調用的是Derived::getLong(),需要的是派生類Derived的指針,怎麼辦?
同時,在(2)這一語句上,返回的是Derived*
指針,但接收的是Base2*
指針,如何在運行時知道對指針進行處理?
解決這兩個問題的方法就是一個被稱為"thunk"的技術。
所謂"thunk",就是在代碼的前面或後面添加一段小的代碼段。
比如在Derived::getLong(),為了調整指針,編譯器會生成這樣一段代碼:
// 偽碼
// thunk for Derived::getLong()
this = this - 0x10
jmp Derived::getLong(this)
而在虛函數表中Derived::getLong()應該在的位置,便由上述thunk的地址代替了。
至於在pb2的clone()函數,則被調整為:
// 偽碼
// thunk for Derived::clone()
this = this - 0x10
Derived* pd = Derived::clone()
pd = (pd == nullptr ? nullptr : pd + 0x10)
return pd
我們看一下反彙編,驗證一下:
可以看到Derived::getLong()確實是這樣的。再看一下Derived::clone():
確實是前面描述的那樣,只不過編譯器將其分為了兩部分,一部分調整this指針,一部分調整返回值。
其實在《深度探索C++對象模型》中,還提到了一種情況,那就是基類指針調用派生類的虛函數,而在派生類的虛函數中又調用基類的虛函數。在這種情況下,在派生類的虛函數中調用基類的虛函數時又要調整this指針。
我覺得這種其實不是問題,因為在派生類中this指針明確是Derived*
類型,既然要調用基類的虛函數,肯定是要將Derived*
類型轉化為Base1*
或者Base2*
類型,自然要進行this指針的調整,這是自然而然的,不需要添加額外的東西。
總的來說,一個派生自n個基類的派生類,除了原本要生成的一個虛函數表外,還要生成n-1個額外的虛函數表。在本例中,有兩個虛函數表被編譯出來:
- 一個主要實體,與Base1(最左側的基類)共用
- 一個次要實體,與Base2(第二個基類)共用
在g++中,這兩個表是緊貼在一起的。我們使用-fdump-class-hierarchy
參數看一下類信息:
Vtable for Derived
Derived::_ZTV7Derived: 9 entries
0 (int (*)(...))0
8 (int (*)(...))(& _ZTI7Derived)
16 (int (*)(...))Base1::getInt // 第一個虛表指針指向的地方
24 (int (*)(...))Derived::clone
32 (int (*)(...))Derived::getLong
40 (int (*)(...))-16
48 (int (*)(...))(& _ZTI7Derived)
56 (int (*)(...))Derived::_ZThn16_N7Derived7getLongEv // 第二個虛表指針指向的地方
64 (int (*)(...))Derived::_ZTchn16_h16_N7Derived5cloneEv
Class Derived
size=32 align=8
base size=32 base align=8
Derived (0x0x7fa4a118e5b0) 0
vptr=((& Derived::_ZTV7Derived) + 16)
Base1 (0x0x7fa4a12e7ea0) 0
primary-for Derived (0x0x7fa4a118e5b0)
Base2 (0x0x7fa4a12e7f00) 16
vptr=((& Derived::_ZTV7Derived) + 56)
虛繼承下的虛函數
《深度探索C++對象模型》中對於虛函數的實現並無講解,只是說再虛繼承體系下虛函數的實現非常複雜,其建議不要在虛基類中定義非靜態的數據成員。所以下麵只是我對於g++對虛繼承下虛函數的實現的觀察,並沒有形成總結。
// test25.cpp
class Point2D {
public:
Point2D(int x, int y)
: m_x(x),
m_y(y)
{}
virtual
~Point2D() {
m_x = m_y = 0;
}
virtual
void allAddOne() {
m_x += 1;
m_y += 1;
}
virtual
int z() const {
return 0;
}
private:
int m_x;
int m_y;
};
class Point3D: virtual public Point2D {
public:
Point3D(int x, int y, int z)
: Point2D(x, y),
m_z(z)
{}
virtual
~Point3D() {
m_z = 0;
}
virtual
void allAddOne() override {
Point2D::allAddOne();
m_z += 1;
}
virtual
int z() const override {
return m_z;
}
private:
int m_z;
};
int main () {
Point3D* p3d = new Point3D(1, 2, 3);
Point2D* p2d = p3d;
p2d->allAddOne();
int z = p2d->z();
}
我們先使用-fdump-class-hierarchy
查看類的信息:
Vtable for Point2D
Point2D::_ZTV7Point2D: 6 entries
0 (int (*)(...))0
8 (int (*)(...))(& _ZTI7Point2D)
16 (int (*)(...))Point2D::~Point2D
24 (int (*)(...))Point2D::~Point2D
32 (int (*)(...))Point2D::allAddOne
40 (int (*)(...))Point2D::z
Class Point2D
size=16 align=8
base size=16 base align=8
Point2D (0x0x7ff517ae7960) 0
vptr=((& Point2D::_ZTV7Point2D) + 16)
Vtable for Point3D
Point3D::_ZTV7Point3D: 16 entries
0 16
8 (int (*)(...))0
16 (int (*)(...))(& _ZTI7Point3D)
24 (int (*)(...))Point3D::~Point3D
32 (int (*)(...))Point3D::~Point3D
40 (int (*)(...))Point3D::allAddOne
48 (int (*)(...))Point3D::z
56 18446744073709551600
64 18446744073709551600
72 18446744073709551600
80 (int (*)(...))-16
88 (int (*)(...))(& _ZTI7Point3D)
96 (int (*)(...))Point3D::_ZTv0_n24_N7Point3DD1Ev
104 (int (*)(...))Point3D::_ZTv0_n24_N7Point3DD0Ev
112 (int (*)(...))Point3D::_ZTv0_n32_N7Point3D9allAddOneEv
120 (int (*)(...))Point3D::_ZTv0_n40_NK7Point3D1zEv
VTT for Point3D
Point3D::_ZTT7Point3D: 2 entries
0 ((& Point3D::_ZTV7Point3D) + 24)
8 ((& Point3D::_ZTV7Point3D) + 96)
Class Point3D
size=32 align=8
base size=12 base align=8
Point3D (0x0x7ff51797d1a0) 0
vptridx=0 vptr=((& Point3D::_ZTV7Point3D) + 24)
Point2D (0x0x7ff517ae7de0) 16 virtual
vptridx=8 vbaseoffset=-24 vptr=((& Point3D::_ZTV7Point3D) + 96)
Point3D對象的結構還是比較簡單的,如下:
(gdb) x/8xw p3d
0x8414e70: 0x08201cd0 0x00000000 0x00000003 0x00000000
0x8414e80: 0x08201d18 0x00000000 0x00000001 0x00000002
很明顯0x08201cd0是Point3D新增的虛表指針,結合類信息,我們知道其指向了((& Point3D::_ZTV7Point3D) + 24);而0x08201d18是繼承自虛基類的放虛表指針的地方,只不過這裡放了Derived類自己的虛表指針,其指向了((& Point3D::_ZTV7Point3D) + 96)。
我們關註的重點是虛基類的虛函數表和其中虛函數的實現:虛函數表中放的是什麼的地址?不像沒有虛基類的多重繼承那樣各個對象的偏移是一定的,(在只有指針或引用的情況下)虛繼承下虛基類的偏移是運行時才能知道的,其中的虛函數又是如何調整this指針的呢?
(gdb) x/4ag 0x08201d18
0x8201d18 <_ZTV7Point3D+96>: 0x8000ba6 <_ZTv0_n24_N7Point3DD1Ev> 0x8000bdb <_ZTv0_n24_N7Point3DD0Ev>
0x8201d28 <_ZTV7Point3D+112>: 0x8000c24 <_ZTv0_n32_N7Point3D9allAddOneEv> 0x8000c3f <_ZTv0_n40_NK7Point3D1zEv>
正如類信息中展示的那樣,虛基類的虛表中放置的正是這幾個函數名字,但這幾個函數是什麼呢?我們使用c++filt看一下:
$ c++filt _ZTv0_n24_N7Point3DD1Ev
virtual thunk to Point3D::~Point3D()
$ c++filt _ZTv0_n24_N7Point3DD0Ev
virtual thunk to Point3D::~Point3D()
$ c++filt _ZTv0_n32_N7Point3D9allAddOneEv
virtual thunk to Point3D::allAddOne()
$ c++filt _ZTv0_n40_NK7Point3D1zEv
virtual thunk to Point3D::z() const
可以看到他們被稱為virtual thunk,看來是和thunk相似的技術,用來調整this指針和返回值,我們來看看其內部是怎麼運行的:
和我們前面討論的thunk非常像,都是調整this指針,只是前面的thunk里this指針調整的值是固定的,而這裡this指針調整的值是動態的放在vptr[-3]
處,我們再看一下這裡放的是什麼,我們直接看g++生成的類信息,虛表指針是指向((& Point3D::_ZTV7Point3D) + 96),那vptr[-3]
就應該是((& Point3D::_ZTV7Point3D) + 72)放的東西了,可以看到是18446744073709551600,把這個值當作一個long類型的值的話正好是-16,這不就是從Point2D*
類型轉化為Point3D*
類型需要減的值嘛(因為Point2D在Point3D類的實體中偏移為16)。我們再檢查一下其他的virtual thunk是不是也是一樣?
嗯,沒問題,再看看下一個:
不好,出現不一樣了,這次偏移是vptr[-4]
這裡,也就是((& Point3D::_ZTV7Point3D) + 64)放的東西,可以看到是18446744073709551600。咦,和上面的是一樣的。考慮到虛表指針指向的前兩項其實是一個函數,只不過一個不調用::operator delete
,一個調用而已。那是不是編譯器為每個虛函數都準備了一個this指針的調整量?我們繼續看最後剩下的那個virtual thunk:
果然是這樣的,這次是vptr[-5]
。
我們可以稍微總結一下虛繼承下虛函數的實現:就是在虛表裡為每個虛函數增加了虛基類指針到override該虛函數的派生類的指針需要對this進行的偏移。
我能做的總結也就是這樣了,如果有大神知道詳細的規則可以評論一下,或者給一個鏈接,謝謝。
這一章後面還有成員函數指針的內容,就留在後面的博客里講吧。