日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > c/c++ >内容正文

c/c++

C++ 面向对象(二)多态 : 虚函数、多态原理、抽象类、虚函数表、继承与虚函数表

發布時間:2024/4/11 c/c++ 23 豆豆
生活随笔 收集整理的這篇文章主要介紹了 C++ 面向对象(二)多态 : 虚函数、多态原理、抽象类、虚函数表、继承与虚函数表 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

目錄

    • 多態
      • 多態的概念
      • 多態的構成條件
    • 虛函數
      • 虛函數的重寫
        • 協變(返回值不同)
        • 析構函數的重寫(函數名不同)
      • final和override
        • final
        • override
    • 重載, 重寫, 重定義對比
    • 抽象類
    • 多態的原理
      • 虛函數表
      • 虛函數表的存儲位置
      • 動態綁定和靜態綁定
    • 繼承與虛函數表
      • 單繼承與虛函數表
      • 多繼承與虛函數表


多態

多態的概念

什么是多態呢?就是一種事物,多種形態。就是對于同一個行為,不同的對象去完成就會產生不同的結果。

舉個生活中的例子,當你去旅游景點游玩時,不同的身份買票的價格也不一樣。比如對于普通人是原價購買,而對于學生和孩子則是半價購買,對于軍人則是優先購買。明明同樣是購買,不同身份帶來的不同結果,就是多態的作用。

在C++中,多態就是對于同一個函數,當調用的對象不同,他的操作也不同。就是指針和引用指向指向哪一個對象,就調用哪一個對象的虛函數

例如:

class Human { public:virtual void print(){cout << "i am a human" << endl;} };class Student : public Human { public:virtual void print(){cout << "i am a student" << endl;} };class Teacher : public Human { public:virtual void print(){cout << "i am a teacher" << endl;} };void ShowIdentity(Human& human) {human.print(); }int main() {Human h;Teacher t;Student s;ShowIdentity(h);ShowIdentity(t);ShowIdentity(s); }


多態的構成條件

這里先給出條件,底下的原理解析那一塊會具體講原因

多態是繼承體系中的一個行為,如果要在繼承體系中構成多態,需要滿足兩個條件

1. 必須通過基類的指針或者引用調用虛函數

2. 被調用的函數必須是虛函數,并且派生類必須要對繼承的基類的虛函數進行重寫


虛函數

虛函數就是被virtual修飾的類成員函數(這里的virtual和虛繼承的virtual雖然是同一個關鍵字,但是作用不一樣)

如:

class Human { public:virtual void print(){cout << "i am a human" << endl;} };

虛函數的重寫

當派生類中有一個和基類完全相同的虛函數(函數名,返回值,參數完全相同),則說明子類的虛函數重寫了基類的虛函數(只重寫了函數實現)

如:

class Human { public:virtual void print(){cout << "i am a human" << endl;} };class Student : public Human { public:virtual void print(){cout << "i am a student" << endl;} };void ShowIdentity(Human &human) {human.print(); }int main() {Human h;Student s;ShowIdentity(h); ShowIdentity(s); }


如果不滿足上面的條件,例如參數不同則會變成重定義。

注意:

#include <iostream>class Base{ public:virtual void Show(int n = 10)const{ //提供缺省參數值std::cout << "Base:" << n << std::endl;} };class Base1 : public Base{ public:virtual void Show(int n = 20)const{ //重新定義繼承而來的缺省參數值std::cout << "Base1:" << n << std::endl;} };int main(){Base* p1 = new Base1; p1->Show(); return 0; }

此時輸出的是Base1:10, 這是出自Effective C++中的一個問題
如果子類重寫了缺省值,此時的子類的缺省值是無效的,使用的還是父類的缺省值
原因是因為多態是動態綁定,而缺省值是靜態綁定。對于P1,他的靜態類型也就是這個指針的類型是Base,所以這里的缺省值是Base的缺省值,而動態類型也就是指向的對象是Base1,所以這里調用的虛函數則是Base1中的虛函數,所以這里就是Base1中的虛函數,Base中的缺省值,也就是Base1:10。

或者可以更簡單的一句話描述,虛函數的重寫只重寫函數實現,不重寫缺省值

這道題最近考試做錯了,就拿出來講了一下

但是也存在兩種例外的情況。


協變(返回值不同)

當基類和派生類的返回值類型不同時,如果基類對象返回基類對象的引用或者指針,派生類對象也返回的是派生類對象的引用或者指針時,就會引起協變。協變也能完成虛函數的重寫

例如:

class Human { public:virtual Human& print(){cout << "i am a human" << endl;return *this;} };class Student : public Human { public:virtual Student& print(){cout << "i am a student" << endl;return *this;} };

如果返回值不是引用或者指針則不會構成協變

class Student : public Human { public:virtual Student print(){cout << "i am a student" << endl;return *this;} };


析構函數的重寫(函數名不同)

析構函數雖然函數名不同,但是也能構成重寫,因為編譯器為了讓析構函數實現多態,會將它的名字處理成destructor,這樣就能也能構成重寫。

為什么編譯器要通過這種方式讓析構函數也能構成重寫呢?
假設存在這種情況,我用一個基類指針或者引用指向派生類對象,如果不構成多態會怎樣

class Human { public:~Human(){cout << "~Human()" << endl;} };class Student : public Human { public:~Student(){cout << "~Student()" << endl;} };int main() {Human* h = new Student;delete h;return 0; }


可以看到,如果不構成多態,那么指針是什么類型的,就會調用什么類型的析構函數,這也就導致了一種情況,如果派生類的析構函數中有資源釋放,而這里卻沒有釋放掉那些資源,就會導致內存泄漏的問題。

所以為了防止這種情況,必須要將析構函數定義為虛函數。這也就是編譯器將析構函數重命名為destructor的原因


final和override

final和override是C++11中提供給用戶用來檢測是否進行重寫的兩個關鍵字。

final

使用final修飾的虛函數不能被重寫。
如果某一個虛函數不想被派生類重寫,就可以用final來修飾這個虛函數

class Human { public:virtual void print() final{cout << "i am a human" << endl;}};class Student : public Human { public:virtual void print(){cout << "i am a student" << endl;} };

override

override關鍵字是用來檢測派生類虛函數是否構成重寫的關鍵字。
在我們寫代碼的時候難免會出現些小錯誤,如基類虛函數沒有virtual或者派生類虛函數名拼錯等問題,這些問題不會被編譯器檢查出來,發生錯誤時也很難一下子鎖定,所以C++增添了override這一層保險,當修飾的虛函數不構成重寫時就會編譯錯誤。

class Human { public:void print(){cout << "i am a human" << endl;}};class Student : public Human { public:virtual void print() override{cout << "i am a student" << endl;} };


重載, 重寫, 重定義對比

重載:
1.在同一作用域
2.函數名相同,參數的類型、順序、數量不同。

重寫(覆蓋):
1.作用域不同,一個在基類一個在派生類
2.函數名,參數,返回值必須相同(協變和析構函數除外)
3.基類和派生類必須都是虛函數(派生類可以不加virtual,基類的虛函數屬性可以繼承,但是最好要加上virtual)

重定義(隱藏):
1.作用域不同,一個在基類一個在派生類
2.函數名相同
3.派生類和基類同名函數如果不構成重寫那就是重定義


抽象類

如果在虛函數的后面加上 =0,并且不進行實現,這樣的虛函數就叫做純虛函數。而包含純虛函數的類,也叫做抽象類或者接口類。抽象類不能實例化出對象,因為他所具有的信息不足以描述一個對象,派生類繼承后也只有在重寫純虛函數后才能實例化出對象。

抽象類就像是一個藍圖,為派生類描述好一個大概的架構,派生類必須實現完這些架構,至于要在這些架構上面做些什么,增加什么,就屬于派生類自己的問題。

例如:

class Human { public:virtual void print() = 0; };class Student : public Human { public:virtual void print(){cout << "i am a student" << endl;} };class Teacher : public Human { public:virtual void print(){cout << "i am a teacher" << endl;} };

普通函數的繼承是一種實現繼承,派生類繼承了基類函數,可以使用函數,繼承的是函數的實現。虛函數的
繼承是一種接口繼承,派生類繼承的是基類虛函數的接口,目的是為了重寫,達成多態,繼承的是接口。所以如果不實現多態,不要把函數定義成虛函數。


多態的原理

虛函數表

這里還是用這兩個類舉例子

class Human { public:virtual void print(){cout << "i am a human" << endl;}virtual void test1(){cout << "1test1" << endl;}void test2(){cout << "1test1" << endl;}int _age; };class Student : public Human { public:virtual void print() {cout << "i am a student" << endl;}void test2(){cout << "2test2" << endl;}int _stuNum; };

還是和上次一樣,首先看看h的大小,按照正常情況,因為h中只有一個成員變量_age,大小應該是四個字節。


但是這里卻是8個。

打開監視窗口觀察

可以看到里面除了_age以外,還有個指針_vfptr,這個指針指向了一個函數指針數組,這個函數指針數組也就是虛函數表,其中的每一個成員指向的都是之前我們實現的虛函數,這個_vfptr也被稱為虛函數表指針
而不是虛函數的test2則沒有被放入表中。

多態的實現也正是借助了這個虛函數表。
首先觀察這個虛函數表,我們可以看到,如果派生類實現了某個虛函數的重寫,那么在派生類的虛函數表中,重寫的虛函數就會覆蓋掉原有的函數,如Student::print。而沒有完成重寫的test1則依舊保留著從基類繼承下來的虛函數Human::test1。

為了進一步驗證基類和派生類虛函數表的關系,我將派生類所有的虛函數重寫去掉
結合上面的內容可以發現,派生類會繼承基類的虛函數表,如果派生類完成了重寫,則會將重寫的虛函數覆蓋掉原有的函數。所以指針或引用指向哪一個對象,就調用對象中虛函數表中對應位置的虛函數,來實現多態。
這也就是為什么需要派生類函數也為虛函數,并且必須要重寫才能實現的原因

繼續分析構成多態的另一個條件,為什么必須要指針或者引用才能構成多態。

int main() {Student s;Human h1 = s;Human* h2 = &s; }


這里可以看到,如果將派生類對象賦值給基類對象,會因為對象切割,導致他的內存布局整個被修改,完全轉換為基類對象的類型,虛函數表也與基類相同,所以不能實現多態。

而如果用基類指針或者引用指向派生類對象,雖然指向的是派生類對象,但是他們的內存布局是兼容的,他不會像賦值一樣改變派生類對象的內存結構,所以派生類對象的虛函數表得到了保留,所以他可以通過訪問派生類對象的虛函數表來實現多態。

總結一下派生類虛函數表的生成過程:
1.首先派生類會將基類的虛函數表拷貝過來
2.如果派生類完成了對虛函數的重寫,則用重寫后的虛函數覆蓋掉虛函數表中繼承下來的基類虛函數
3.如果派生類自己又新增了虛函數,則添加在虛函數表的最后面

常見問題解析:
內聯函數可以是虛函數嗎?
不可以,內聯函數沒有地址,無法放進虛函數表中。
靜態成員函數可以是虛函數嗎?
不可以,靜態成員函數沒有this指針,無法訪問虛函數表。
構造函數可以是虛函數嗎?
不可以,虛函數表指針也是對象的成員之一,是在構造函數初始化列表初始化時才生成的,不可能是虛函數
析構函數可以是虛函數嗎?
可以,上面有寫,最好把基類析構函數聲明為虛函數,防止使用基類指針或者引用指向派生類對象時,派生類的析構函數沒有調用,可能導致內存泄漏。

對象訪問虛函數快還是普通函數快?
如果不構成多態的話,虛函數和普通函數的訪問是一樣快的,但是如果構成多態,調用虛函數就得到虛函數表中查找,就會導致速度變慢,所以普通函數更快一些。


虛函數表的存儲位置

從上面的觀察可以看出來,虛函數存于虛函數表中,那么虛函數又存儲在哪里呢?
這里就來驗證一下

int main() {Student s1;int a = 0;int* p1 = &a;char* p2= "helloworld";int* p3 = new int;printf("棧變量:%p\n", p1);printf("代碼段常量:%p\n", p2);printf("堆變量:%p\n", p3);printf("普通函數地址:%p\n", ShowIdentity);printf("虛函數地址:%p\n", &Student::print);printf("虛函數表地址:%p\n", *(int*)&s1); }


通過對比可以看到,虛函數表與常量,函數一樣存儲于代碼段中。

所以得出結論,虛函數表在編譯階段生成,存儲于代碼段。


動態綁定和靜態綁定

對象的靜態類型:對象在聲明時采用的類型。是在編譯期確定的。(比如下面的h1,Human也就是他原本的類型就是靜態類型,而他指向的對象的類型Student也就是動態類型)
對象的動態類型:目前所指對象的類型。是在運行期決定的。對象的動態類型可以更改,但是靜態類型無法更改
靜態綁定:綁定的是對象的靜態類型,某特性(比如函數)依賴于對象的靜態類型,發生在編譯期。
動態綁定:綁定的是對象的動態類型,某特性(比如函數)依賴于對象的動態類型,發生在運行期。

接著我們通過匯編代碼,來觀察多態是在哪個階段實現的, 就可以知道它是靜態還是動態。

int main() {Student s1;Human& h1 = s1;Human h2 = s1;h1.print();h2.print();return 0; }


可以看到h1的print是滿足多態的,這里調用的函數是在
這一階段中找到eax中存儲的虛函數指針,所以可以發現,滿足多態的調用是在運行的時候,到對象中的找到虛函數指針來完成的調用


而下面h2的print則不滿足多態,所以是直接在編譯時從符號表中找到函數的地址后調用。

所以可以得出的結論是,滿足多態的函數調用時在運行的時候調用的,也就是動態多態。而之前重載那一章節也曾經說過重載也是一種多態的表現,只不過重載是在編譯的時候完成的調用,所以也被靜態多態

  • 靜態綁定又稱為前期綁定(早綁定),在程序編譯期間確定了程序的行為,也稱為靜態多態,比如:函數重載
  • 動態綁定又稱后期綁定(晚綁定),是在程序運行期間,根據具體拿到的類型確定程序的具體行為,調用具體的函數,也稱為動態多態。

  • 繼承與虛函數表

    單繼承與虛函數表

    class Human { public:virtual void print(){cout << "Human::print" << endl;}int _age; };class Student : public Human { public:virtual void print(){cout << "Student::print" << endl;}virtual void test1(){cout << "Student::test1" << endl;}int _stuNum; };


    對于單繼承的虛函數表,他會直接繼承基類的虛函數表,如果完成了重寫,則會覆蓋掉原來的虛函數,如果有新的虛函數test1(),則會加在基類虛函數表的尾部。但是由于編譯器的問題所以這里并不會顯示出來。

    所以可以通過代碼直接從內存中查看

    typedef void(*vfPtr) ();void Print(vfPtr vfTable[]) {cout << " 虛表地址>" << vfTable << endl;for (int i = 0; vfTable[i] != nullptr; ++i){printf(" 第%d個虛函數地址 :0X%x,->", i, vfTable[i]);vfPtr f = vfTable[i];f();}cout << endl; }int main() {Human b;Student d;vfPtr* vfTable = (vfPtr*)(*(int*)& b);Print(vfTable);vfPtr* vfTable = (vfPtr*)(*(int*)& d);Print(vfTable);return 0; }


    可以看到,如果派生類有新的虛函數,則會加在虛函數表的尾部。


    多繼承與虛函數表

    class Human { public:virtual void print(){cout << "i am a human" << endl;}virtual void test1(){cout << "1test1" << endl;}int _age; };class Student : public Human { public:virtual void print() {cout << "i am a student" << endl;}virtual void test1(){cout << "2test1" << endl;}int _stuNum; };class Test : public Human, public Student { public:virtual void print(){cout << "i am a Test" << endl;}virtual void test2(){cout << "2test2" << endl;} };

    對于多繼承來說,派生類會拷貝兩個基類的虛函數表

    同樣的,編譯器無法顯示,所以繼續用代碼從內存中讀取。

    typedef void(*vfPtr) ();void Print(vfPtr vfTable[]) {cout << " 虛表地址>" << vfTable << endl;for (int i = 0; vfTable[i] != nullptr; ++i){printf(" 第%d個虛函數地址 :0X%x,->", i, vfTable[i]);vfPtr f = vfTable[i];f();}cout << endl; }int main() {Test t;vfPtr* vfTableb = (vfPtr*)(*(int*)& t);Print(vfTableb);vfPtr* vfTabled = (vfPtr*)(*(int*)& t + sizeof(Human));Print(vfTabled);return 0; }


    同樣的,重寫的虛函數會覆蓋原有虛函數,而派生類未重寫的虛函數test2()則會放到第一個繼承基類部分的虛函數表中,也就是這里的Human的虛函數表中。


    對于多態和對象模型這一部分的問題,我還有很多地方理解的不夠好,可以參考一些陳皓大佬的這幾篇博客來進一步學習。
    C++ 虛函數表解析
    C++ 對象的內存布局(上)
    C++ 對象的內存布局(下)

    總結

    以上是生活随笔為你收集整理的C++ 面向对象(二)多态 : 虚函数、多态原理、抽象类、虚函数表、继承与虚函数表的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。