多態(tài)中函數(shù)的重寫(基類指針訪問派生類函數(shù)),只重寫函數(shù)的實現(xiàn),而不重寫聲明。
class Person
{
public:
virtual void fun(int i = 0)
{
cout << "Person"<<" "<<i;
}
};
class Student:public Person
{
public:
virtual void fun(int i = 1)
{
cout << "student" <<" "<<i;
}
};
int main()
{
Person p;
Student st;
Person* pp = &st;
pp->fun();
return 0;
}
結(jié)果是 student 0 原因在于重寫時只重寫函數(shù)的實現(xiàn),就是說相當于Person的fun的聲明和Student的函數(shù)實現(xiàn)的拼在一起所以缺省值是0。
為什么多態(tài)調(diào)用(重寫)只能用父類的指針和引用,不能子類指針或者引用,不能是父類對象?
如果是子類指針或者引用就不是多態(tài)調(diào)用了只是單存子類對父類的重定義,隱藏函數(shù)。
上一篇文章提到的,多態(tài)的本質(zhì)就是基類和派生類的虛表中保存的虛函數(shù)地址被覆蓋了,多態(tài)調(diào)用意味著訪問的必須是子類的虛表而不是父類的。
子類對象直接賦值父類不會拷貝虛表虛函數(shù)的地址
上圖為賦值前,下圖為賦值后,如圖他們的__vfptr始終不同,所以父類對象必然無法訪問子類對象的虛函數(shù)的地址。
為什么指針和引用可以?
父類型指針表示它范圍的范圍是父類,所以它指向子對象時,本質(zhì)上依然說訪問子類的父類部分,虛表依然是子類的虛表。
引用同理,相當于切割出子類中父類的部分。本質(zhì)上依然是子類的虛表。
為什么父類指針可以指向子類對象?可以指向意味著結(jié)構(gòu)相似,原因在于,繼承相當于把父對象一整個拷貝放在子對象中,結(jié)構(gòu)相似也是向上轉(zhuǎn)換的基礎(chǔ)
虛表的存儲在代碼段
證明思路:輸出各個區(qū)的地址和虛表的地址,進行比較,字節(jié)相差較少說明在哪個區(qū)。
int main()
{
Person p;
Student st;
int a = 1;
printf("棧上:%x\n", &a);
int* b = new int;
printf("堆上:%x\n", b);
static int c = 0;
printf("靜態(tài)區(qū):%x\n", &c);
const char* d = "abcde";
printf("代碼段:%x\n", d);
printf("虛表1:%x\n", *((int*)&p));
printf("虛表2:%x\n", *((int*)&st));
return 0;
}
注意打印對象是打印對象的成員變量的值,這里是因為__vptr內(nèi)置變量(保存虛表地址)的在成員變量首位,所以可以打印出來,同時int* 是只取虛表的地址后四個字節(jié)(小端機),%x也是只打印地址的后4位字節(jié)。
通過比較可發(fā)現(xiàn),虛表和代碼段的位置更近,所以虛表在代碼段中。
派生類新的虛函數(shù)保存在虛表中,原有虛函數(shù)的地址的下面
class Person
{
public:
virtual void fun(int i = 0)
{
cout << "Person"<<" "<<i;
}
int _a;
};
class Student:public Person
{
public:
virtual void fun(int i = 1)
{
cout << "student" <<" "<<i;
}
virtual void fun1()
{
cout << "new virtual fun1()";
}
int _b;
};
fun1()是Student的虛函數(shù),fun1保存在子函數(shù)的虛表中
證明:虛表保存函數(shù)指針地址,虛表可以看成指針數(shù)組,所以我們可以把虛表的函數(shù)指針打印出來。
typedef void(*FUNC_PTR) ();//重定義函數(shù)指針類型
//形參是數(shù)組,實參為數(shù)組指針
void PrintVFT(FUNC_PTR table[])
{
//vs會在虛表末尾保存一個空指針,所以循環(huán)到nullptr為止
for (size_t i = 0; table[i] != nullptr; i++)
{
printf("[%d]:%p\n", i, table[i]);
}
}
int main()
{
Person ps;
Student st;
int vft1 = *((int*)&ps);
//86位機器地址是32位轉(zhuǎn)換成int*
PrintVFT((FUNC_PTR*)vft1);
int vft2 = *((int*)&st);
PrintVFT((FUNC_PTR*)vft2);
return 0;
}
?如圖上面為父類虛表保存的地址,下面為派生類虛表保存的指針地址。重寫的虛函數(shù)覆蓋了原有的地址,并且新地址在虛表內(nèi)。
靜態(tài)多態(tài):指的是函數(shù)重載,指的是編譯的時候函數(shù)地址確定了
動態(tài)多態(tài):繼承,虛函數(shù)重寫,調(diào)用的函數(shù)地址的確定是在運行時去虛表中確定的
多繼承的多態(tài)問題
typedef void(*FUNC_PTR) ();
void PrintVFT(FUNC_PTR table[])
{
for (size_t i = 0; table[i] != nullptr; i++)
{
printf("[%d]:%p", i, table[i]);
FUNC_PTR f = table[i];
f();
printf("\n");
}
}
class Base1 {
public:
virtual void func1() { cout << "Base1::func1" << endl; }
virtual void func2() { cout << "Base1::func2" << endl; }
private:
int b1;
};
class Base2 {
public:
virtual void func1() {
cout << "Base2::func1" << endl;
}
virtual void func2() { cout << "Base2::func2" << endl; }
private:
int b2;
};
class Derive :public Base1, public Base2
{
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
private:
int d1;
};
int main()
{
Derive d;
cout << sizeof(d) << endl;
int vft1 = *((int*)&d);
Base2* ptr = &d;
int vft2 = *((int*)ptr);
PrintVFT((FUNC_PTR*)vft1);
PrintVFT((FUNC_PTR*)vft2);
return 0;
}
下面代碼的Derive繼承了Base1和Base2,其中兩個fun1()都被繼承了。
打印結(jié)果
?為什么是20?
?因為是一整個對象繼承,所以會存在兩個虛表,base1,base2虛表指針+int變量 8+8+4=20
由上面的結(jié)果圖可知fun1在兩個虛表中被重寫,且都調(diào)用了同一個函數(shù)。但是地址卻不一樣,
實際上調(diào)用虛表2的fun()的地址,會改變指針位置和虛表1fun()指針相同,再調(diào)用函數(shù)。
反匯編證明
b1,b2指針分別調(diào)用fun1(),反匯編,call指令進入func1函數(shù),此時
注意此處fun1()的地址是0C92840h
調(diào)用base2的fun1虛表地址,此時地址是0C94670h
進入call指令,ecx-8,再jump向0C91244h地址最后到base1虛表的地址。
?
文章來源:http://www.zghlxwxcb.cn/news/detail-707318.html
?簡單來說指向base2部分的指針,先指向base1的,再調(diào)用指針1保存的重寫函數(shù)的地址。文章來源地址http://www.zghlxwxcb.cn/news/detail-707318.html
到了這里,關(guān)于波奇學(xué)C++:多態(tài)知識點的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!