為什么要了解C++的對(duì)象模型艰管,我覺得第一點(diǎn)就是了解了C++的對(duì)象模型之后可以避免很多C++的錯(cuò)誤寫法闷游;第二點(diǎn)可以加深對(duì)C++編譯器的理解镊尺,了解C++編譯器在編譯的時(shí)候做了哪些幕后工作悲立;第三點(diǎn)是可以加強(qiáng)對(duì)計(jì)算機(jī)底層的理解,并且可以可以增加對(duì)C語言的理解档冬。文中的內(nèi)容膘茎,是我讀了《深度探索C++對(duì)象模型》這本書之后的一點(diǎn)心得體會(huì)。所有的例子的運(yùn)行環(huán)境是VS2013,64位酷誓,Windows10系統(tǒng)披坏。
如果父類有虛函數(shù),那么子類可以覆寫父類的虛函數(shù)盐数,達(dá)到多態(tài)的效果棒拂。但問題是,在編譯器編譯的時(shí)候不能確定該調(diào)用哪一個(gè)函數(shù)娘扩,所以只能在運(yùn)行時(shí)確定着茸。要在運(yùn)行時(shí)確定壮锻,類對(duì)象里面就必須有虛函數(shù)地址的信息。很明顯涮阔,如果把每個(gè)虛函數(shù)的地址都存入對(duì)象中猜绣,是很浪費(fèi)的,所以對(duì)象里面僅僅保存一個(gè)指向一個(gè)含有類中所有虛函數(shù)地址的指針敬特,這個(gè)地址就是虛函數(shù)表掰邢。相同類對(duì)象的虛函數(shù)表是相同的,所以每個(gè)類的虛函數(shù)表在內(nèi)存中只有一份伟阔。虛函數(shù)表和指針都是在編譯器編譯的時(shí)候加進(jìn)去的辣之,不同的編譯器在實(shí)現(xiàn)細(xì)節(jié)上有差別,這里以VS2013為例皱炉。
虛函數(shù)指針的位置
按常理來說怀估,虛函數(shù)指針不可能放在類對(duì)象的中間,只能放在頭部和尾部合搅。這樣就比較好確定了多搀,如下的代碼就可以區(qū)分是否在頭部。
#include<iostream>
using namespace std;
class A{
public:
int val;
virtual void func(){
cout << "A::func()" << endl;
}
};
int main(){
A a;
char *p1 = (char *)&a;
char *p2 = (char *)&a.val;
if (p1 != p2){
cout << "start" << endl;
}
else{
cout << "end" << endl;
}
return 0;
}
在VS2013中灾部,虛函數(shù)指針被放在了對(duì)象的頭部康铭。
通過虛函數(shù)地址調(diào)用虛函數(shù)
先看下面的代碼:
#include<iostream>
using namespace std;
class A{
public:
int val = 100;
virtual void f1(){
cout << "A::f1()" << endl;
}
virtual void f2(){
cout << "A.val = " << val << endl;
}
};
typedef void(*func)(A *a);
int main(){
A a;
func** pvptr = (func **)&a;
func* vptr = *pvptr;
for (int i = 0; i < 2; i++){
vptr[i](&a);
}
return 0;
}
為了簡(jiǎn)單起見,這里虛函數(shù)的參數(shù)和返回值都是一樣的赌髓,值得注意的是从藤,如果使用這種方式調(diào)用函數(shù),第一個(gè)參數(shù)需要傳入this指針锁蠕,不然會(huì)出問題夷野。虛函數(shù)表是類型為func的數(shù)組,而a的首地址存的是func*類型的匿沛,再對(duì)a取地址扫责,得到的是func**類型。運(yùn)行結(jié)果表明逃呼,在虛函數(shù)表中虛函數(shù)的排列是按照虛函數(shù)的聲明順序來排列的鳖孤,這也是符合一般的習(xí)慣。
可以使用VS2013中提供的命令行提示工具來查看生成類的布局抡笼,在命令行提示工具中輸入:
/* cl /d1 reportSingleClassLayout類名 源文件名 */
cl /d1 reportSingleClassLayoutA virtual_function.cpp
就可以打印出類A的相關(guān)信息苏揣。在Linux下,也有類似的命令:
g++ -fdump -class -hierarchy -fsyntax -only 源文件名
在輸出中可以看到如下的內(nèi)容:
class A size(16):
+---
0 | {vfptr}
8 | val
| <alignment member> (size=4)
+---
A::$vftable@:
| &A_meta
| 0
0 | &A::f1
1 | &A::f2
可以看出類A的布局的確如上面所說推姻。
靜態(tài)聯(lián)編和動(dòng)態(tài)聯(lián)編
簡(jiǎn)單的說平匈,靜態(tài)聯(lián)編就是在編譯的時(shí)候就確定函數(shù)的調(diào)用地址,而動(dòng)態(tài)聯(lián)編就是在運(yùn)行時(shí)在確定函數(shù)調(diào)用的地址。在C++中實(shí)現(xiàn)動(dòng)態(tài)聯(lián)編需要同時(shí)滿足以下三個(gè)條件:虛函數(shù)增炭,繼承關(guān)系忍燥,基類指針或引用指向子類對(duì)象。這會(huì)導(dǎo)致下面的區(qū)別:
#include<iostream>
using namespace std;
class A{
public:
int val = 100;
virtual void f(){
cout << "A::f()" << endl;
}
};
class B :public A{
public:
virtual void f() override{
cout << "B::f()" << endl;
}
};
typedef void(*func)();
int main(){
B b;
A a = b;
A *pa = &b;
a.f();
pa->f();
func **vpa = (func **)&b;
func *va = *vpa;
(*va)();
return 0;
}
運(yùn)行程序隙姿,會(huì)發(fā)現(xiàn)以下的現(xiàn)象梅垄,使用a.f()調(diào)用的父類的方法,而直接使用虛函數(shù)調(diào)用還是調(diào)用的是父類的方法输玷。說明虛函數(shù)指針在賦值的時(shí)候也是被修改了的队丝。通過反匯編,可以發(fā)現(xiàn)欲鹏,使用a.f()調(diào)用的時(shí)候是直接跳轉(zhuǎn)到函數(shù)地址机久,而使用pa->f()調(diào)用需要的步驟更多,說明pa->f()是動(dòng)態(tài)聯(lián)編赔嚎,在運(yùn)行的是才決定調(diào)用哪個(gè)函數(shù)膘盖。
a.f();
00007FF7523B4B2D lea rcx,[a]
00007FF7523B4B32 call A::f (07FF7523B1190h)
pa->f();
00007FF7523B4B37 mov rax,qword ptr [pa]
00007FF7523B4B3C mov rax,qword ptr [rax]
00007FF7523B4B3F mov rcx,qword ptr [pa]
00007FF7523B4B44 call qword ptr [rax]
虛函數(shù)指針有關(guān)的錯(cuò)誤
先看下面一個(gè)程序:
#include<iostream>
using namespace std;
class A{
public:
int val;
virtual void f(){
cout << "A::f()" << endl;
}
A(){
memset(this, 0, sizeof(A));
}
A(const A& obj){
memcpy(this, &obj, sizeof(A));
}
};
class B :public A{
public:
virtual void f() override{
cout << "B::f()" << endl;
}
};
int main(){
A *a1 = new A();
//a1->f();
B b;
A a(b);
a.f();
A *p = &a;
p->f();
return 0;
}
這個(gè)程序有兩個(gè)問題,第一個(gè)尽狠,a1->f()會(huì)出現(xiàn)錯(cuò)誤衔憨,第二,a.f()調(diào)用的是A的函數(shù)袄膏,p->f()調(diào)用的是B的函數(shù)掺冠。其實(shí)是使用memset會(huì)把虛函數(shù)指針清空沉馆,而memcpy會(huì)把虛函數(shù)指針也賦值德崭。第一個(gè)a.f()因?yàn)槭庆o態(tài)聯(lián)編,沒有經(jīng)過虛函數(shù)指針眉厨。
如果出現(xiàn)了這種奇怪的錯(cuò)誤锌奴,不懂虛函數(shù)指針,會(huì)覺得非常奇怪憾股。