欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

超級詳細講解C++中的多態(tài)

 更新時間:2022年05月31日 11:35:36   作者:programing菜鳥  
多態(tài)是在不同繼承關(guān)系的類對象,去調(diào)同一函數(shù),產(chǎn)生了不同的行為,下面這篇文章主要給大家介紹了關(guān)于C++中多態(tài)的相關(guān)資料,文中通過實例代碼介紹的非常詳細,需要的朋友可以參考下

多態(tài)概念引入

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

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

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

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

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

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

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

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

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

int main()
{
   Student st;
   Person p;
   BuyTicket(&st);//子類對象切片過去
   BuyTicket(&p);//父類對象傳地址
}

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

1

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

2

這說明了很重要的一點,如果滿足多態(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,因為子類繼承了父類的虛函數(shù),
   //編譯器會認為你是想要重寫虛函數(shù)。
   //void fun() 可以直接這樣,也對,但不推薦。           
   virtual void fun()//子類重寫父類虛函數(shù)
   {
     cout << "Student->fun()" << endl;
   }
};

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

協(xié)變:

子類的虛函數(shù)和父類的虛函數(shù)的返回值可以不同,也能構(gòu)成重載。但需要子類的返回值是一個子類的指針或者引用,父類的返回值是一個父類的指針或者引用,且返回值代表的兩個類也成繼承關(guān)系。這個叫做協(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ù),因為我們希望析構(gòu)函數(shù)的調(diào)用跟指針指向的對象有關(guān),而跟指針的類型無關(guān)。這不就是多態(tài)嗎?但是結(jié)果卻調(diào)用了A的析構(gòu)函數(shù)。

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

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

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

這也解釋了為什么子類不寫virtual可以構(gòu)成重寫,因為編譯器怕你忘記析構(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新增了兩個關(guān)鍵字。用final修飾的虛函數(shù)無法重寫。用final修飾的類無法被繼承。final像這個單詞的意思一樣,這就是最終的版本,不用再更新了。

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ù),編譯器會檢查這個虛函數(shù)是否重寫。如果沒有重寫,編譯器會報錯。

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

class B : public A 
{
  public:
  //這里我想重寫fun,但寫成了fun1,因為有override,編譯器會報錯。
    virtual void fun1() override
    {
     cout << endl;
    }
};

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

這里我們來理一理這三個概念。

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

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

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

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

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

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

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

2、抽象類

2.1 抽象類的概念

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

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

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

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

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

2,表示某些抽象類。

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

普通函數(shù)的繼承就是實現(xiàn)繼承,虛函數(shù)的繼承就是接口繼承。子類繼承了函數(shù)的實現(xiàn),可以直接使用。虛函數(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();//傳過來一個子類指針調(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)是怎樣實現(xiàn)的呢?

先來一道題目,

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

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

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

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

44

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

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

class A
{
  public:
  void fun1()
  {}
  virtual void fun2()
  {}
};

A* ap = nullptr;
ap->fun1(); //調(diào)用成功,因為這是普通函數(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)存模型。

子類跟父類一樣有一個虛表指針。

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

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

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

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

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

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

234

下面來一道面試題:

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

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

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

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

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

我們創(chuàng)建兩個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 << "全價票" << endl; //成人票全價
   }
};
class Student : public Person //學(xué)生
{
   public:
   virtual void fun() //子類完成對父類虛函數(shù)的重寫
   {
       cout << "半價票" << endl;//學(xué)生票半價
   }
};
void BuyTicket(Person* p)
{
   p->fun();
}

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

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

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

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

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

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

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

這里我附上一個有意思的問題:

就是在子類已經(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++提供的一個回避虛函數(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;
	}
};

我們想個辦法打印虛表,

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

結(jié)果如下:

456

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

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

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

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

5555

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

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

不是的。實際上兩個fun1是同一個fun1,里面放的是跳轉(zhuǎn)指令而已。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;
	}
};

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

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

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

Ret:

ret

總結(jié)

到此這篇關(guān)于C++多態(tài)的文章就介紹到這了,更多相關(guān)C++多態(tài)詳解內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • C++實現(xiàn)修改函數(shù)代碼HOOK的封裝方法

    C++實現(xiàn)修改函數(shù)代碼HOOK的封裝方法

    這篇文章主要介紹了C++實現(xiàn)修改函數(shù)代碼HOOK的封裝方法,有助于深入了解C++的HOOK原理,需要的朋友可以參考下
    2014-10-10
  • opencv提取輪廓大于某個閾值的圖像

    opencv提取輪廓大于某個閾值的圖像

    這篇文章主要為大家詳細介紹了opencv提取輪廓大于某個閾值的圖像,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2020-03-03
  • C語言中typedef的用法以及#define區(qū)別詳解

    C語言中typedef的用法以及#define區(qū)別詳解

    這篇文章主要給大家介紹了關(guān)于C語言中typedef用法以及#define區(qū)別的相關(guān)資料,typedef 是用來定義一種類型的新別名的,它不同于宏(#define),不是簡單的字符串替換。而#define只是簡單的字符串替換(原地擴展),需要的朋友可以參考下
    2021-07-07
  • 如何在C++中實現(xiàn)按位存取

    如何在C++中實現(xiàn)按位存取

    實現(xiàn)緊湊存取,不是按一個字節(jié)一個字節(jié)地存取,而是按位存取,本文就是介紹了如何在C++中實現(xiàn)按位存取,需要的朋友可以參考下
    2015-07-07
  • C++實現(xiàn)LeetCode(33.在旋轉(zhuǎn)有序數(shù)組中搜索)

    C++實現(xiàn)LeetCode(33.在旋轉(zhuǎn)有序數(shù)組中搜索)

    這篇文章主要介紹了C++實現(xiàn)LeetCode(33.在旋轉(zhuǎn)有序數(shù)組中搜索),本篇文章通過簡要的案例,講解了該項技術(shù)的了解與使用,以下就是詳細內(nèi)容,需要的朋友可以參考下
    2021-07-07
  • C語言實現(xiàn)多項式的相加

    C語言實現(xiàn)多項式的相加

    這篇文章主要為大家介紹了C語言實現(xiàn)多項式的相加,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2019-10-10
  • C++數(shù)據(jù)結(jié)構(gòu)二叉搜索樹的實現(xiàn)應(yīng)用與分析

    C++數(shù)據(jù)結(jié)構(gòu)二叉搜索樹的實現(xiàn)應(yīng)用與分析

    從這篇博客開始,我就要和大家介紹有關(guān)二叉搜索樹的知識,它還衍生出了兩棵樹——AVL樹和紅黑樹,在后面兩篇博客我都會介紹。今天先從二叉搜索樹開始引入
    2022-02-02
  • C++使用sort對容器排序的實現(xiàn)

    C++使用sort對容器排序的實現(xiàn)

    C++ STL 標準庫中的sort()函數(shù)專門用來對容器或普通數(shù)組中指定范圍內(nèi)的元素進行排序,本文就詳細的介紹一下怎么實現(xiàn),需要的朋友們下面隨著小編來一起學(xué)習學(xué)習吧
    2021-05-05
  • C/C++利用原生套接字抓取FTP數(shù)據(jù)包

    C/C++利用原生套接字抓取FTP數(shù)據(jù)包

    這篇文章主要為大家詳細介紹了如何基于原始套接字的網(wǎng)絡(luò)數(shù)據(jù)包捕獲與分析工具,通過實時監(jiān)控網(wǎng)絡(luò)流量,實現(xiàn)抓取流量包內(nèi)的FTP通信數(shù)據(jù),需要的小伙伴可以參考下
    2023-12-12
  • C/C++位段超詳細整理大全

    C/C++位段超詳細整理大全

    以位為單位來定義結(jié)構(gòu)體中的成員變量所占的空間內(nèi)存,含有位段的結(jié)構(gòu)體稱為位段結(jié)構(gòu),這篇文章主要給大家介紹了關(guān)于C/C++位段的相關(guān)資料,需要的朋友可以參考下
    2024-01-01

最新評論