都說面向對象的三大特性是封裝、繼承、多態。C++作為一門面向對象程式語言,肯定也是具備了面向對象的三大特性,那麼在C++中是如何實現多態的呢?
在C++中是通過虛函數動態綁定的方式實現多態的。
虛函數與純虛函數
首先我們來回顧一下虛函數,在C++中是使用virtual關鍵字修飾的函數就是虛函數,下面是一個簡單的虛函數例子:
class Base{
public:
// 虛函數,必須實現,否則編譯報錯
virtual void f1() const{
std::cout << "我是Base的f1函數" << std::endl;
}
};
class A:public Base{
void f1() const override{
std::cout << "我是A的f1函數" << std::endl;
}
};
虛函數必須在基類中實現,如果不實現的話就會編譯報錯。
如果我們不想實現虛函數的話可以將其聲明為純虛函數,純虛函數的聲明方式如下:
virtual 返回值類型 函數名(函數參數) = 0;
聲明了純虛函數的類稱為抽象類,繼承抽象類的最終子類必須父類的純虛函數,否則不能生產對應的類對象。以下是一個純虛函數的例子:
class Base{
public:
// 虛函數,必須實現,否則編譯報錯
virtual void f1() const{
std::cout << "我是Base的f1函數" << std::endl;
}
// 純虛函數,不需要實現
virtual void f2() const = 0;
};
class A:public Base{
public:
void f1() const override{
std::cout << "我是A的f1函數" << std::endl;
}
void f2() const override{
std::cout << "我是A的純虛函數f2" << std::endl;
}
};
有了虛函數我們就能通過基類的的指針進行動態綁定,在運行時訪問到子類的函數,但是動態綁定只能發生在指針或引用上。例如在以下的例子中,函數test3是不會訪問到子類的函數的, 它訪問的函數依然是基類的虛函數,也就是說它沒有發生動態綁定,因為它既不是指針也不是引用。
class Base{
public:
// 虛函數,必須實現,否則編譯報錯
virtual void f1() const{
std::cout << "我是Base的f1函數" << std::endl;
}
};
class A:public Base{
public:
void f1() const override{
std::cout << "我是A的f1函數" << std::endl;
}
};
// 引用傳遞參數
void test1(const Base &base){
base.f1();
}
// 指針傳遞參數
void test2(Base *base){
base->f1();
}
// 非引用、非指針傳遞參數
void test3(Base base){
base.f1();
}
int main(int arg,char** argv) {
Base *base = new A();
test1(*base);
test2(base);
test3(*base);
return 0;
}
【騰訊文檔】FFmpegWebRTCRTMPRTSPHLSRTP播放器-音視頻流媒體高級開發-資料領取FFmpegWebRTCRTMPRTSPHLSRTP鎾斁鍣�-闊寵棰戞祦濯掍綋楂樼駭寮€鍙�-璧勬枡棰嗗彇
運行列印結果:
我是A的f1函數
我是A的f1函數
我是Base的f1函數
如果一個類可能會被繼承,這個類的析構函數應該被聲明為一個虛函數,否則會引發內存泄漏。例如以下例子:
class Base{
public:
// 虛函數,必須實現,否則編譯報錯
virtual void f1() const{
std::cout << "我是Base的f1函數" << std::endl;
}
~Base(){
std::cout << "Base的析構函數" << std::endl;
}
};
class A:public Base{
public:
void f1() const override{
std::cout << "我是A的f1函數" << std::endl;
}
~A(){
std::cout << "A的析構函數" << std::endl;
}
};
int main(int arg,char** argv) {
Base *base = new A();
delete base;
return 0;
}
運行輸出如下:
Base的析構函數
從運行結果可以看出A沒有被正確析構,這是因為它的基類Base的析構函數沒有被聲明為虛函數的原因,此時只要我們把Base類的析構函數聲明為虛函數即可修復這個內存泄漏的問題,也就是:
class Base{
public:
// 虛函數,必須實現,否則編譯報錯
virtual void f1() const{
std::cout << "我是Base的f1函數" << std::endl;
}
// 可能被繼承的類的析構函數應該是一個虛函數
virtual ~Base(){
std::cout << "Base的析構函數" << std::endl;
}
};
虛函數總結:
1、當我們在派生類中覆蓋了某個虛函數時,可以再一次使用virtual關鍵字指出該函數的性質。然而這麼做並非必須,因為一旦某個函數被聲明成虛函數,則在所有派生類中它都是虛函數。
2、虛函數只有在引用或者指針調用時才會發生動態綁定;
3、基類的析構函數需要聲明為虛函數;
4、虛函數必須要在基類實現,不實現,編譯會報錯;
5、如果子類沒有實現父類的純虛函數,則該子類不能被構造成一個對象。
多態實現原理-虛函數表
通過上面的例子我們知道了在C++中通過引用或指針的形式進行虛函數的動態綁定而實現多態,那麼動態綁定在C++中是如何實現呢?答案是虛函數表。
所謂的虛函數表就是:
當編譯器在編譯過程中遇到virtual關鍵字時,它不會對函數調用進行綁定,而是為包含虛函數的類建立一張虛函數表Vtable。在虛函數表中,編譯器按照虛函數的聲明順序依次保存虛函數地址。同時,編譯器會在類中添加一個隱藏的虛函數指針VPTR,指向虛函數表。在創建對象時,將虛函數指針VPTR放置在對象的起始位置,為其分配空間,並調用構造函數將其初始化為虛函數表地址。需要注意的是,虛函數表不占用對象空間。
虛函數表總結:
1、單繼承下的虛函數表
虛函數表中的指針順序,按照虛函數聲明的順序排序;基類的虛函數指針在派生類的前面。
2、多繼承下的虛函數表
多繼承關係下會有多個虛函數表,也會有多個指向不同虛函數表的指針;