溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

C++中的多態(tài)如何實(shí)現(xiàn)

發(fā)布時(shí)間:2022-05-31 13:49:24 來源:億速云 閱讀:125 作者:iii 欄目:開發(fā)技術(shù)

這篇文章主要介紹“C++中的多態(tài)如何實(shí)現(xiàn)”,在日常操作中,相信很多人在C++中的多態(tài)如何實(shí)現(xiàn)問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”C++中的多態(tài)如何實(shí)現(xiàn)”的疑惑有所幫助!接下來,請跟著小編一起來學(xué)習(xí)吧!

    多態(tài)概念引入

    多態(tài)字面意思就是多種形態(tài)。

    我們先來想一想在日常生活中的多態(tài)例子:買票時(shí),成人買票全價(jià),如果是學(xué)生那么半價(jià),如果是軍人,就可以優(yōu)先買票。不同的人買票會有不同的實(shí)現(xiàn)方法,這就是多態(tài)。

    1、C++中多態(tài)的實(shí)現(xiàn)

    1.1 多態(tài)的構(gòu)成條件

    C++的多態(tài)必須滿足兩個(gè)條件:

    1 必須通過基類的指針或者引用調(diào)用虛函數(shù)

    2 被調(diào)用的函數(shù)是虛函數(shù),且必須完成對基類虛函數(shù)的重寫

    我們來看看具體實(shí)現(xiàn)。

    class Person //成人
    {
      public:
      virtual void fun()
       {
           cout << "全價(jià)票" << endl; //成人票全價(jià)
       }
    };
    class Student : public Person //學(xué)生
    {
       public:
       virtual void fun() //子類完成對父類虛函數(shù)的重寫
       {
           cout << "半價(jià)票" << endl;//學(xué)生票半價(jià)
       }
    };
    void BuyTicket(Person* p)
    {
       p->fun();
    }
    
    int main()
    {
       Student st;
       Person p;
       BuyTicket(&st);//子類對象切片過去
       BuyTicket(&p);//父類對象傳地址
    }

    調(diào)用的兩個(gè)BuyTicket() 答案是什么呢?

    C++中的多態(tài)如何實(shí)現(xiàn)

    如果不滿足多態(tài)呢?

    C++中的多態(tài)如何實(shí)現(xiàn)

    這說明了很重要的一點(diǎn),如果滿足多態(tài),編譯器會調(diào)用指針指向?qū)ο蟮奶摵瘮?shù),而與指針的類型無關(guān)。如果不滿足多態(tài),編譯器會直接根據(jù)指針的類型去調(diào)用虛函數(shù)。

    1.2 虛函數(shù)

    用virtual修飾的關(guān)鍵字就是虛函數(shù)。

    虛函數(shù)只能是類中非靜態(tài)的成員函數(shù)。

    virtual void fun() //error! 在類外面的函數(shù)不能是虛函數(shù)
    {}

    1.3虛函數(shù)的重寫

    子類和父類中的虛函數(shù)擁有相同的名字,返回值,參數(shù)列表,那么稱子類中的虛函數(shù)重寫了父類的虛函數(shù),或者叫做覆蓋。

    class Person
    {
      public:
       virtual void fun()
       {
          cout << "Person->fun()" << endl;
       }
    };
    class Student
    {
       public:
       //子類重寫的虛函數(shù)可以不加virtual,因?yàn)樽宇惱^承了父類的虛函數(shù),
       //編譯器會認(rèn)為你是想要重寫虛函數(shù)。
       //void fun() 可以直接這樣,也對,但不推薦。           
       virtual void fun()//子類重寫父類虛函數(shù)
       {
         cout << "Student->fun()" << endl;
       }
    };

    虛函數(shù)重寫的兩個(gè)例外:

    協(xié)變:

    子類的虛函數(shù)和父類的虛函數(shù)的返回值可以不同,也能構(gòu)成重載。但需要子類的返回值是一個(gè)子類的指針或者引用,父類的返回值是一個(gè)父類的指針或者引用,且返回值代表的兩個(gè)類也成繼承關(guān)系。這個(gè)叫做協(xié)變。

    class Person
    {
      public:
       virtual Person* fun()//返回父類指針
       {
          cout << "Person->fun()" << endl;
          return nullptr;
       }
    };
    class Student
    {
       public:
                //返回子類指針,雖然返回值不同,也構(gòu)成重寫
       virtual Student* fun()//子類重寫父類虛函數(shù)
       {
         cout << "Student->fun()" << endl;
         return nullptr;
       }
    };

    也可以這樣,也是協(xié)變,

    class A
    {};
    class B : public A
    {};   //B繼承A
    class Person
    {
      public:
       virtual A* fun()//返回A類指針
       {
          return nullptr;
       }
    };
    class Student
    {
       public:
                //返回B類指針,雖然返回值不同,也構(gòu)成重寫
       virtual B* fun()//子類重寫父類虛函數(shù)
       {
         return nullptr;
       }
    };

    2.析構(gòu)函數(shù)的重寫

    析構(gòu)函數(shù)是否需要重寫呢?

    讓我們來考慮這樣一種情況,

    //B繼承了A,他們的析構(gòu)函數(shù)沒有重寫。
    class A
    {
      public:
      ~A()
      {
         cout << "~A()" << endl;
      }
    };
    class B : public A
    {
      public:
      ~B()
      {
        cout << "~B()" << endl;
      }
    };
    
     A* a = new B; //把B的對象切片給A類型的指針。
     delete a; //調(diào)用的是誰的析構(gòu)函數(shù)呢?你希望調(diào)用誰的呢?

    顯然我們希望調(diào)用B的析構(gòu)函數(shù),因?yàn)槲覀兿M鰳?gòu)函數(shù)的調(diào)用跟指針指向的對象有關(guān),而跟指針的類型無關(guān)。這不就是多態(tài)嗎?但是結(jié)果卻調(diào)用了A的析構(gòu)函數(shù)。

    所以析構(gòu)函數(shù)要實(shí)現(xiàn)多態(tài)。But,析構(gòu)函數(shù)名字天生不一樣,怎么實(shí)現(xiàn)多態(tài)?

    實(shí)際上,析構(gòu)函數(shù)被編譯器全部換成了Destructor,所以我們加上virtual就可以。

    只要父類的析構(gòu)函數(shù)用virtual修飾,無論子類是否有virtual,都構(gòu)成析構(gòu)。

    這也解釋了為什么子類不寫virtual可以構(gòu)成重寫,因?yàn)榫幾g器怕你忘記析構(gòu)。

    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新增了兩個(gè)關(guān)鍵字。用final修飾的虛函數(shù)無法重寫。用final修飾的類無法被繼承。final像這個(gè)單詞的意思一樣,這就是最終的版本,不用再更新了。

    class A final //A類無法被繼承
    {
    public:
      virtual void fun() final //fun函數(shù)無法被重寫
      {}
    };
    
    class B : public A //error
    {
      public:
        virtual void fun() //error
        {
         cout << endl;
        }
    };

    被override修飾的虛函數(shù),編譯器會檢查這個(gè)虛函數(shù)是否重寫。如果沒有重寫,編譯器會報(bào)錯(cuò)。

    class A  
    {
    public:
      virtual void fun() 
      {}
    };
    
    class B : public A 
    {
      public:
      //這里我想重寫fun,但寫成了fun1,因?yàn)橛衞verride,編譯器會報(bào)錯(cuò)。
        virtual void fun1() override
        {
         cout << endl;
        }
    };

    1.5 重載,覆蓋(重寫),重定義(隱藏)

    這里我們來理一理這三個(gè)概念。

    1.重載:重載函數(shù)處在同一作用域。

    函數(shù)名相同,函數(shù)列表必須不同。

    2.覆蓋:必須是虛函數(shù),且處在父類和子類中。

    返回值,參數(shù)列表,函數(shù)名必須完全相同(協(xié)變除外)。

    3.重定義:子類和父類的成員變量相同或者函數(shù)名相同,

    子類隱藏父類的對應(yīng)成員。

    子類和父類的同名函數(shù)不是重定義就是重寫。

    2、抽象類

    2.1 抽象類的概念

    再虛函數(shù)的后面加上=0就是純虛函數(shù),有純虛函數(shù)的類就是抽象類,也叫做接口類。抽象類無法實(shí)例化出對象。抽象類的子類也無法實(shí)例化出對象,除非重寫父類的虛函數(shù)。

    class Car
    {
     public:
        virtual void fun() = 0; //不用實(shí)現(xiàn),只寫接口就行。
    }

    這并不意味著純虛函數(shù)不能寫實(shí)現(xiàn),只是我們大部分情況下不寫。

    那么虛函數(shù)有什么用呢?

    1,強(qiáng)制子類重寫虛函數(shù),完成多態(tài)。

    2,表示某些抽象類。

    2.2 接口繼承和實(shí)現(xiàn)繼承

    普通函數(shù)的繼承就是實(shí)現(xiàn)繼承,虛函數(shù)的繼承就是接口繼承。子類繼承了函數(shù)的實(shí)現(xiàn),可以直接使用。虛函數(shù)重寫后只會繼承接口,重寫實(shí)現(xiàn)。所以如果不用多態(tài),不要把函數(shù)寫成虛函數(shù)。

    純虛函數(shù)就體現(xiàn)了接口繼承。下面我們來一道題,展現(xiàn)一下接口繼承。

    class A
    {
       public:
       virtual void fun(int val = 0)//父類虛函數(shù)
       {
         cout <<"A->val = "<< val << endl;
       }
       void Fun()
       {
          fun();//傳過來一個(gè)子類指針調(diào)用fun()
       }
    };
    class B: public A
    {
       public:
        virtual void fun(int val = 1)//子類虛函數(shù)
        {
           cout << "B->val = " << val << endl;
        }
    };
    
    B b;
    A* a = &b;
    a->Fun();

    結(jié)果是什么呢?

    B->val = 0

    子類對象切片給父類指針,傳給Fun函數(shù),滿足多態(tài),會去調(diào)用子類的fun函數(shù),但是子類的虛函數(shù)繼承了父類的接口,所以val是父類的0.

    3、 多態(tài)的原理

    3.1 虛函數(shù)表

    多態(tài)是怎樣實(shí)現(xiàn)的呢?

    先來一道題目,

    class A
    {
      public:
       virtual void fun()
       {}
       protected:
       int _a;
    };

    sizeof(A)是多少?是4嗎?NO,NO,NO!

    答案是8個(gè)字節(jié)。

    我們定義一個(gè)A類型的對象a,打開調(diào)試窗口,發(fā)現(xiàn)a的內(nèi)容如下

    C++中的多態(tài)如何實(shí)現(xiàn)

    我們發(fā)現(xiàn)除了成員變量_a以外,還多了一個(gè)指針。這個(gè)指針是不準(zhǔn)確的,實(shí)際上應(yīng)該是_vftptr(virtual function table pointer),即虛函數(shù)表指針,簡稱虛表指針。在計(jì)算類大小的時(shí)候要加上這個(gè)指針的大小。那么虛表是什么呢?虛表就是存放虛函數(shù)的地址地方。每當(dāng)我們?nèi)フ{(diào)用虛函數(shù),編譯器就會通過虛表指針去虛表里面查找。

    下面我們用一個(gè)小栗子來說明虛函數(shù)的使用會用指針。

    class A
    {
      public:
      void fun1()
      {}
      virtual void fun2()
      {}
    };
    
    A* ap = nullptr;
    ap->fun1(); //調(diào)用成功,因?yàn)檫@是普通函數(shù)的調(diào)用
    ap->fun2(); //調(diào)用失敗,虛函數(shù)需要對指針操作,無法操作空指針。

    我們先來看看繼承的虛函數(shù)表。

    class A
    {
      public:
       virtual void fun1()
       {}
       virtual void fun2()
       {}
    };
    class B : public A
    {
     public:
       virtual void fun1()//重寫父類虛函數(shù)
       {}
       virtual void fun3()
       {}
    };
    A a;
    B b; //我們通過調(diào)試看看對象a和b的內(nèi)存模型。

    C++中的多態(tài)如何實(shí)現(xiàn)

    子類跟父類一樣有一個(gè)虛表指針。

    子類的虛函數(shù)表一部分繼承自父類。如果重寫了虛函數(shù),那么子類的虛函數(shù)會在虛表上覆蓋父類的虛函數(shù)。

    本質(zhì)上虛函數(shù)表是一個(gè)虛函數(shù)指針數(shù)組,最后一個(gè)元素是nullptr,代表虛表的結(jié)束。

    所以,如果繼承了虛函數(shù),那么

    1 子類先拷貝一份父類虛表,然后用一個(gè)虛表指針指向這個(gè)虛表。

    2 如果有虛函數(shù)重寫,那么在子類的虛表上用子類的虛函數(shù)覆蓋。

    3 子類新增的虛函數(shù)按其在子類中的聲明次序增加到子類虛表的最后。

    C++中的多態(tài)如何實(shí)現(xiàn)

    下面來一道面試題:

    虛函數(shù)存在哪里?

    虛函數(shù)表存在哪里?

    虛函數(shù)是帶有virtual的函數(shù),虛函數(shù)表是存放虛函數(shù)地址的指針數(shù)組,虛函數(shù)表指針指向這個(gè)數(shù)組。對象中存的是虛函數(shù)指針,不是虛函數(shù)表。

    虛函數(shù)和普通函數(shù)一樣存在代碼段。

    那么虛函數(shù)表存在哪里呢?

    我們創(chuàng)建兩個(gè)A對象,發(fā)現(xiàn)他們的虛函數(shù)指針相同,這說明他們的虛函數(shù)表屬于類,不屬于對象。所以虛函數(shù)表應(yīng)該存在共有區(qū)。

    堆?堆需要動態(tài)開辟,動態(tài)銷毀,不合適。

    靜態(tài)區(qū)?靜態(tài)區(qū)存放全局變量和靜態(tài)變量不合適。

    所以綜合考慮,把虛函數(shù)表也存放在了代碼段。

    3.2多態(tài)的原理

    我們現(xiàn)在來看看多態(tài)的原理。

    class Person //成人
    {
      public:
      virtual void fun()
       {
           cout << "全價(jià)票" << endl; //成人票全價(jià)
       }
    };
    class Student : public Person //學(xué)生
    {
       public:
       virtual void fun() //子類完成對父類虛函數(shù)的重寫
       {
           cout << "半價(jià)票" << endl;//學(xué)生票半價(jià)
       }
    };
    void BuyTicket(Person* p)
    {
       p->fun();
    }

    C++中的多態(tài)如何實(shí)現(xiàn)

    這樣就實(shí)現(xiàn)了不同對象去調(diào)用同一函數(shù),展現(xiàn)出不同的形態(tài)。

    滿足多態(tài)的函數(shù)調(diào)用是程序運(yùn)行是去對象的虛表查找的,而虛表是在編譯時(shí)確定的。

    普通函數(shù)的調(diào)用是編譯時(shí)就確定的。

    3.3動態(tài)綁定與靜態(tài)綁定

    1.靜態(tài)綁定又稱為前期綁定(早綁定),在程序編譯期間確定了程序的行為,也稱為靜態(tài)多態(tài),比如:函數(shù)重載

    2.動態(tài)綁定又稱后期綁定(晚綁定),是在程序運(yùn)行期間,根據(jù)具體拿到的類型確定程序的具體行為,調(diào)用具體的函數(shù),也稱為動態(tài)多態(tài)。

    我們說的多態(tài)一般是指動態(tài)多態(tài)。

    這里我附上一個(gè)有意思的問題:

    就是在子類已經(jīng)覆蓋了父類的虛函數(shù)的情況下,為什么子類還是可以調(diào)用“被覆蓋”的父類的虛函數(shù)呢?

    #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++提供的一個(gè)回避虛函數(shù)的機(jī)制

    通過加作用域(正如你所嘗試的),使得函數(shù)在編譯時(shí)就綁定。

    (這題來自:虛函數(shù))

    4 、繼承中的虛函數(shù)表

    4.1 單繼承中的虛函數(shù)表

    這里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;
    	}
    };

    我們想個(gè)辦法打印虛表,

    typedef void(*V_PTR)(); //typedef一下函數(shù)指針,相當(dāng)于把返回值為void型的
    //函數(shù)指針定義成 V_PTR.
    void PrintPFTable(V_PTR* table)//打印虛函數(shù)表
    {  //因?yàn)樘摫碜詈笠粋€(gè)為nllptr,我們可以利用這個(gè)打印虛表。
    	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對象的前四個(gè)字節(jié),就是虛表的指針,
    	      //前面我們說了虛函數(shù)表本質(zhì)是一個(gè)存虛函數(shù)指針的指針數(shù)組,
    	      //這個(gè)數(shù)組最后面放了一個(gè)nullptr
         // 1.先取b的地址,強(qiáng)轉(zhuǎn)成一個(gè)int*的指針
         // 2.再解引用取值,就取到了b對象前4個(gè)字節(jié)的值,這個(gè)值就是指向虛表的指針
         // 3.再強(qiáng)轉(zhuǎn)成V_PTR*,這是我們打印虛表函數(shù)的類型。
         // 4.虛表指針傳給PrintPFTable函數(shù),打印虛表
         // 5,有時(shí)候編譯器資源釋放不完全,我們需要清理一下,不然會打印多余結(jié)果。
    	PrintPFTable((V_PTR*)(*(int*)&b));
    	PrintPFTable((V_PTR*)(*(int*)&d));

    結(jié)果如下:

    C++中的多態(tài)如何實(shí)現(xiàn)

    4.2 多繼承中的虛函數(shù)表

    我們先來看一看一道題目,

    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嗎?一個(gè)虛表指針,三個(gè)lnt,考慮內(nèi)存對齊后確實(shí)是16.但是結(jié)果是20.

    我們來看看內(nèi)存模型。在VS下,c竟然有兩個(gè)虛指針

    C++中的多態(tài)如何實(shí)現(xiàn)

    每個(gè)虛表里都有一個(gè)fun1函數(shù)。

    所以C的內(nèi)存模型應(yīng)該是這樣的,

    C++中的多態(tài)如何實(shí)現(xiàn)

    而且如果C自己有多余的虛函數(shù),會按照繼承順序補(bǔ)在第一張?zhí)摫砗竺妗?/p>

    下面還有一個(gè)問題,可以看到C::fun1在兩張?zhí)摫砩隙几采w了,但是它們的地址不一樣,是不是說在代碼段有兩段相同的C::fun1呢?

    不是的。實(shí)際上兩個(gè)fun1是同一個(gè)fun1,里面放的是跳轉(zhuǎn)指令而已。C++也會不犯這個(gè)小問題。

    最后,我們來打印一下多繼承的虛表。

    //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;
    	}
    };

    打印的細(xì)節(jié),從Base2繼承過來的虛表指針放在第一個(gè)虛表指針后面,我們想要拿到這個(gè)指針需要往后挪一個(gè)指針加上一個(gè)int的字節(jié),但是指針的大小跟操作系統(tǒng)的位數(shù)有關(guān),所以我們可以用加上Base2的大小個(gè)字節(jié)來偏移。

    這里注意要先強(qiáng)轉(zhuǎn)成char*,不然指針的加減會根據(jù)指針的類型來確定。

    Derive d;
    	PrintPFTable((V_PTR*)(*(int*)&d));
    	PrintPFTable((V_PTR*)(*(int*)((char*)&d+sizeof(Base2))));

    Ret:

    C++中的多態(tài)如何實(shí)現(xiàn)

    到此,關(guān)于“C++中的多態(tài)如何實(shí)現(xiàn)”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實(shí)踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識,請繼續(xù)關(guān)注億速云網(wǎng)站,小編會繼續(xù)努力為大家?guī)砀鄬?shí)用的文章!

    向AI問一下細(xì)節(jié)

    免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

    c++
    AI