柚子快報(bào)邀請(qǐng)碼778899分享:開發(fā)語言 【C++】多態(tài)
柚子快報(bào)邀請(qǐng)碼778899分享:開發(fā)語言 【C++】多態(tài)
多態(tài)的概念
了解多態(tài)之前,我們?cè)诂F(xiàn)實(shí)生活一定經(jīng)歷過這倆個(gè)例子:
????????在購(gòu)買火車票的時(shí)候,會(huì)根據(jù)你的類型來確定你的票價(jià):
成人:全價(jià)學(xué)生:半價(jià)軍人:免費(fèi)
? ? ? ? 而某外賣系統(tǒng)的紅包系統(tǒng),也會(huì)根據(jù)一定類型來確定紅包大?。?/p>
新人:大額紅包不經(jīng)常使用該外賣系統(tǒng)的人:中等額度經(jīng)常使用外面系統(tǒng)的人:小額紅包
從這些例子中,可以大致對(duì)多態(tài)有一定的理解:
多態(tài)的概念:通俗來講,就是多種形態(tài),具體點(diǎn)就是去完成某個(gè)行為,當(dāng)不同的對(duì)象去完成時(shí)會(huì)產(chǎn)生出不同的狀態(tài)。
多態(tài)的定義以及實(shí)現(xiàn)
虛函數(shù)
class Person
{
public:
virtual void fare()
{
cout << "全價(jià)" << endl;
}
};
虛函數(shù):被virtual修飾的類成員函數(shù)被稱為虛函數(shù)。
虛函數(shù)的重寫
class Person
{
public:
virtual void fare()
{
cout << "全價(jià)" << endl;
}
};
class Student : public Person
{
public:
virtual void fare()
{
cout << "半價(jià)" << endl;
}
};
虛函數(shù)的重寫(覆蓋):派生類中有一個(gè)跟基類完全相同的虛函數(shù)(即派生類虛函數(shù)與基類虛函數(shù)的返回值相同,函數(shù)名字,參數(shù)列表完全相同),稱為子類的虛函數(shù)重寫了基類的虛函數(shù)。
class Person
{
public:
virtual void fare()
{
cout << "全價(jià)" << endl;
}
};
class Student : public Person
{
public:
void fare()
{
cout << "半價(jià)" << endl;
}
};
【注意】觀察上述代碼,在重寫基類虛函數(shù)的時(shí)候,派生類的虛函數(shù)在不加virtual關(guān)鍵字時(shí),雖然也可也構(gòu)成重寫(因?yàn)槔^承后基類的虛函數(shù)被繼承下來,在派生類依舊保持虛函數(shù)屬性),但是這種寫法不是很規(guī)范,不建議這樣使用。
通過觀察代碼,前倆段代碼是通過對(duì)象類型來調(diào)用函數(shù),而后面?zhèn)z條代碼是通過看指向的對(duì)象,如果指向的對(duì)象是基類,則調(diào)用基類;如果指向的對(duì)象是派生類,則調(diào)用派生類。
虛函數(shù)重寫的倆個(gè)例外:
1.協(xié)變(基類與與派生類虛函數(shù)返回值類型不同)
派生類重寫基類虛函數(shù)時(shí),與基類虛函數(shù)返回值類型不同。即基類虛函數(shù)返回基類對(duì)象的指針或者引用,派生類虛函數(shù)返回派生類對(duì)象的指針或者引用,稱為協(xié)變。
class A{};
class B : public A{};
class Person
{
public:
virtual A* fare()
{
cout << "全價(jià)" << endl;
}
};
class Student : public Person
{
public:
virtual B* fare()
{
cout << "半價(jià)" << endl;
}
};
2.析構(gòu)函數(shù)的重寫(基類與派生類析構(gòu)函數(shù)的名字不同)
如果基類的析構(gòu)函數(shù)為虛函數(shù),此時(shí)派生類析構(gòu)函數(shù)只要定義,無論是否加virtual關(guān)鍵字,都與基類的析構(gòu)函數(shù)構(gòu)成重寫,雖然基類與派生類析構(gòu)函數(shù)名字不同。雖然函數(shù)名不相同,看起來違背了重寫的規(guī)則,但是這是不對(duì)的,這里可以理解為編譯器對(duì)析構(gòu)函數(shù)的名稱做了特殊處理,編譯后析構(gòu)函數(shù)的名稱被統(tǒng)一處理稱desructor。
觀察下面這段代碼:
class Person
{
public:
~Person()
{
cout << "~Person" << endl;
}
};
class Student : public Person
{
public:
~Student()
{
cout << "~Student" << endl;
}
};
int main()
{
Person* p = new Person;
delete p;
p = new Student;
delete p;
return 0;
}
這段代碼使用了為經(jīng)過處理的析構(gòu)函數(shù),使用Person這個(gè)指針進(jìn)行析構(gòu)第一次p的時(shí)候,會(huì)調(diào)用Person這個(gè)析構(gòu)函數(shù),而使用p析構(gòu)第二次p的時(shí)候,還是只會(huì)調(diào)用Person這個(gè)析構(gòu)函數(shù)。
?
class Person
{
public:
virtual ~Person()
{
cout << "~Person" << endl;
}
};
class Student : public Person
{
public:
virtual ~Student()
{
cout << "~Student" << endl;
}
};
int main()
{
Person* p = new Person;
delete p;
p = new Student;
delete p;
return 0;
}
????????將析構(gòu)函數(shù)前面加上virtual,進(jìn)行虛函數(shù)的重寫,可以將類析構(gòu)函數(shù)都被處理為destructor這個(gè)統(tǒng)一的名字。
????????這里也是由于期望p->destructor()是一個(gè)多態(tài)調(diào)用,而不是普通調(diào)用。通過多態(tài)調(diào)用,可以將第二次的p在student析構(gòu)一次,在Student析構(gòu)一次,在Person析構(gòu)一次。
? ? ? ? 這也是為什么將析構(gòu)函數(shù)統(tǒng)一處理稱destructor這個(gè)名字的原因,為了保證函數(shù)名的統(tǒng)一。
class Person
{
public:
virtual ~Person()
{
cout << "~Person" << endl;
}
};
class Student : public Person
{
public:
virtual ~Student()
{
cout << "~Student" << endl;
}
};
int main()
{
Person* p1 = new Person;
delete p1;
Person* p2 = new Student;
delete p2;
return 0;
}
【注意】只有派生類student的析構(gòu)函數(shù)重寫了person的析構(gòu)函數(shù),當(dāng)p1析構(gòu)時(shí)會(huì)調(diào)用person的析構(gòu)函數(shù),而p2析構(gòu)的時(shí)候會(huì)調(diào)用person與student的析構(gòu)函數(shù)。
【注意】派生類的析構(gòu)可以不寫。
多態(tài)的構(gòu)成條件
多態(tài)時(shí)在不同繼承關(guān)系的類對(duì)象,去調(diào)用同一個(gè)函數(shù),產(chǎn)生了不同的行為。
在繼承中構(gòu)成多態(tài)的倆個(gè)條件:
1.必須通過基類的指針或者引用調(diào)用函數(shù)。
2.被調(diào)用的函數(shù)必須是虛函數(shù),且派生類必須對(duì)基類的虛函數(shù)進(jìn)行重寫。
重寫虛函數(shù)的條件以及一些例外:
條件1:三同“函數(shù)名相同、參數(shù)相同、返回值相同”
條件2:必須是虛函數(shù)(virtual)
例外1:派生類的重寫虛函數(shù)可以不寫virtual,但是建議都加上。
例外2:協(xié)變的返回值可以不同,但是要求返回值必須是父子類關(guān)系的引用與指針。
例外3:析構(gòu)函數(shù)可以名字不同。
【注意】:對(duì)于多態(tài)而言,不同的對(duì)象傳遞過去,會(huì)調(diào)用不同的函數(shù),多態(tài)調(diào)用看的是指向的對(duì)象,而普通類型調(diào)用看的是當(dāng)前類型。
C++11特性:override與fianl
從前面的學(xué)習(xí)中了解到,C++對(duì)函數(shù)重寫的要求比較嚴(yán)格,但是可以由于各種原因?qū)е潞瘮?shù)名字母次序?qū)懛炊鵁o法構(gòu)成重載,而這種錯(cuò)誤在編譯期間是不會(huì)報(bào)錯(cuò)的,只有在程序運(yùn)行時(shí)沒有達(dá)到預(yù)期的結(jié)果需要進(jìn)行調(diào)試會(huì)得不償失。
C++11中提供了override和final倆個(gè)關(guān)鍵字,以便幫助用戶檢測(cè)是否重寫。
fianl:檢測(cè)虛函數(shù),表示該虛函數(shù)不能再被重寫。
class A
{
public:
virtual void Fun1() final
{
cout << "A" << endl;
}
};
class B : public class A
{
public:
virtual void Fun1()
{
cout << "B" << endl;
}
};
override:檢查派生類虛函數(shù)是否重寫了基類某個(gè)虛函數(shù),如果沒有重寫編譯錯(cuò)誤。
class A
{
public:
virtual void Fun1()
{
cout << "A" << endl;
}
};
class B : public A
{
public:
virtual void Fun2() override
{
cout << "B" << endl;
}
};
如何設(shè)計(jì)一個(gè)不想被繼承的類?
倆種方法:
1.C++98的方法:將基類構(gòu)造函數(shù)私有化(private)
class A
{
public:
private:
A()
{}
};
class B : public A
{
public:
B()
{}
};
此時(shí)會(huì)之間報(bào)錯(cuò):無法訪問 private 成員(在“A”類中聲明),私有構(gòu)造函數(shù)在子類不可見,而派生類的構(gòu)造函數(shù)需要調(diào)用基類的構(gòu)造函數(shù)。
? ? ? ? 伴隨著報(bào)錯(cuò)還會(huì)出現(xiàn)一系列的問題:該類如何進(jìn)行初始化操作?可以使用一個(gè)函數(shù)來調(diào)用構(gòu)造函數(shù)。
class A
{
public:
A Creatobj()
{
return A();
}
private:
A()
{}
};
?此時(shí)需要考慮的是,如果沒有初始化對(duì)象,那么該如何調(diào)用在函數(shù)來初始化對(duì)象呢?這是一個(gè)先有蛋還是先有雞的關(guān)系。
可以使用靜態(tài)函數(shù)來初始化創(chuàng)建對(duì)象:
class A
{
public:
static A Creatobj()
{
return A();
}
private:
A()
{}
};
int main()
{
A::Creatobj();
return 0;
}
可以通過這樣的方式來進(jìn)行對(duì)對(duì)象的初始化。
2.C++11的方法:基類后面加一個(gè)fianl可以變成最終類。
在C++11中規(guī)定,如果一個(gè)類后面加關(guān)鍵字fianl會(huì)導(dǎo)致這個(gè)類變成最終類,而使其無法被繼承。
class A final
{
public:
};
class B : public A
{
public:
};
重載、覆蓋(重寫)、隱藏(重定義)的對(duì)比
多態(tài)的原理
虛函數(shù)表
觀察下面這段代碼:
class A
{
public:
virtual void Fun1()
{
cout << "A::Fun1()" << endl;
}
protected:
int _a;
};
int main()
{
A a;
cout << sizeof(a) << endl;
return 0;
}
在x86環(huán)境下,sizeof(a)的值為8.
????????這個(gè)答案可能有點(diǎn)不合常理,這是因?yàn)槌薩a成員是int類型占了4個(gè)字節(jié),還多了一個(gè)_vfptr的指針占4個(gè)字節(jié),該指針被放在對(duì)象的前面(這是在vs環(huán)境下,有些平臺(tái)可能會(huì)放在對(duì)象的后面,這個(gè)跟平臺(tái)有關(guān)系),對(duì)象中的這個(gè)指針被稱為虛函數(shù)表指針(v代表virtual,f代表function)。
? ? ? ? 一個(gè)含有虛函數(shù)的類中都至少都會(huì)有一個(gè)虛函數(shù)表指針,這是因?yàn)樘摵瘮?shù)的地址要被放在虛函數(shù)表中,虛函數(shù)表也簡(jiǎn)稱虛表。
class A
{
public:
virtual void Fun1()
{
cout << "A::Fun1()" << endl;
}
virtual void Fun2()
{
cout << "A::Fun2()" << endl;
}
virtual void Fun3()
{
cout << "A::Fun3()" << endl;
}
protected:
int _a;
};
如果我們存放多個(gè)虛函數(shù),在這個(gè)虛函數(shù)指針?biāo)赶虻谋碇芯蜁?huì)出現(xiàn)三個(gè)地址。
class A
{
public:
virtual void Fun1()
{
cout << "A::Fun1()" << endl;
}
virtual void Fun2()
{
cout << "A::Fun2()" << endl;
}
void Fun3()
{
cout << "A::Fun3()" << endl;
}
protected:
int _a;
};
class B : public A
{
public:
virtual void Fun1()
{
cout << "B::Fun2()" << endl;
}
void Fun4()
{
cout << "B::Fun4()" << endl;
}
protected:
int _b;
};
int main()
{
A a;
B b;
return 0;
}
下面將通過測(cè)試來講解這段代碼:
1.派生類對(duì)象b中也有一個(gè)虛表函數(shù),b對(duì)象由倆部分組成,一部風(fēng)是基類繼承下來的成員,虛表指針也就是存在這部分的,另一部分是自己的成員。
2.基類a對(duì)象和派生類b對(duì)象虛表是不一樣的,這里可以發(fā)現(xiàn)Fun1函數(shù)完成了重寫,所以b的虛表中存的是重寫的B::Fun1,所以虛函數(shù)的重寫也叫做覆蓋,覆蓋就是指虛表中虛函數(shù)的覆蓋。重寫是語法的叫法,覆蓋是原理層的叫法。
3.另外Fun2繼承下來后是虛函數(shù),所以被放進(jìn)了虛表,F(xiàn)un3也被繼承了下來,但是由于沒有被關(guān)鍵字virtual修飾,不是虛函數(shù),所以不會(huì)放進(jìn)虛表。
4.虛函數(shù)表本質(zhì)是一存虛函數(shù)指針的指針數(shù)組,一般情況下這個(gè)數(shù)組最后面都會(huì)放一個(gè)nullptr。
class A
{
public:
virtual void func(int val = 1)
{
std::cout << "A->" << val << std::endl;
}
virtual void test()
{
func();
}
};
class B : public A
{
public:
void func(int val = 0)
{
std::cout << "B->" << val << std::endl;
}
};
int main(int argc, char* argv[])
{
B* p = new B;
p->test();
return 0;
}
測(cè)試這段代碼:其輸出結(jié)果是B->1.
? ? ? ? 這是因?yàn)樘摵瘮?shù)的重寫的是虛函數(shù)的實(shí)現(xiàn),聲明式不會(huì)被改變的,所以在派生類可以不寫virtual,只需要存在三同即好,編譯器會(huì)在檢查之前,將基類的聲明域派生類的函數(shù)實(shí)現(xiàn)結(jié)合。
5.總結(jié)派生類的虛表生成:
先將基類中的虛表內(nèi)容拷貝一份到派生類虛表中如果派生類重寫了基類中的某個(gè)虛函數(shù),用派生類自己的虛函數(shù)覆蓋虛表中基類的虛函數(shù)派生類自己新增的虛函數(shù)按其在派生類中的聲明次序增加到派生類虛表的后面。
6.虛函數(shù)的重寫是接口繼承。
7.虛表是存在常量區(qū)的。
class A
{
public:
virtual void Fun1()
{
cout << "A::Fun1()" << endl;
}
protected:
int _a;
};
class B : public A
{
public:
virtual void Fun1()
{
cout << "B::Fun2()" << endl;
}
protected:
int _b;
};
int main()
{
int a = 0;
printf("棧區(qū):%p\n", &a);
static int b = 0;
printf("靜態(tài)區(qū):%p\n", &b);
int* p = new int;
printf("堆區(qū):%p\n", p);
const char* str = "hello";
printf("常量區(qū):%p\n", str);
A aa;
printf("虛表aa:%p\n", *((int*)&aa));
B bb;
printf("虛表bb:%p\n", *((int*)&bb));
return 0;
}
通過這段代碼的測(cè)試:
基本可以鎖定虛表存儲(chǔ)在常量區(qū)。
8.虛表里存的是虛函數(shù)指針,不是虛函數(shù),虛函數(shù)和普通函數(shù)一樣,都是存在代碼段里的。對(duì)象中存的也不是虛表,而是虛表指針。
多態(tài)的原理
之前的學(xué)習(xí)中我們知道,多態(tài)的條件有倆個(gè):
1.必須通過基類的指針或者引用調(diào)用函數(shù)。
2.被調(diào)用的函數(shù)必須是虛函數(shù),且派生類必須對(duì)基類的虛函數(shù)進(jìn)行重寫。
class A
{
public:
virtual void Fun1()
{
cout << "A::Fun1()" << endl;
}
protected:
int _a;
};
class B : public A
{
public:
virtual void Fun1()
{
cout << "B::Fun2()" << endl;
}
protected:
int _b;
};
int main()
{
A a;
A& pa = a;
pa.Fun1();
B b;
A& pb = b;
pb.Fun1();
return 0;
}
通過測(cè)試,我們可以知道不同對(duì)象去進(jìn)行調(diào)用函數(shù)Fun1的時(shí)候會(huì)展現(xiàn)不同的形態(tài)。
這里有倆個(gè)問題:
1.為什么需要使用基類的指針才可以調(diào)用函數(shù),而不能是派生類指針或者引用。
? ? ? ? 是因?yàn)榕缮愔羔樆蛞弥荒苷{(diào)用派生類的指針和引用,基類的指針或者引用不僅可以調(diào)用基類的指針或引用,也可以調(diào)用派生類的指針或引用。
2.為什么不能是父類的對(duì)象?
? ? ? ? 對(duì)象的切片與指針或者引用的切片不同,當(dāng)派生類賦值給基類對(duì)象進(jìn)行切片,對(duì)象會(huì)進(jìn)行拷貝,但是不會(huì)拷貝虛表,假設(shè)如果拷貝了虛表,然后調(diào)用了派生類的成員函數(shù),但是當(dāng)使用基類指針去指向該基類對(duì)象的時(shí)候,就也只能調(diào)用派生類的成員函數(shù),而不是基類的成員函數(shù)。
class A
{
public:
virtual void Fun1()
{
cout << "A::Fun1()" << endl;
}
protected:
int _a;
};
class B : public A
{
public:
virtual void Fun1()
{
cout << "B::Fun1()" << endl;
}
protected:
int _b;
};
int main()
{
B b;
A& pb = b;
pb.Fun1();
b.Fun1();
return 0;
}
我們通過反匯編的角度,分析使用基類引用與對(duì)象調(diào)用函數(shù)的不同:
這是基類引用時(shí)的匯編代碼。
pb中存的時(shí)基類的指針,將pb移動(dòng)到eax中。
[eax]就是取eax值指向的內(nèi)容,這里相當(dāng)于把b對(duì)象頭4個(gè)字節(jié)(虛表指針)移動(dòng)到了edx
[edx]就是取edx值指向的內(nèi)容,這里相當(dāng)于把虛表中的頭4個(gè)字節(jié)存的虛函數(shù)指針移動(dòng)到eax
call eax中存虛函數(shù)的指針。這里可以看出滿足多態(tài)的調(diào)用,不是在編譯時(shí)確定的,是運(yùn)行起來以后對(duì)象里面取到的。
這里不滿足多態(tài)的條件,所以這里是普通函數(shù)的調(diào)用轉(zhuǎn)換成地址時(shí),是在編譯時(shí)已經(jīng)從符號(hào)表確認(rèn)了函數(shù)的地址,直接call地址。
? ? ? ? 通過上面的了解,看出滿足多態(tài)以后的函數(shù)調(diào)用,不是在編譯時(shí)確定的,是運(yùn)行起來以后到對(duì)象里面取棧的,不滿足多態(tài)的函數(shù)調(diào)用時(shí)編譯時(shí)確認(rèn)好的。
動(dòng)態(tài)綁定與靜態(tài)綁定
在上述例子中總結(jié):
1.靜態(tài)綁定:靜態(tài)綁定又稱為前期綁定(早綁定),在程序編譯期間確定了程序的行為,也稱為靜態(tài)多態(tài),比如函數(shù)重載。
2.動(dòng)態(tài)綁定:動(dòng)態(tài)綁定又稱為后期綁定(晚綁定),是在程序運(yùn)行期間,根據(jù)具體拿到的類型確定程序的具體行為,調(diào)用具體的函數(shù),也稱為動(dòng)態(tài)多態(tài)。
單繼承和多繼承關(guān)系的虛函數(shù)表
單繼承中的虛函數(shù)表
class A
{
public:
virtual void Fun1()
{
cout << "A::Fun1()" << endl;
}
virtual void Fun2()
{
cout << "A::Fun2()" << endl;
}
protected:
int _a;
};
class B : public A
{
public:
virtual void Fun1()
{
cout << "B::Fun1()" << endl;
}
virtual void Fun3()
{
cout << "B::Fun3()" << endl;
}
virtual void Fun4()
{
cout << "B::Fun4()" << endl;
}
protected:
int _b;
};
使用這段代碼進(jìn)行測(cè)試的時(shí)候,在編譯的監(jiān)視窗口看不見fun3和fun4。
這里是編譯器的監(jiān)視窗口故意隱藏了這倆個(gè)函數(shù),也可以認(rèn)為這是一個(gè)bug。
// 打印函數(shù)指針數(shù)組
// void PrintVFT(FUNC_PTR table[])
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");
}
int main()
{
A a;
B b;
int vft1 = *((int*)&a);
PrintVFT((FUNC_PTR*)vft1);
int vft2 = *((int*)&b);
PrintVFT((FUNC_PTR*)vft2);
return 0;
}
這里提供一種思路:
取出a,b對(duì)象的頭4個(gè)字節(jié),就是虛表的指針,前面講解了虛函數(shù)的本質(zhì)是一個(gè)存虛函數(shù)指針的指針數(shù)組,這個(gè)數(shù)組最后面放了一個(gè)nullptr。
1.先取a的地址,強(qiáng)轉(zhuǎn)成一個(gè)int*的指針。
2.再解引用取值,就取到了a對(duì)象的頭4個(gè)字節(jié)的指針,這個(gè)值就是指向虛表的指針。
3.再?gòu)?qiáng)轉(zhuǎn)成FUNC_PTR*,因?yàn)樘摫砭褪且粋€(gè)存FUNC_PTR類型(虛函數(shù)指針類型)的數(shù)組。
4.虛表指針傳遞給PrintVFT進(jìn)行打印虛表。
5.需要說明的是這個(gè)打印虛表的代碼經(jīng)常會(huì)崩潰,因?yàn)榫幾g器有時(shí)對(duì)虛表的處理不干凈,虛表最后沒有放nullptr的話,會(huì)導(dǎo)致越界,這是編譯器的問題。只需要清理解決方案即可。
多繼承中的虛函數(shù)表
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;
};
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
cout << " 虛表地址>" << vTable << endl;
for (int i = 0; vTable[i] != nullptr; ++i)
{
printf(" 第%d個(gè)虛函數(shù)地址 :0X%x,->", i, vTable[i]);
VFPTR f = vTable[i];
f();
}
cout << endl;
}
int main()
{
Derive d;
VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
PrintVTable(vTableb1);
VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));
PrintVTable(vTableb2);
return 0;
}
?多繼承派生類的未重寫的虛函數(shù)放在第一個(gè)繼承基類部分的虛函數(shù)表中。
菱形繼承與虛擬菱形繼承
菱形繼承
class A
{
public:
virtual void Func1()
{
cout << "A::Func1" << endl;
}
public:
int _a;
};
class B : public A
{
public:
virtual void Fun1()
{
cout << "B::Func1" << endl;
}
public:
int _b;
};
class C : public A
{
public:
virtual void Fun1()
{
cout << "B::Func1" << endl;
}
public:
int _c;
};
class D : public B, public C
{
public:
virtual void Fun1()
{
cout << "B::Func1" << endl;
}
public:
int _d;
};
int main()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
這里需要注意在d的所在地址第一塊區(qū)域是類B的所在區(qū)域,里面包含了虛表的地址,以及數(shù)據(jù)B::_a和_b。第二塊區(qū)域是類C的所在區(qū)域,里面包含了虛表的地址,以及數(shù)據(jù)C::_a和_c。
菱形虛擬繼承
class A
{
public:
virtual void Func1()
{
cout << "A::Func1" << endl;
}
public:
int _a;
};
class B : virtual public A
{
public:
virtual void Fun1()
{
cout << "B::Func1" << endl;
}
public:
int _b;
};
class C : virtual public A
{
public:
virtual void Fun1()
{
cout << "B::Func1" << endl;
}
public:
int _c;
};
class D : public B, public C
{
public:
virtual void Fun1()
{
cout << "B::Func1" << endl;
}
public:
int _d;
};
int main()
{
D d;
d._a = 1;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
????????實(shí)際中不建議設(shè)計(jì)出菱形繼承及菱形虛擬繼承,一方面太復(fù)雜容易出現(xiàn)問題,另一方面這樣的模型,訪問基類成員有一定的性能損耗。
? ? ? ? 所以菱形繼承、菱形虛擬繼承一般實(shí)際中很少使用。?
抽象類
概念
? ? ? ? 在虛函數(shù)的后面寫上=0,則這個(gè)函數(shù)變成純虛函數(shù)。包含純虛函數(shù)的類叫做抽象類(也叫接口類),抽象類不能實(shí)例化出對(duì)象。派生類繼承之后也不能實(shí)例化出對(duì)象,只能重寫純虛函數(shù),派生類才能實(shí)例化出對(duì)象。純虛函數(shù)規(guī)范了派生類必須重寫,另外純虛函數(shù)更體現(xiàn)處理接口繼承。
class Car
{
public:
inline virtual void Drive() = 0;
};
class Benz :public Car
{
public:
inline virtual void Drive()
{
cout << "Benz-舒適" << endl;
}
};
class BMW :public Car
{
public:
inline virtual void Drive()
{
cout << "BMW-操控" << endl;
}
};
class BYD :public Car
{
public:
inline virtual void Drive()
{
cout << "BYD-build year dream" << endl;
}
};
void Func(Car* p)
{
p->Drive();
}
int main()
{
Func(new Benz);
Func(new BMW);
Func(new BYD);
return 0;
}
這個(gè)類在現(xiàn)實(shí)世界沒有對(duì)應(yīng)的實(shí)體,其用途可以是作為指針或者引用使用。
接口繼承和實(shí)現(xiàn)繼承
? ? ? ? 普通函數(shù)的繼承是一種實(shí)現(xiàn)繼承,派生類繼承了基類函數(shù),可以使用函數(shù),繼承的是函數(shù)的實(shí)現(xiàn)。
? ? ? ? 虛函數(shù)的繼承是一種接口繼承,派生類繼承的是基類函數(shù)的接口,目的是為了重寫,達(dá)成多態(tài),繼承的是接口。所以如果不實(shí)現(xiàn)多態(tài),就不需要把函數(shù)定義為虛函數(shù)。
面試相關(guān)題
1.
什么是多態(tài)?
答:多態(tài)分為靜態(tài)多態(tài):函數(shù)重載;和動(dòng)態(tài)多態(tài):繼承中的虛函數(shù)重寫+基類指針引用。
2.
什么是重載、重寫
(
覆蓋
)
、重定義
(
隱藏
)
?
答:參考本節(jié)課件內(nèi)容
3.
多態(tài)的實(shí)現(xiàn)原理?
答:靜態(tài)多態(tài):函數(shù)名修飾規(guī)則;動(dòng)態(tài)多態(tài):虛函數(shù)表
4. inline
函數(shù)可以是虛函數(shù)嗎?
答:可以,不過編譯器就忽略
inline
屬性,這個(gè)函數(shù)就不再是inline,因?yàn)樘摵瘮?shù)要放到虛表中去。
5.
靜態(tài)成員可以是虛函數(shù)嗎?
答:不能,因?yàn)殪o態(tài)成員函數(shù)沒有
this
指針,使用類型
::
成員函數(shù)的調(diào)用方式無法訪問虛函數(shù)表,所以靜態(tài)成員函數(shù)無法放進(jìn)虛函數(shù)表。
6.
構(gòu)造函數(shù)可以是虛函數(shù)嗎?
答:不能,因?yàn)閷?duì)象中的虛函數(shù)表指針是在構(gòu)造函數(shù)初始化列表 階段才初始化的。
7.
析構(gòu)函數(shù)可以是虛函數(shù)嗎?什么場(chǎng)景下析構(gòu)函數(shù)是虛函數(shù)?
答:可以,并且最好把基類的析 構(gòu)函數(shù)定義成虛函數(shù)。參考本節(jié)課件內(nèi)容
8.
對(duì)象訪問普通函數(shù)快還是虛函數(shù)更快?
答:首先如果是普通對(duì)象,是一樣快的。如果是指針 對(duì)象或者是引用對(duì)象,則調(diào)用的普通函數(shù)快,因?yàn)闃?gòu)成多態(tài),運(yùn)行時(shí)調(diào)用虛函數(shù)需要到虛函 數(shù)表中去查找。
9.
虛函數(shù)表是在什么階段生成的,存在哪的?
答:虛函數(shù)表是在編譯階段就生成的,一般情況 下存在代碼段(
常量區(qū)
)
的。
10. C++
菱形繼承的問題?虛繼承的原理?
答:參考繼承課件。注意這里不要把虛函數(shù)表和虛基表搞混了。
11.
什么是抽象類?抽象類的作用?
答:參考(3.
抽象類)。抽象類強(qiáng)制重寫了虛函數(shù),另外抽象類體現(xiàn)出了接口繼承關(guān)系。
柚子快報(bào)邀請(qǐng)碼778899分享:開發(fā)語言 【C++】多態(tài)
相關(guān)閱讀
本文內(nèi)容根據(jù)網(wǎng)絡(luò)資料整理,出于傳遞更多信息之目的,不代表金鑰匙跨境贊同其觀點(diǎn)和立場(chǎng)。
轉(zhuǎn)載請(qǐng)注明,如有侵權(quán),聯(lián)系刪除。