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

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程语言 > c/c++ >内容正文

c/c++

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

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

一、虛函數(shù)

1. 概念

多態(tài)指當(dāng)不同的對象收到相同的消息時,產(chǎn)生不同的動作

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

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

C++多態(tài)實現(xiàn)的原理:

  • ?當(dāng)類中聲明虛函數(shù)時,編譯器會在類中生成一個虛函數(shù)表
  • 虛函數(shù)表是一個存儲成員函數(shù)地址的數(shù)據(jù)結(jié)構(gòu)
  • 虛函數(shù)表是由編譯器自動生成與維護的
  • ?virtual成員函數(shù)會被編譯器放入虛函數(shù)表中
  • 存在虛函數(shù)表時,每個對象中都有一個指向虛函數(shù)表的指針

?

2. 類的虛表

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

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

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

虛表是一個指針數(shù)組,其元素是虛函數(shù)的指針,每個元素對應(yīng)一個虛函數(shù)的函數(shù)指針。需要指出的是,普通的函數(shù)即非虛函數(shù),其調(diào)用并不需要經(jīng)過虛表,所以虛表的元素并不包括普通函數(shù)的函數(shù)指針。?
虛表內(nèi)的條目,即虛函數(shù)指針的賦值發(fā)生在編譯器的編譯階段,也就是說在代碼的編譯階段,虛表就可以構(gòu)造出來了。

?

3. 虛表指針

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

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

4. 動態(tài)綁定

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所示。

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

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

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

【注意】非虛函數(shù)的調(diào)用不用經(jīng)過虛表,故不需要虛表中的指針指向這些函數(shù)。

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

#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); //函數(shù)指針 int main() {Base b;// 這里指針操作比較混亂,在此稍微解析下:// *****printf("虛表地址:%p\n", *(int *)&b); 解析*****:// 1.&b代表對象b的起始地址// 2.(int *)&b 強轉(zhuǎn)成int *類型,為了后面取b對象的前四個字節(jié),前四個字節(jié)是虛表指針// 3.*(int *)&b 取前四個字節(jié),即vptr虛表地址//// *****printf("第一個虛函數(shù)地址:%p\n", *(int *)*(int *)&b);*****:// 根據(jù)上面的解析我們知道*(int *)&b是vptr,即虛表指針.并且虛表是存放虛函數(shù)指針的// 所以虛表中每個元素(虛函數(shù)指針)在32位編譯器下是4個字節(jié),因此(int *)*(int *)&b// 這樣強轉(zhuǎn)后為了后面的取四個字節(jié).所以*(int *)*(int *)&b就是虛表的第一個元素.// 即f()的地址.// 那么接下來的取第二個虛函數(shù)地址也就依次類推. 始終記著vptr指向的是一塊內(nèi)存,// 這塊內(nèi)存存放著虛函數(shù)地址,這塊內(nèi)存就是我們所說的虛表.//printf("虛表地址:%p\n", *(int *)&b);printf("第一個虛函數(shù)地址:%p\n", *(int *)*(int *)&b);printf("第二個虛函數(shù)地址:%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(); }

輸出結(jié)果:

通過這個示例,我們可以看到,我們可以通過強行把&b轉(zhuǎn)成int *,取得虛函數(shù)表的地址,然后,再次取址就可以得到第一個虛函數(shù)的地址了,也就是Base::f(),這在上面的程序中得到了驗證(把int* 強制轉(zhuǎn)成了函數(shù)指針)。通過這個示例,我們就可以知道如果要調(diào)用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()

?

二、一般繼承?

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

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

?

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

對于實例:Derive d; 的虛函數(shù)表如下:

我們可以看到下面幾點:

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

?

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

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

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

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

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

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

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

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

由b所指的內(nèi)存中的虛函數(shù)表的f()的位置已經(jīng)被Derive::f()函數(shù)地址所取代,于是在實際調(diào)用發(fā)生時,是Derive::f()被調(diào)用了。這就實現(xiàn)了多態(tài)。

?

四、單一的一般繼承

?下面,我們假設(shè)有如下所示的一個繼承關(guān)系:

注意,在這個繼承關(guān)系中,父類,子類,孫子類都有自己的一個成員變量。而了類覆蓋了父類的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; }

輸出結(jié)果:

使用圖片表示如下:

可見以下幾個方面:

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

?

參考資料

  • c++中虛基類表和虛函數(shù)表的布局

?

?

總結(jié)

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

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。