轉載:虛擬函式和虛繼承的記憶體分佈
最近做題的時候經常遇到虛擬函式和虛繼承的記憶體分佈情況問題,自己也有點生疏了,所以,趕緊在這裡回憶補充一下!
先從一個類的記憶體分佈情況開始看起:
環境:VS2012
class A
{
int a;
public:
A()
:a(1)
{}
void fun()
{
cout<<"A"<<endl;
}
};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
按照我的理解,記憶體中只有一個int a; 這個類的大小為 4;下面寫個測試:
A a;
cout<<sizof(a )<<endl;
- 1
- 2
- 1
- 2
先通過除錯看記憶體情況:
sizeof(a) 的結果:
結果和預想的一樣,當然這只是複習的一個熱身;
將fun 變為虛擬函式:
virtual void fun()
{
cout<<"A"<<endl;
}
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
先通過除錯看記憶體情況:
檢視 sizeof(a)的大小:
我們可以看到,在類中加入虛擬函式之後,例項之後的物件會有一個虛擬函式指標,指向一個虛擬函式表,而虛擬函式表中存放的是虛擬函式地址!
下面我們來一個B類繼承A類,看看記憶體的分佈:
class B: public A
{
int b;
public:
B()
:b(2)
{}
void fun()
{
cout<<"B"<<endl;
}
};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
B繼承A,並且有一個成員 b,重寫了A的fun;
測試:
B b;
cout<<"sizeof(b) = "<<sizeof(b)<<endl;
- 1
- 2
- 1
- 2
除錯檢視b的記憶體佈局:
記憶體佈局分析: 我們可以看到先存放的是A 的成員 a, 然後存放的才是 B的成員 b;
sizeof(b) 的結果:
此時我們再讓A的fun為虛擬函式,看看 B繼承 A 之後b的記憶體分佈:
sizeof(b) 的 大小:
分析: 此時b的記憶體佈局是:先是一個虛表指標,然後是 a , b;
我們再看看一個類分別繼承 A 和 B的情況:此時是這樣的情況
class C: public A, public B
{
int c;
public:
C()
:c(3)
{}
void fun()
{
cout<<"C"<<endl;
}
};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
C類先繼承 A 再繼承 B 類, 注意:此時我們B類沒有繼承 A 類,並且沒有虛擬函式
測試
C c;
cout<<"sizeof(c) = "<<sizeof(c)<<endl;
- 1
- 2
- 1
- 2
除錯檢視記憶體:
檢視 sizeof(c) 的結果:
此時將 A 和 B 的fun設為虛擬函式,看看 c 的記憶體佈局:
sizeof(c)的結果
分析: 記憶體佈局,首先是一個虛表指標,和A的一致,然後是a的成員,接著是B的虛表指標,B的資料成員, 最後是C的資料成員;
感覺情況好多,後面的sizeof的結果就不截圖了;
到這裡,我們可以對含有虛擬函式的類的物件的記憶體佈局做一個總結:
首先,記憶體的開始一定是一個虛表指標,指向一個虛表,如果含有繼承,則和第一個繼承的基類的虛表指標一致,然後先存放基類的資料成員,再存放自己的資料成員;還有一點,那就是如果子類中對基類的虛擬函式進行了重寫,那麼會在虛表中覆蓋掉基類虛擬函式的地址!
下面我們要看的是多重繼承的記憶體分佈情況,這裡就只看一種:菱形繼承;
什麼是菱形繼承?
上面我們舉過一個例子, c 分別繼承 A 和 B; 如果我讓 A 和 B 分別再繼承一個基類 Base 的話,這就叫菱形繼承:
大概邏輯關係如下: 沒有虛擬函式
class Base
{
int base;
public:
Base()
:base(0)
{}
};
class A:public Base
{
int a;
public:
A()
:a(1)
{}
void fun()
{
cout<<"A"<<endl;
}
};
class B : public Base
{
int b;
public:
B()
:b(2)
{}
void fun()
{
cout<<"B"<<endl;
}
};
class C: public A, public B
{
int c;
public:
C()
:c(3)
{}
void fun()
{
cout<<"C"<<endl;
}
};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
那麼此時如果有一個 C 的物件 c 的話,c的記憶體佈局應該是怎麼樣的呢?
我們看看 c 的記憶體佈局:
意料之中的佈局,但是有一個問題,c 從A繼承了一個 Base 的 base,又從B繼承了一個Base 的 base,那麼我們在呼叫的時候到底用的是哪一個? 而且如果base是一個非常大的結構體的話,每一個繼承都需要建立一個base,那麼豈不是很浪費空間,這就是多重繼承的二義性和冗餘的問題;
而虛繼承存在的目的便是為了解決這個問題,那麼它是如何解決的呢?
即使得A和B都虛繼承Base,那麼Base就是一個虛基類,只儲存一份,而A和B中關於base的儲存,都是儲存一個指標,指向一個base, 因為把Base的成員存放在了記憶體的最後,這樣根據相對偏移量就找到了這個共有的base;
說了這些都是紙上談兵,終覺淺,我們還是直接來檢視記憶體的佈局吧;
class A:virtual public Base
{
int a;
public:
A()
:a(1)
{}
void fun()
{
cout<<"A"<<endl;
}
};
class B : virtual public Base
{
int b;
public:
B()
:b(2)
{}
void fun()
{
cout<<"B"<<endl;
}
};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
例項一個C 的物件 c ,看記憶體佈局:
我們可以 看到原本應該是00 00 00 00的位置被指標代替了,而base只在最後存了一份;
實際上這兩個指標指向一個虛基表,記錄當前位置到最底下base的偏移量;這樣就達到了共享的目的;
如果對於虛基類只被構造了一次抱有懷疑,可以在前面的測試中的建構函式中加入測試語句檢視;
先複習到這吧,,,好累,後面繼續補充!