- 系列
c++內存分布之虛函數(單一繼承)
c++內存分布之虛函數(多繼承) 【本文】
結論
1.虛函數表指針 和 虛函數表
- 1.1 影響虛函數表指針個數的因素只和派生類的父類個數有關。多一個父類,派生類就多一個虛函數表指針,同時,派生類的虛函數表就額外增加一個
- 1.2 派生類和父類同時含有虛函數,派生類的虛函數按照父類聲明的順序(從左往右),存放在繼承的第一個父類中虛函數表后面,而不是單獨再額外建立一張虛函數表
- 1.3 按照
先聲明、先存儲、先父類、再子類
的順序存放類的成員變量 - 1.4 無論是派生類還是父類,當出現了虛函數(普通虛函數、虛析構函數、純虛函數),排在內存布局最前面的一定是虛函數表指針
2.覆蓋繼承
其實,覆蓋繼承不夠准確。
2.1 成員變量覆蓋
- 派生類和父類出現了同名的成員變量時,派生類僅僅將父類的同名成員隱藏了,而非覆蓋替換
- 派生類調用成員變量時,按照就近原則,調用自身的同名變量,解決了當調用同名變量時出現的二義性的現象
2.2 成員函數覆蓋
需要考慮是否有虛函數的情況
存在虛函數的覆蓋繼承
父類和派生類出現了同名虛函數函數((普通虛函數、純虛函數),派生類的虛函數表中將子類的同名虛函數的地址替換為自身的同名虛函數的地址-------多態出現
不存在虛函數的覆蓋繼承
父類和派生類同時出現同名成員函數,這與成員變量覆蓋繼承的情況是一樣的,派生類屏蔽父類的同名函數
關於
- 演示環境: VS2017 + 32位程序
- 多繼承(本文主要展開)
- 代碼寫的不夠規范: 因為多態中,任何帶虛函數的基類類的析構函數都應該是虛析構函數。但是我這里沒有寫出來,目的是縮短文章篇幅。
序號 | 情況(多繼承,基類個數:2) |
---|---|
1 | 基類均無虛函數(A,派生類有虛函數,B,派生類不含有虛函數) |
2 | 兩個基類中只有一個類有虛函數(A,派生類有虛函數,B,派生類不含有虛函數) |
3 | 兩個基類中都含有虛函數(A,派生類有虛函數,B,派生類不含有虛函數) |
1.基類均無虛函數
1.1 基類均無虛函數,派生類有虛函數
1.1.1 代碼
// 基類
class baseA
{
public:
int _ma = 1;
int _mb = 2;
};
class baseB
{
public:
int _mc = 3;
int _md = 4;
};
// 派生類
class deriveA : public baseA, public baseB
{
public:
virtual void print() { std::cout << "deriveA::print()\n\n\n"; }
int _me = 3;
int _mf = 4;
};
1.1.2 內存分布
1>class deriveA size(28):
1> +---
1> 0 | {vfptr}
1> 4 | +--- (base class baseA)
1> 4 | | _ma
1> 8 | | _mb
1> | +---
1>12 | +--- (base class baseB)
1>12 | | _mc
1>16 | | _md
1> | +---
1>20 | _me
1>24 | _mf
1> +---
1>
1>deriveA::$vftable@:
1> | &deriveA_meta
1> | 0
1> 0 | &deriveA::print
- 因為派生類存在虛函數,故排在最前的是虛函數表指針(此時,虛函數表指針屬於派生類,而非基類),接着在世基類成員變量,這里先是基類baseA,然后才是基類baseB,最后才是派生類成員變量。基類成員變量存儲順序與聲明順序有關(從靠近派生類的基類開始,先基類baseA,然后是基類baseB)。這個順序與之前總結的規律一致: 先基類再派生類,先聲明,先存儲。
- 虛函數表。由於只有派生類存在虛函數,故虛函數表中只有派生類的虛函數地址。
1.1.3 sizeof
28字節 = 基類和派生類總共六個int成員變量 + 虛函數表指針4字節
1.1.4 監視結果
1.2 基類無虛函數,派生類也沒有虛函數
1.2.1 代碼
// 基類
class baseA
{
public:
int _ma = 1;
int _mb = 2;
};
class baseB
{
public:
int _mc = 3;
int _md = 4;
};
// 派生類
class deriveA : public baseA, public baseB
{
public:
int _me = 3;
int _mf = 4;
};
1.2.2 內存布局
1>class deriveA size(24):
1> +---
1> 0 | +--- (base class baseA)
1> 0 | | _ma
1> 4 | | _mb
1> | +---
1> 8 | +--- (base class baseB)
1> 8 | | _mc
1>12 | | _md
1> | +---
1>16 | _me
1>20 | _mf
1> +---
- 虛函數表指針。因為不存在虛函數,故沒有虛函數表指針表和虛函數表。
- 同樣的,靠近派生類的是baseA,然后才是B,故按照先聲明先存儲的順序,先是基類baseA的成員變量,然后是基類baseB的成員變量,最后才是派生類的成員變量。
1.2.3 sizeof
24字節= 基類總4個int成員變量 + 派生類的2個int成員變量 = 4 * 4 + 4 * 2 = 24
1.2.4 監視結果
2.兩個基類中只有一個類有虛函數
2.1 兩個基類中只有一個類有虛函數,派生類有虛函數
2.1.1 代碼
// 基類
class baseA
{
public:
virtual void show() { std::cout << "virtual baseA::show() \n\n"; }
int _ma = 1;
int _mb = 2;
};
class baseB
{
public:
int _mc = 3;
int _md = 4;
};
// 派生類
class deriveA : public baseA, public baseB
{
public:
virtual void print() { std::cout << "virtual deriveA::print() \n\n"; }
int _me = 3;
int _mf = 4;
};
2.1.2 內存布局
1>class deriveA size(28):
1> +---
1> 0 | +--- (base class baseA)
1> 0 | | {vfptr}
1> 4 | | _ma
1> 8 | | _mb
1> | +---
1>12 | +--- (base class baseB)
1>12 | | _mc
1>16 | | _md
1> | +---
1>20 | _me
1>24 | _mf
1> +---
1>
1>deriveA::$vftable@:
1> | &deriveA_meta
1> | 0
1> 0 | &baseA::show
1> 1 | &deriveA::print
- 虛函數表指針。
- 由於基類和派生類都存在虛函數,故存在虛函數表指針。
- 內存布局順序:先基類,再派生,先聲明先存儲。代碼中,基類baseA存在虛函數且基類baseA先聲明,所以,{vfptr}的位置放在了基類baseA中,且排在第一位,然后才是基類baseA的成員變量,接着是基類baseB的成員變量,最后才是派生類的成員變量。
- 虛函數表。
- 存儲虛函數的虛函數表。本例中,虛函數只有基類baseA和派生類才有,故,按照先聲明,先存儲的順序。依次為:&baseA::show,&deriveA::print。
2.1.3 sizeof
28字節 = 基類總共4個int成員變量 + 派生類的2個int成員變量 + 一個虛函數表指針4字節
2.1.4 監視結果
2.1.5 交換baseA和baseB的順序呢?
- 代碼
// 基類
class baseA
{
public:
virtual void show() { std::cout << "virtual baseA::show() \n\n"; }
int _ma = 1;
int _mb = 2;
};
class baseB
{
public:
int _mc = 3;
int _md = 4;
};
// 派生類
class deriveA : public baseB, public baseA
{
public:
virtual void print() { std::cout << "virtual deriveA::print() \n\n"; }
int _me = 3;
int _mf = 4;
};
- 內存分布
1>class deriveA size(28):
1> +---
1> 0 | +--- (base class baseA)
1> 0 | | {vfptr}
1> 4 | | _ma
1> 8 | | _mb
1> | +---
1>12 | +--- (base class baseB)
1>12 | | _mc
1>16 | | _md
1> | +---
1>20 | _me
1>24 | _mf
1> +---
1>
1>deriveA::$vftable@:
1> | &deriveA_meta
1> | 0
1> 0 | &baseA::show
1> 1 | &deriveA::print
1>
1>deriveA::print this adjustor: 0
-
你肯定也發現了。這與2.1.2內存布局的情況是一摸一樣的。
-
存儲順序:先基類,再派生,先聲明,先存儲。由於存在虛函數,所以,這個需要優先考慮。
-
盡管baseB更靠近派生類,但是baseA的優先級更高,因為基類baseA存在虛函數而基類baseB不存在。
-
虛表依然僅按照先聲明像存儲的順序存儲虛函數。基類優先級 > 派生類優先級。
-
2.1.5.3 sizeof
與 2.1.3 sizeof相同。因為沒有成員變量增加和減少。
2.2 兩個基類中只有一個類有虛函數,派生類沒有虛函數
2.2.1 代碼
// 基類
class baseA
{
public:
virtual void show() { std::cout << "virtual baseA::show() \n\n"; }
int _ma = 1;
int _mb = 2;
};
class baseB
{
public:
int _mc = 3;
int _md = 4;
};
// 派生類
class deriveA : public baseA, public baseB
{
public:
int _me = 3;
int _mf = 4;
};
2.2.1 內存分布
1>class deriveA size(28):
1> +---
1> 0 | +--- (base class baseA)
1> 0 | | {vfptr}
1> 4 | | _ma
1> 8 | | _mb
1> | +---
1>12 | +--- (base class baseB)
1>12 | | _mc
1>16 | | _md
1> | +---
1>20 | _me
1>24 | _mf
1> +---
1>
1>deriveA::$vftable@:
1> | &deriveA_meta
1> | 0
1> 0 | &baseA::show
- 虛函數表指針。 因為只有基類baseA存在虛函數,故需要第一個考慮虛函數表指針。 且基類baseA先聲明。
- 虛函數表,因為只有基類baseA存在虛函數,故虛函數表只保存了基類的虛函數地址。
- 存儲順序: 先基類,再派生,先聲明像存儲。所以;最先是baseA,然后是baseB,最后才是派生類。
2.2.2 交換基類順序呢?
- 代碼
class deriveA : public baseB, public baseA
{
.....
}
- 內存分布
1>
1>class deriveA size(28):
1> +---
1> 0 | +--- (base class baseA)
1> 0 | | {vfptr}
1> 4 | | _ma
1> 8 | | _mb
1> | +---
1>12 | +--- (base class baseB)
1>12 | | _mc
1>16 | | _md
1> | +---
1>20 | _me
1>24 | _mf
1> +---
1>
1>deriveA::$vftable@:
1> | &deriveA_meta
1> | 0
1> 0 | &baseA::show
- 你肯定又發現了,這與上面的內存分布情況是一樣的。
- 為什么?因為基類有兩個,且只有其中一個存在虛函數,所以,按照先聲明存儲的規則,且考慮到有虛函數的基類的優先級大於沒有虛函數的基類。故,含有虛函數的類成員變量排在內存分布圖的虛函數表指針的后面,而且是緊挨着。
2.2.3 sizeof
2.2.4 監視結果
3.兩個基類中都含有虛函數
3.1 兩個基類中都含有虛函數,派生類有虛函數
3.1.1 代碼
// 基類
class baseA
{
public:
virtual void show() { std::cout << "virtual baseA::show() \n\n"; }
int _ma = 1;
int _mb = 2;
};
class baseB
{
public:
virtual void play() { std::cout << "virtual baseB::play() \n\n"; }
int _mc = 3;
int _md = 4;
};
// 派生類
class deriveA : public baseA, public baseB
{
public:
virtual void print() { std::cout << "deriveA::print()\n\n"; }
int _me = 3;
int _mf = 4;
};
3.1.2 內存分布
1>class deriveA size(32):
1> +---
1> 0 | +--- (base class baseA)
1> 0 | | {vfptr}
1> 4 | | _ma
1> 8 | | _mb
1> | +---
1>12 | +--- (base class baseB)
1>12 | | {vfptr}
1>16 | | _mc
1>20 | | _md
1> | +---
1>24 | _me
1>28 | _mf
1> +---
1>
1>deriveA::$vftable@baseA@:
1> | &deriveA_meta
1> | 0
1> 0 | &baseA::show
1> 1 | &deriveA::print
1>
1>deriveA::$vftable@baseB@:
1> | -12
1> 0 | &baseB::play
1>
1>deriveA::print this adjustor: 0
- 等等,你是不是也發現了,內存模型中出現了2個{vfptr}。一個屬於基類baseA,另一個屬於基類baseB。
- 虛函數表。 發現了吧:1.派生類的虛函數表多了一張表,2.派生類的虛函數是放在第一張虛函數表中。
- 按照先前的順序:先基類,再派生,先聲明,先存儲。但是有虛函數的類要優先考慮。這里基類baseA和baseB還有派生類都含有虛函數。那么先看基類,按照先聲明先存儲的順序,baseA基類相對baseB基類先聲明,故基類baseA的虛函數表指針首先被存儲,接着再是基類baseA的成員變量,然后是基類baseB的虛函數表指針,基類baseB的成員變量。最后是派生類。
- 為什么派生類的虛函數是追加在第一張虛表的后面? 請看下面的一段匯編(沒學過匯編,不獻丑)結論: 派生類的虛函數是追加在第一張虛表的后面。當需要使用派生類的虛函數是,用第一張表的虛函數表指針指向派生類的虛函數即可。(個人觀點)下面的匯編也應該是這樣:1,找到虛函數表的起始地址,2.找到派生類的虛函數偏移,3.使用虛函數表指針指向派生類的虛函數。
deriveA *pda = &da;
00A7A02E lea eax,[ebp-28h]
00A7A031 mov dword ptr [ebp-34h],eax
pda->print();
00A7A034 mov eax,dword ptr [ebp-34h]
00A7A037 mov edx,dword ptr [eax]
00A7A039 mov esi,esp
00A7A03B mov ecx,dword ptr [ebp-34h]
00A7A03E mov eax,dword ptr [edx+4]
00A7A041 call eax
00A7A043 cmp esi,esp
00A7A045 call 00A714C4
3.1.3 sizeof
32字節 = 基類總共4個int成員變量 + 派生類的2個int變量 + 2個虛函數表指針 = 4 * 4 + 2 * 4 + 2 * 4 = 32
3.1.4 監視結果
監視結果中看不到派生類的虛函數存儲情況。
3.2 兩個基類中都含有虛函數,派生類沒有虛函數
3.2.1 代碼
// 基類
class baseA
{
public:
virtual void show() { std::cout << "virtual baseA::show() \n\n"; }
int _ma = 1;
int _mb = 2;
};
// 基類
class baseB
{
public:
virtual void play() { std::cout << "virtual baseB::play() \n\n"; }
int _mc = 1;
int _md = 2;
};
// 派生類
class deriveA : public baseA , public baseB
{
public:
int _me = 3;
int _mf = 4;
};
3.2.2 內存布局
1>class deriveA size(32):
1> +---
1> 0 | +--- (base class baseA)
1> 0 | | {vfptr}
1> 4 | | _ma
1> 8 | | _mb
1> | +---
1>12 | +--- (base class baseB)
1>12 | | {vfptr}
1>16 | | _mc
1>20 | | _md
1> | +---
1>24 | _me
1>28 | _mf
1> +---
1>
1>deriveA::$vftable@baseA@:
1> | &deriveA_meta
1> | 0
1> 0 | &baseA::show
1>
1>deriveA::$vftable@baseB@:
1> | -12
1> 0 | &baseB::play
- 虛函數表指針。基類baseA先於基類baseB聲明,兩個基類同時含有虛函數,但是先聲明,先存儲。故先存儲基類baseA的虛函數表指針。
- 虛函數表。 多繼承,故每張表各自存儲自己的虛函數表的信息。
3.2.3 sizeof
3.2.4 監視結果
4.擴展繼承3個基類
4.1 3個基類都有虛函數, 派生類沒有虛函數
4.1.1 代碼
// 基類
class baseA
{
public:
virtual void show() { std::cout << "virtual baseA::show() \n\n"; }
int _ma = 1;
int _mb = 2;
};
class baseB
{
public:
virtual void play() { std::cout << "virtual baseB::play() \n\n"; }
int _mc = 3;
int _md = 4;
};
class baseC
{
public:
virtual void listening() { std::cout << "virtual baseC::listening() \n\n"; }
int _mh = 7;
int _mi = 8;
};
// 派生類
class deriveA : public baseA, public baseB, public baseC
{
public:
int _me = 3;
int _mf = 4;
};
4.1.2 內存布局
1>class deriveA size(44):
1> +---
1> 0 | +--- (base class baseA)
1> 0 | | {vfptr}
1> 4 | | _ma
1> 8 | | _mb
1> | +---
1>12 | +--- (base class baseB)
1>12 | | {vfptr}
1>16 | | _mc
1>20 | | _md
1> | +---
1>24 | +--- (base class baseC)
1>24 | | {vfptr}
1>28 | | _mh
1>32 | | _mi
1> | +---
1>36 | _me
1>40 | _mf
1> +---
1>
1>deriveA::$vftable@baseA@:
1> | &deriveA_meta
1> | 0
1> 0 | &baseA::show
1>
1>deriveA::$vftable@baseB@:
1> | -12
1> 0 | &baseB::play
1>
1>deriveA::$vftable@baseC@:
1> | -24
1> 0 | &baseC::listening
4.2 三個基類中每個基類都有虛函數,派生類也有虛函數
4.2.1 代碼
// 基類
class baseA
{
public:
virtual void show() { std::cout << "virtual baseA::show() \n\n"; }
int _ma = 1;
int _mb = 2;
};
class baseB
{
public:
virtual void play() { std::cout << "virtual baseB::play() \n\n"; }
int _mc = 3;
int _md = 4;
};
class baseC
{
public:
virtual void listening() { std::cout << "virtual baseC::listening() \n\n"; }
int _mh = 7;
int _mi = 8;
};
// 派生類
class deriveA : public baseA, public baseB, public baseC
{
public:
virtual void show() { std::cout << "virtual deriveA::show() \n\n"; }
int _me = 3;
int _mf = 4;
};
4.2.2 內存布局
1>class deriveA size(44):
1> +---
1> 0 | +--- (base class baseA)
1> 0 | | {vfptr}
1> 4 | | _ma
1> 8 | | _mb
1> | +---
1>12 | +--- (base class baseB)
1>12 | | {vfptr}
1>16 | | _mc
1>20 | | _md
1> | +---
1>24 | +--- (base class baseC)
1>24 | | {vfptr}
1>28 | | _mh
1>32 | | _mi
1> | +---
1>36 | _me
1>40 | _mf
1> +---
1>
1>deriveA::$vftable@baseA@:
1> | &deriveA_meta
1> | 0
1> 0 | &deriveA::show
1>
1>deriveA::$vftable@baseB@:
1> | -12
1> 0 | &baseB::play
1>
1>deriveA::$vftable@baseC@:
1> | -24
1> 0 | &baseC::listening
1>
1>deriveA::show this adjustor: 0
- Note: 派生類的虛函數是放在第一張虛函數表中的。
4.3 三個基類中其中2個基類都有虛函數,另一個基類沒有虛函數,派生類沒有虛函數
4.3.1 代碼
// 基類
class baseA
{
public:
int _ma = 1;
int _mb = 2;
};
class baseB
{
public:
virtual void play() { std::cout << "virtual baseB::play() \n\n"; }
int _mc = 3;
int _md = 4;
};
class baseC
{
public:
virtual void listening() { std::cout << "virtual baseC::listening() \n\n"; }
int _mh = 7;
int _mi = 8;
};
// 派生類
class deriveA : public baseA, public baseB, public baseC
{
public:
int _me = 3;
int _mf = 4;
};
4.3.2 內存布局
1>class deriveA size(40):
1> +---
1> 0 | +--- (base class baseB)
1> 0 | | {vfptr}
1> 4 | | _mc
1> 8 | | _md
1> | +---
1>12 | +--- (base class baseC)
1>12 | | {vfptr}
1>16 | | _mh
1>20 | | _mi
1> | +---
1>24 | +--- (base class baseA)
1>24 | | _ma
1>28 | | _mb
1> | +---
1>32 | _me
1>36 | _mf
1> +---
1>
1>deriveA::$vftable@baseB@:
1> | &deriveA_meta
1> | 0
1> 0 | &baseB::play
1>
1>deriveA::$vftable@baseC@:
1> | -12
1> 0 | &baseC::listening
4.4 三個基類中其中2個基類都有虛函數,另一個基類沒有虛函數,派生類有虛函數
4.4.1 代碼
// 基類
class baseA
{
public:
// virtual void show() { std::cout << "virtual baseA::show() \n\n"; }
int _ma = 1;
int _mb = 2;
};
class baseB
{
public:
virtual void play() { std::cout << "virtual baseB::play() \n\n"; }
int _mc = 3;
int _md = 4;
};
class baseC
{
public:
virtual void listening() { std::cout << "virtual baseC::listening() \n\n"; }
int _mh = 7;
int _mi = 8;
};
// 派生類
class deriveA : public baseA, public baseB, public baseC
{
public:
virtual void show() { std::cout << "virtual deriveA::show() \n\n"; }
int _me = 3;
int _mf = 4;
};
4.4.2 內存分布
1>
1>class deriveA size(40):
1> +---
1> 0 | +--- (base class baseB)
1> 0 | | {vfptr}
1> 4 | | _mc
1> 8 | | _md
1> | +---
1>12 | +--- (base class baseC)
1>12 | | {vfptr}
1>16 | | _mh
1>20 | | _mi
1> | +---
1>24 | +--- (base class baseA)
1>24 | | _ma
1>28 | | _mb
1> | +---
1>32 | _me
1>36 | _mf
1> +---
1>
1>deriveA::$vftable@baseB@:
1> | &deriveA_meta
1> | 0
1> 0 | &baseB::play
1> 1 | &deriveA::show
1>
1>deriveA::$vftable@baseC@:
1> | -12
1> 0 | &baseC::listening
1>
1>deriveA::show this adjustor: 0
- Note: 派生類的虛函數存放在第一張虛函數表中。