網站首頁 編程語言 正文
多態概念引入
多態字面意思就是多種形態。
我們先來想一想在日常生活中的多態例子:買票時,成人買票全價,如果是學生那么半價,如果是軍人,就可以優先買票。不同的人買票會有不同的實現方法,這就是多態。
1、C++中多態的實現
1.1 多態的構成條件
C++的多態必須滿足兩個條件:
1 必須通過基類的指針或者引用調用虛函數
2 被調用的函數是虛函數,且必須完成對基類虛函數的重寫
我們來看看具體實現。
class Person //成人
{
public:
virtual void fun()
{
cout << "全價票" << endl; //成人票全價
}
};
class Student : public Person //學生
{
public:
virtual void fun() //子類完成對父類虛函數的重寫
{
cout << "半價票" << endl;//學生票半價
}
};
void BuyTicket(Person* p)
{
p->fun();
}
int main()
{
Student st;
Person p;
BuyTicket(&st);//子類對象切片過去
BuyTicket(&p);//父類對象傳地址
}
調用的兩個BuyTicket() 答案是什么呢?
如果不滿足多態呢?
這說明了很重要的一點,如果滿足多態,編譯器會調用指針指向對象的虛函數,而與指針的類型無關。如果不滿足多態,編譯器會直接根據指針的類型去調用虛函數。
1.2 虛函數
用virtual修飾的關鍵字就是虛函數。
虛函數只能是類中非靜態的成員函數。
virtual void fun() //error! 在類外面的函數不能是虛函數
{}
1.3虛函數的重寫
子類和父類中的虛函數擁有相同的名字,返回值,參數列表,那么稱子類中的虛函數重寫了父類的虛函數,或者叫做覆蓋。
class Person
{
public:
virtual void fun()
{
cout << "Person->fun()" << endl;
}
};
class Student
{
public:
//子類重寫的虛函數可以不加virtual,因為子類繼承了父類的虛函數,
//編譯器會認為你是想要重寫虛函數。
//void fun() 可以直接這樣,也對,但不推薦。
virtual void fun()//子類重寫父類虛函數
{
cout << "Student->fun()" << endl;
}
};
虛函數重寫的兩個例外:
協變:
子類的虛函數和父類的虛函數的返回值可以不同,也能構成重載。但需要子類的返回值是一個子類的指針或者引用,父類的返回值是一個父類的指針或者引用,且返回值代表的兩個類也成繼承關系。這個叫做協變。
class Person
{
public:
virtual Person* fun()//返回父類指針
{
cout << "Person->fun()" << endl;
return nullptr;
}
};
class Student
{
public:
//返回子類指針,雖然返回值不同,也構成重寫
virtual Student* fun()//子類重寫父類虛函數
{
cout << "Student->fun()" << endl;
return nullptr;
}
};
也可以這樣,也是協變,
class A
{};
class B : public A
{}; //B繼承A
class Person
{
public:
virtual A* fun()//返回A類指針
{
return nullptr;
}
};
class Student
{
public:
//返回B類指針,雖然返回值不同,也構成重寫
virtual B* fun()//子類重寫父類虛函數
{
return nullptr;
}
};
2.析構函數的重寫
析構函數是否需要重寫呢?
讓我們來考慮這樣一種情況,
//B繼承了A,他們的析構函數沒有重寫。
class A
{
public:
~A()
{
cout << "~A()" << endl;
}
};
class B : public A
{
public:
~B()
{
cout << "~B()" << endl;
}
};
A* a = new B; //把B的對象切片給A類型的指針。
delete a; //調用的是誰的析構函數呢?你希望調用誰的呢?
顯然我們希望調用B的析構函數,因為我們希望析構函數的調用跟指針指向的對象有關,而跟指針的類型無關。這不就是多態嗎?但是結果卻調用了A的析構函數。
所以析構函數要實現多態。But,析構函數名字天生不一樣,怎么實現多態?
實際上,析構函數被編譯器全部換成了Destructor,所以我們加上virtual就可以。
只要父類的析構函數用virtual修飾,無論子類是否有virtual,都構成析構。
這也解釋了為什么子類不寫virtual可以構成重寫,因為編譯器怕你忘記析構。
class A
{
public:
virtual ~A()
{
cout << "~A()" << endl;
}
};
class B : public A
{
public:
virtual ~B()
{
cout << "~B()" << endl;
}
};
1.4 C++11 override && final
C++11新增了兩個關鍵字。用final修飾的虛函數無法重寫。用final修飾的類無法被繼承。final像這個單詞的意思一樣,這就是最終的版本,不用再更新了。
class A final //A類無法被繼承
{
public:
virtual void fun() final //fun函數無法被重寫
{}
};
class B : public A //error
{
public:
virtual void fun() //error
{
cout << endl;
}
};
被override修飾的虛函數,編譯器會檢查這個虛函數是否重寫。如果沒有重寫,編譯器會報錯。
class A
{
public:
virtual void fun()
{}
};
class B : public A
{
public:
//這里我想重寫fun,但寫成了fun1,因為有override,編譯器會報錯。
virtual void fun1() override
{
cout << endl;
}
};
1.5 重載,覆蓋(重寫),重定義(隱藏)
這里我們來理一理這三個概念。
1.重載:重載函數處在同一作用域。
函數名相同,函數列表必須不同。
2.覆蓋:必須是虛函數,且處在父類和子類中。
返回值,參數列表,函數名必須完全相同(協變除外)。
3.重定義:子類和父類的成員變量相同或者函數名相同,
子類隱藏父類的對應成員。
子類和父類的同名函數不是重定義就是重寫。
2、抽象類
2.1 抽象類的概念
再虛函數的后面加上=0就是純虛函數,有純虛函數的類就是抽象類,也叫做接口類。抽象類無法實例化出對象。抽象類的子類也無法實例化出對象,除非重寫父類的虛函數。
class Car
{
public:
virtual void fun() = 0; //不用實現,只寫接口就行。
}
這并不意味著純虛函數不能寫實現,只是我們大部分情況下不寫。
那么虛函數有什么用呢?
1,強制子類重寫虛函數,完成多態。
2,表示某些抽象類。
2.2 接口繼承和實現繼承
普通函數的繼承就是實現繼承,虛函數的繼承就是接口繼承。子類繼承了函數的實現,可以直接使用。虛函數重寫后只會繼承接口,重寫實現。所以如果不用多態,不要把函數寫成虛函數。
純虛函數就體現了接口繼承。下面我們來一道題,展現一下接口繼承。
class A
{
public:
virtual void fun(int val = 0)//父類虛函數
{
cout <<"A->val = "<< val << endl;
}
void Fun()
{
fun();//傳過來一個子類指針調用fun()
}
};
class B: public A
{
public:
virtual void fun(int val = 1)//子類虛函數
{
cout << "B->val = " << val << endl;
}
};
B b;
A* a = &b;
a->Fun();
結果是什么呢?
B->val = 0
子類對象切片給父類指針,傳給Fun函數,滿足多態,會去調用子類的fun函數,但是子類的虛函數繼承了父類的接口,所以val是父類的0.
3、 多態的原理
3.1 虛函數表
多態是怎樣實現的呢?
先來一道題目,
class A
{
public:
virtual void fun()
{}
protected:
int _a;
};
sizeof(A)是多少?是4嗎?NO,NO,NO!
答案是8個字節。
我們定義一個A類型的對象a,打開調試窗口,發現a的內容如下
我們發現除了成員變量_a以外,還多了一個指針。這個指針是不準確的,實際上應該是_vftptr(virtual function table pointer),即虛函數表指針,簡稱虛表指針。在計算類大小的時候要加上這個指針的大小。那么虛表是什么呢?虛表就是存放虛函數的地址地方。每當我們去調用虛函數,編譯器就會通過虛表指針去虛表里面查找。
下面我們用一個小栗子來說明虛函數的使用會用指針。
class A
{
public:
void fun1()
{}
virtual void fun2()
{}
};
A* ap = nullptr;
ap->fun1(); //調用成功,因為這是普通函數的調用
ap->fun2(); //調用失敗,虛函數需要對指針操作,無法操作空指針。
我們先來看看繼承的虛函數表。
class A
{
public:
virtual void fun1()
{}
virtual void fun2()
{}
};
class B : public A
{
public:
virtual void fun1()//重寫父類虛函數
{}
virtual void fun3()
{}
};
A a;
B b; //我們通過調試看看對象a和b的內存模型。
子類跟父類一樣有一個虛表指針。
子類的虛函數表一部分繼承自父類。如果重寫了虛函數,那么子類的虛函數會在虛表上覆蓋父類的虛函數。
本質上虛函數表是一個虛函數指針數組,最后一個元素是nullptr,代表虛表的結束。
所以,如果繼承了虛函數,那么
1 子類先拷貝一份父類虛表,然后用一個虛表指針指向這個虛表。
2 如果有虛函數重寫,那么在子類的虛表上用子類的虛函數覆蓋。
3 子類新增的虛函數按其在子類中的聲明次序增加到子類虛表的最后。
下面來一道面試題:
虛函數存在哪里?
虛函數表存在哪里?
虛函數是帶有virtual的函數,虛函數表是存放虛函數地址的指針數組,虛函數表指針指向這個數組。對象中存的是虛函數指針,不是虛函數表。
虛函數和普通函數一樣存在代碼段。
那么虛函數表存在哪里呢?
我們創建兩個A對象,發現他們的虛函數指針相同,這說明他們的虛函數表屬于類,不屬于對象。所以虛函數表應該存在共有區。
堆?堆需要動態開辟,動態銷毀,不合適。
靜態區?靜態區存放全局變量和靜態變量不合適。
所以綜合考慮,把虛函數表也存放在了代碼段。
3.2多態的原理
我們現在來看看多態的原理。
class Person //成人
{
public:
virtual void fun()
{
cout << "全價票" << endl; //成人票全價
}
};
class Student : public Person //學生
{
public:
virtual void fun() //子類完成對父類虛函數的重寫
{
cout << "半價票" << endl;//學生票半價
}
};
void BuyTicket(Person* p)
{
p->fun();
}
這樣就實現了不同對象去調用同一函數,展現出不同的形態。
滿足多態的函數調用是程序運行是去對象的虛表查找的,而虛表是在編譯時確定的。
普通函數的調用是編譯時就確定的。
3.3動態綁定與靜態綁定
1.靜態綁定又稱為前期綁定(早綁定),在程序編譯期間確定了程序的行為,也稱為靜態多態,比如:函數重載
2.動態綁定又稱后期綁定(晚綁定),是在程序運行期間,根據具體拿到的類型確定程序的具體行為,調用具體的函數,也稱為動態多態。
我們說的多態一般是指動態多態。
這里我附上一個有意思的問題:
就是在子類已經覆蓋了父類的虛函數的情況下,為什么子類還是可以調用“被覆蓋”的父類的虛函數呢?
#include <iostream>
using namespace std;
class Base {
public:
virtual void func() {
cout << "Base func\n";
}
};
class Son : public Base {
public:
void func() {
Base::func();
cout << "Son func\n";
}
};
int main()
{
Son b;
b.func();
return 0;
}
輸出:Base func
Son func
這是C++提供的一個回避虛函數的機制
通過加作用域(正如你所嘗試的),使得函數在編譯時就綁定。
(這題來自:虛函數)
4 、繼承中的虛函數表
4.1 單繼承中的虛函數表
這里DV繼承BV。
class BV
{
public:
virtual void Fun1()
{
cout << "BV->Fun1()" << endl;
}
virtual void Fun2()
{
cout << "BV->Fun2()" << endl;
}
};
class DV : public BV
{
public:
virtual void Fun1()
{
cout << "DV->Fun1()" << endl;
}
virtual void Fun3()
{
cout << "DV->Fun3()" << endl;
}
virtual void Fun4()
{
cout << "DV->Fun4()" << endl;
}
};
我們想個辦法打印虛表,
typedef void(*V_PTR)(); //typedef一下函數指針,相當于把返回值為void型的
//函數指針定義成 V_PTR.
void PrintPFTable(V_PTR* table)//打印虛函數表
{ //因為虛表最后一個為nllptr,我們可以利用這個打印虛表。
for (size_t i = 0; table[i] != nullptr; ++i)
{
printf("table[%d] : %p->", i, table[i]);
V_PTR f = table[i];
f();
cout << endl;
}
}
BV b;
DV d;
// 取出b、d對象的前四個字節,就是虛表的指針,
//前面我們說了虛函數表本質是一個存虛函數指針的指針數組,
//這個數組最后面放了一個nullptr
// 1.先取b的地址,強轉成一個int*的指針
// 2.再解引用取值,就取到了b對象前4個字節的值,這個值就是指向虛表的指針
// 3.再強轉成V_PTR*,這是我們打印虛表函數的類型。
// 4.虛表指針傳給PrintPFTable函數,打印虛表
// 5,有時候編譯器資源釋放不完全,我們需要清理一下,不然會打印多余結果。
PrintPFTable((V_PTR*)(*(int*)&b));
PrintPFTable((V_PTR*)(*(int*)&d));
結果如下:
4.2 多繼承中的虛函數表
我們先來看一看一道題目,
class A
{
public:
virtual void fun1()
{
cout << "A->fun1()" << endl;
}
protected:
int _a;
};
class B
{
public:
virtual void fun1()
{
cout << "B->fun1()" << endl;
}
protected:
int _b;
};
class C : public A, public B
{
public:
virtual void fun1()
{
cout << "C->fun1()" << endl;
}
protected:
int _c;
};
C c;
//sizeof(c) 是多少呢?
sizeof( c )的大小是多少呢?是16嗎?一個虛表指針,三個lnt,考慮內存對齊后確實是16.但是結果是20.
我們來看看內存模型。在VS下,c竟然有兩個虛指針
每個虛表里都有一個fun1函數。
所以C的內存模型應該是這樣的,
而且如果C自己有多余的虛函數,會按照繼承順序補在第一張虛表后面。
下面還有一個問題,可以看到C::fun1在兩張虛表上都覆蓋了,但是它們的地址不一樣,是不是說在代碼段有兩段相同的C::fun1呢?
不是的。實際上兩個fun1是同一個fun1,里面放的是跳轉指令而已。C++也會不犯這個小問題。
最后,我們來打印一下多繼承的虛表。
//Derive繼承Base1和Base2
class Base1
{
public:
virtual void fun1()
{
cout << "Base1->fun1()" << endl;
}
virtual void fun2()
{
cout << "Base1->fun2()" << endl;
}
};
class Base2
{
public:
virtual void fun1()
{
cout << "Base2->fun1()" << endl;
}
virtual void fun2()
{
cout << "Base2->fun2()" << endl;
}
};
class Derive : public Base1, public Base2
{
public:
virtual void fun1()
{
cout << "Derive->fun1()" << endl;
}
virtual void fun3()
{
cout << "Derive->fun3()" << endl;
}
};
打印的細節,從Base2繼承過來的虛表指針放在第一個虛表指針后面,我們想要拿到這個指針需要往后挪一個指針加上一個int的字節,但是指針的大小跟操作系統的位數有關,所以我們可以用加上Base2的大小個字節來偏移。
這里注意要先強轉成char*,不然指針的加減會根據指針的類型來確定。
Derive d;
PrintPFTable((V_PTR*)(*(int*)&d));
PrintPFTable((V_PTR*)(*(int*)((char*)&d+sizeof(Base2))));
Ret:
總結
原文鏈接:https://blog.csdn.net/qq_53558968/article/details/116886784
相關推薦
- 2022-09-26 網絡瀏覽器中運行Python腳本PyScript剖析_python
- 2022-12-31 Kotlin?Jetpack組件ViewModel使用詳解_Android
- 2022-03-21 C#中使用CliWrap讓命令行交互舉重若輕_C#教程
- 2022-05-19 python中join與os.path.join()函數實例詳解_python
- 2022-06-15 Python中的?any()?函數和?all()?函數_python
- 2023-03-29 Python-apply(lambda?x:?)的使用及說明_python
- 2022-06-19 docker容器非root用戶提權的問題解決_docker
- 2022-04-21 Docker容器跨主機通信overlay網絡的解決方案_docker
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支