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

歡迎訪問 生活随笔!

生活随笔

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

c/c++

【C++ Primer | 15】虚函数表剖析(一)

發布時間:2023/11/30 c/c++ 28 豆豆
生活随笔 收集整理的這篇文章主要介紹了 【C++ Primer | 15】虚函数表剖析(一) 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

一、虛函數

1. 概念

多態指當不同的對象收到相同的消息時,產生不同的動作

  • 編譯時多態(靜態綁定),函數重載,運算符重載,模板。
  • 運行時多態(動態綁定),虛函數機制。

為了實現C++的多態,C++使用了一種動態綁定的技術。這個技術的核心是虛函數表(下文簡稱虛表)。本文介紹虛函數表是如何實現動態綁定的。

C++多態實現的原理:

  • ?當類中聲明虛函數時,編譯器會在類中生成一個虛函數表
  • 虛函數表是一個存儲成員函數地址的數據結構
  • 虛函數表是由編譯器自動生成與維護的
  • ?virtual成員函數會被編譯器放入虛函數表中
  • 存在虛函數表時,每個對象中都有一個指向虛函數表的指針

?

2. 類的虛表

每個包含了虛函數的類都包含一個虛表。?
我們知道,當一個類(A)繼承另一個類(B)時,類A會繼承類B的函數的調用權。所以如果一個基類包含了虛函數,那么其繼承類也可調用這些虛函數,換句話說,一個類繼承了包含虛函數的基類,那么這個類也擁有自己的虛表。

我們來看以下的代碼。類A包含虛函數vfunc1,vfunc2,由于類A包含虛函數,故類A擁有一個虛表。

class A { public:virtual void vfunc1();virtual void vfunc2();void func1();void func2(); private:int m_data1, m_data2; };

虛表是一個指針數組,其元素是虛函數的指針,每個元素對應一個虛函數的函數指針。需要指出的是,普通的函數即非虛函數,其調用并不需要經過虛表,所以虛表的元素并不包括普通函數的函數指針。?
虛表內的條目,即虛函數指針的賦值發生在編譯器的編譯階段,也就是說在代碼的編譯階段,虛表就可以構造出來了。

?

3. 虛表指針

虛表是屬于類的,而不是屬于某個具體的對象,一個類只需要一個虛表即可。同一個類的所有對象都使用同一個虛表。?
為了指定對象的虛表,每個對象的內部包含一個虛表的指針,來指向自己所使用的虛表。為了讓每個包含虛表的類的對象都擁有一個虛表指針,編譯器在類中添加了一個指針,*__vptr,用來指向虛表。這樣,當類的對象在創建時便擁有了這個指針,且這個指針的值會自動被設置為指向類的虛表。

上面指出,一個繼承類的基類如果包含虛函數,那個這個繼承類也有擁有自己的虛表,故這個繼承類的對象也包含一個虛表指針,用來指向它的虛

4. 動態綁定

class A { public:virtual void vfunc1();virtual void vfunc2();void func1();void func2(); private:int m_data1, m_data2; };class B : public A { public:virtual void vfunc1();void func1(); private:int m_data3; };class C: public B { public:virtual void vfunc2();void func2(); private:int m_data1, m_data4; };

類A是基類,類B繼承類A,類C又繼承類B。類A,類B,類C,其對象模型如下圖3所示。

由于這三個類都有虛函數,故編譯器為每個類都創建了一個虛表,即類A的虛表(A vtbl),類B的虛表(B vtbl),類C的虛表(C vtbl)。類A,類B,類C的對象都擁有一個虛表指針,*__vptr,用來指向自己所屬類的虛表。?

  • 類A包括兩個虛函數,故A vtbl包含兩個指針,分別指向A::vfunc1()和A::vfunc2()。?
  • 類B繼承于類A,故類B可以調用類A的函數,但由于類B重寫了B::vfunc1()函數,故B vtbl的兩個指針分別指向B::vfunc1()和A::vfunc2()。?
  • 類C繼承于類B,故類C可以調用類B的函數,但由于類C重寫了C::vfunc2()函數,故C vtbl的兩個指針分別指向B::vfunc1()(指向繼承的最近的一個類的函數)和C::vfunc2()。?

雖然圖3看起來有點復雜,但是只要抓住“對象的虛表指針用來指向自己所屬類的虛表,虛表中的指針會指向其繼承的最近的一個類的虛函數”這個特點,便可以快速將這幾個類的對象模型在自己的腦海中描繪出來。[非虛函數的調用不用經過虛表,故不需要虛表中的指針指向這些函數。

【注意】非虛函數的調用不用經過虛表,故不需要虛表中的指針指向這些函數。

下面以代碼說明,代碼如下:

#include <iostream> using namespace std;class Base { public:virtual void f() { cout << "Base::f" << endl; }virtual void g() { cout << "Base::g" << endl; }virtual void h() { cout << "Base::h" << endl; } };typedef void(*Fun)(void); //函數指針 int main() {Base b;// 這里指針操作比較混亂,在此稍微解析下:// *****printf("虛表地址:%p\n", *(int *)&b); 解析*****:// 1.&b代表對象b的起始地址// 2.(int *)&b 強轉成int *類型,為了后面取b對象的前四個字節,前四個字節是虛表指針// 3.*(int *)&b 取前四個字節,即vptr虛表地址//// *****printf("第一個虛函數地址:%p\n", *(int *)*(int *)&b);*****:// 根據上面的解析我們知道*(int *)&b是vptr,即虛表指針.并且虛表是存放虛函數指針的// 所以虛表中每個元素(虛函數指針)在32位編譯器下是4個字節,因此(int *)*(int *)&b// 這樣強轉后為了后面的取四個字節.所以*(int *)*(int *)&b就是虛表的第一個元素.// 即f()的地址.// 那么接下來的取第二個虛函數地址也就依次類推. 始終記著vptr指向的是一塊內存,// 這塊內存存放著虛函數地址,這塊內存就是我們所說的虛表.//printf("虛表地址:%p\n", *(int *)&b);printf("第一個虛函數地址:%p\n", *(int *)*(int *)&b);printf("第二個虛函數地址:%p\n", *((int *)*(int *)(&b) + 1));Fun pfun = (Fun)*((int *)*(int *)(&b)); //vitural f();printf("f():%p\n", pfun);pfun();pfun = (Fun)(*((int *)*(int *)(&b) + 1)); //vitural g();printf("g():%p\n", pfun);pfun(); }

輸出結果:

通過這個示例,我們可以看到,我們可以通過強行把&b轉成int *,取得虛函數表的地址,然后,再次取址就可以得到第一個虛函數的地址了,也就是Base::f(),這在上面的程序中得到了驗證(把int* 強制轉成了函數指針)。通過這個示例,我們就可以知道如果要調用Base::g()和Base::h(),其代碼如下

(Fun)*((int*)*(int*)(&b)+0); // Base::f() (Fun)*((int*)*(int*)(&b)+1); // Base::g() (Fun)*((int*)*(int*)(&b)+2); // Base::h()

?

二、一般繼承?

下面,再讓我們來看看繼承時的虛函數表是什么樣的。假設有如下所示的一個繼承關系:

class Base { public:virtual void f() { cout << "Base::f()" << endl; }virtual void g() { cout << "Base::g()" << endl; }virtual void h() { cout << "Base::h()" << endl; } };class Derive : public Base { public:virtual void f1() { cout << "Base::f1()" << endl; }virtual void g1() { cout << "Base::g1()" << endl; }virtual void h1() { cout << "Base::h1()" << endl; } };

?

?請注意,在這個繼承關系中,子類沒有重載任何父類的函數。那么,在派生類的實例中,其虛函數表如下所示:

對于實例:Derive d; 的虛函數表如下:

我們可以看到下面幾點:

  • 虛函數按照其聲明順序放于表中。
  • 父類的虛函數在子類的虛函數前面。

?

三、一般繼承(有虛函數覆蓋)?

覆蓋父類的虛函數是很顯然的事情,不然,虛函數就變得毫無意義。下面,我們來看一下,如果子類中有虛函數重載了父類的虛函數,會是一個什么樣子?假設,我們有下面這樣的一個繼承關系。

class Base { public:virtual void f() { cout << "Base::f()" << endl; }virtual void g() { cout << "Base::g()" << endl; }virtual void h() { cout << "Base::h()" << endl; } };class Derive : public Base { public:virtual void f() { cout << "Base::f1()" << endl; }virtual void g1() { cout << "Base::g1()" << endl; }virtual void h1() { cout << "Base::h1()" << endl; } };

為了讓大家看到被繼承過后的效果,在這個類的設計中,我只覆蓋了父類的一個函數:f()。那么,對于派生類的實例,其虛函數表會是下面的一個樣子:

我們從表中可以看到下面幾點,

  • 覆蓋的f()函數被放到了虛表中原來父類虛函數的位置。
  • 沒有被覆蓋的函數依舊。

這樣,我們就可以看到對于下面這樣的程序:

Base *b = new Derive(); b->f();

由b所指的內存中的虛函數表的f()的位置已經被Derive::f()函數地址所取代,于是在實際調用發生時,是Derive::f()被調用了。這就實現了多態。

?

四、單一的一般繼承

?下面,我們假設有如下所示的一個繼承關系:

注意,在這個繼承關系中,父類,子類,孫子類都有自己的一個成員變量。而了類覆蓋了父類的f()方法,孫子類覆蓋了子類的g_child()及其超類的f()。

測試代碼:

#include<iostream> using namespace std;class Parent { public:int iparent;Parent(): iparent(10) {}virtual void f() { cout << " Parent::f()" << endl; }virtual void g() { cout << " Parent::g()" << endl; }virtual void h() { cout << " Parent::h()" << endl; } };class Child : public Parent { public:int ichild;Child(): ichild(100) {}virtual void f() { cout << "Child::f()" << endl; }virtual void g_child() { cout << "Child::g_child()" << endl; }virtual void h_child() { cout << "Child::h_child()" << endl; } };class GrandChild : public Child { public:int igrandchild;GrandChild(): igrandchild(1000) {}virtual void f() { cout << "GrandChild::f()" << endl; }virtual void g_child() { cout << "GrandChild::g_child()" << endl; }virtual void h_grandchild() { cout << "GrandChild::h_grandchild()" << endl; } };int main() {typedef void(*Fun)(void);GrandChild gc;int** pVtab = (int**)&gc;cout << "[0] GrandChild::_vptr->" << endl;for (int i = 0; (Fun)pVtab[0][i] != NULL; i++) {Fun pFun = (Fun)pVtab[0][i];cout << " [" << i << "] ";pFun();}cout << "[1] Parent.iparent = " << (int)pVtab[1] << endl;cout << "[2] Child.ichild = " << (int)pVtab[2] << endl;cout << "[3] GrandChild.igrandchild = " << (int)pVtab[3] << endl; }

輸出結果:

使用圖片表示如下:

可見以下幾個方面:

  • 虛函數表在最前面的位置。
  • 成員變量根據其繼承和聲明順序依次放在后面。
  • 在單一的繼承中,被overwrite的虛函數在虛函數表中得到了更新。

?

參考資料

  • c++中虛基類表和虛函數表的布局

?

?

總結

以上是生活随笔為你收集整理的【C++ Primer | 15】虚函数表剖析(一)的全部內容,希望文章能夠幫你解決所遇到的問題。

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