🔶多態基礎概念
?🔶概念
??🔱多態性
??🔱多態——重新(覆蓋)
?🔶示例
??🔶基本使用方法
??🔶特例
???🔱協變
???🔱析構函數重寫
?🔱多態原理
??🔱1. 虛函數形成虛表
??🔱2. 虛函數存儲位置(覆蓋)
??🔱3. 多態中重寫的虛函數存儲位置
???🔱1. 重寫原理——虛表
???🔱2. 單繼承中,子類新增虛函數會存到父類的虛表中——普通繼承
???🔱3. 單繼承中,子類新增虛函數會存到父類的虛表中——虛繼承
???🔱4. 同類公用一個虛表;父類和子類不共用一張虛表
?🔱多態例題
🔱經典問題
多態基礎概念
概念
多態性
?1. 靜態多態
:函數重載和運算符重載
?2. 動態多態
:繼承和虛函數
多態——重寫(覆蓋)
?1. 父類的指針/引用調用虛函數
?2. 調用的虛函數必須是子類重寫的虛函數
這樣就能在指針調用相應的對象函數的時候使用相應的成員函數,具體看示例
這里條件很嚴格
重寫的函數要是一摸一樣
——返回值,函數名,參數個數,參數位置,參數類型都要完全一樣,虛函數之后的const也要一樣
示例
基本使用方法
- 父類中需要使用virtual修飾函數,子類中virtual可以不寫
class A
{
public:virtual void func(){puts("A-->func");}
};
class B:public A
{
public:virtual void func(){puts("B-->func");}
};
int main()
{// 父類指向子類A* a1 = new B;a1->func();// 父類指向父類a1 = new A;a1->func();// 父類引用子類B tb;A& a2 = tb;a2.func();// 父類引用父類A ta;A& a3 = ta; // 不能直接使用a2=ta,引用不能重新賦值,雖然他不會報錯,但是他的結果是錯的a3.func();return 0;
}
final
修飾類——不能繼承
修飾虛函數——不能背重寫
override
——這個函數一定要重新父類的某一個虛函數
一定要注意這兩個關鍵字加載虛函數結尾
特例
協變
虛函數的返回值可以不一樣
,只能出現父類返回父類的指針/引用,子類返回子類的指針/引用
不可以一個返回指針,一個返回引用
只能同時返回指針/同時返回引用
析構函數重寫
#include<iostream>
using namespace std;
typedef void (*T)();
class A
{
public:virtual void fun1(){cout << "A::fun1" << endl;}virtual void fun2(){cout << "A::fun2" << endl;}~A(){cout << "delete A" << endl;}int _a = 1;};
class B :public A
{
public:virtual void fun1(){cout << "B::fun1" << endl;}virtual void fun3(){cout << "B::fun3" << endl;}~B(){cout << "delete B" << endl;}int _b = 2;
};
int main()
{A* a = new B;delete a;return 0;
}
delete
釋放看的是類型
,也就是說這里delete調用的是A的析構函數
根本上說,delete
會被處理成—>destructor() + operator delete
,所以他們能構成重寫
,在具體實現的時候需要寫成virtual
virtual ~A(){cout << "delete A" << endl;}
?多態原理
?1. 虛函數形成虛表
#include<iostream>
using namespace std;
typedef void (*T)();
class A
{
public:virtual void fun1(){cout << "A::fun1" << endl;}virtual void fun2(){cout << "A::fun2" << endl;}};
int main()
{A a;return 0;
}
?2. 虛函數存儲位置
虛函數和普通函數放在一起,虛表存儲在代碼段
?3. 多態中重寫的虛函數存儲位置
🎭1. 重寫原理——虛表
#include<iostream>
using namespace std;
typedef void (*T)();
class A
{
public:virtual void fun1(){cout << "A::fun1" << endl;}virtual void fun2(){cout << "A::fun2" << endl;}};
class B :public A
{
public:virtual void fun1(){cout << "B::fun1" << endl;}
};
int main()
{B b;return 0;
}
🎭2. 單繼承中,子類新增虛函數會存到父類的虛表中——普通繼承
#include<iostream>
using namespace std;
typedef void (*T)();
class A
{
public:virtual void fun1(){cout << "A::fun1" << endl;}virtual void fun2(){cout << "A::fun2" << endl;}};
class B :public A
{
public:virtual void fun1(){cout << "B::fun1" << endl;}virtual void fun3(){cout << "B::fun3" << endl;}
};
int main()
{B b;print((T*)(*(int*)(&b)));return 0;
}
vs中虛表通常在最后一個都是0,Linux不是
🎭3. 單繼承中,子類新增虛函數會存到父類的虛表中——虛繼承
#include<iostream>
using namespace std;
typedef void (*T)();
class A
{
public:virtual void fun1(){cout << "A::fun1" << endl;}virtual void fun2(){cout << "A::fun2" << endl;}int _a = 1;};
class B :virtual public A
{
public:virtual void fun1(){cout << "B::fun1" << endl;}virtual void fun3(){cout << "B::fun3" << endl;}int _b = 2;
};
int main()
{B b;A* a = &b; // 要注意這種寫法,確保他能準確跳到下一個虛表處print((T*)(*(int*)(a)));print((T*)(*(int*)(&b)));return 0;
}
🎭4. 同類公用一個虛表;父類和子類不共用一張虛表
#include<iostream>
using namespace std;
typedef void (*T)();
class A
{
public:virtual void fun1(){cout << "A::fun1" << endl;}virtual void fun2(){cout << "A::fun2" << endl;}int _a = 1;};
class B :virtual public A
{
public:virtual void fun1(){cout << "B::fun1" << endl;}int _b = 2;
};
int main()
{A a;B b1;B b2;return 0;
}
🖼多態例題
class A
{
public:virtual void fun(int val = 1){cout << "val = " << val << endl;}virtual void test(){fun();}
};
class B :public A
{
public:virtual void fun(int val = 0){cout << "val = " << val << endl;}
};
int main()
{A* a = new B;a->test();return 0;
}
void print(T a[])
{for (int i = 0; a[i] != 0; i++){printf("[%d]--->%p\n", i, a[i]);}puts("");
}
int main()
{B b;print((T*)(*(int*)(&b)));return 0;
}
- 父類指向子類,調用的test函數,test函數是父類的虛函數,類內的函數有一個默認的this指針,test內部調用的fun函數實際上是this->fun
(this類型是A*——父類的指針指向虛函數)
,fun是子類重寫的虛函數(函數是子類重寫的虛函數)
——滿足多態條件- 虛函數中的
this是根據是否重寫確定的
,這里的test沒有被重寫
,是A*this
指針,然后調用fun,fun是經過重寫的函數
,所以調用的是重寫的函數- 虛函數繼承的是函數的接口,重寫的是函數的實現
所以缺省值才是1
#include<iostream>
using namespace std;class A
{
public:virtual void fun(int val = 0){printf("A::fun()--> %d", val);}virtual void test(){fun();}
};
class B:public A
{
public:void fun(int val = 1){printf("B::fun()--> %d", val);}
};
int main()
{B b;b.test();return 0;
}
🔒經典問題
- 什么是多態?
- 什么是重載、重寫(覆蓋)、重定義(隱藏)?
- 多態的實現原理?
- inline函數可以是虛函數嗎?
可以,不構成多態就是inline,構成多態就不是inline
- 靜態成員可以是虛函數嗎?
不能,因為靜態成員函數沒有this指針,使用類型::成員函數
的調用方式無法訪問虛函數表,所以靜態成員函數無法放進虛函數表。
- 構造函數可以是虛函數嗎?
不能,虛表在編譯時生成
在調用構造函數之后,但是虛表指針在成員初始化之前
- 析構函數可以是虛函數嗎?
本就應該是,在
A* a = new B;
這種場景下,在釋放子類對象時,需要將析構函數變成虛函數
- 對象訪問普通函數快還是虛函數更快?
首先如果是普通對象,是一樣快的。如果構成多態,就是普通函數快,因為運行時調用虛函數需要到虛函數表中去查找。
- 虛函數表是在什么階段生成的,存在哪的?
虛函數表是在編譯階段就生成的,一般情況下存在代碼段(常量區)的。
- C++菱形繼承的問題?虛繼承的原理?
菱形繼承會造成
祖宗類數據冗余的問題
在每一個繼承自祖宗類的派生類中,使用一個指針
,指向一個偏移量
,根據偏移量找到的地址就是祖宗類的數據,并且這個數據只有一份
- 什么是抽象類?抽象類的作用?
抽象類含有形如
virtual void fun() =0;
的基類/派生類
強制派生類重寫父類的實現
從
原理
的角度理解,重寫之后將fun虛函數進行覆蓋
,test是A*this調用
經過重寫的虛函數fun
,符合多態的條件
,并且繼承的是接口不是實現
,fun虛函數繼承父類函數接口
,并使用重寫的虛函數實現
,最終形成了這個樣子
優秀多態文章
優秀多態文章
為什么要使用父類指針和引用實現多態,而不能使用對象?
虛析構函數
虛表位置
虛表位置