1. 程式人生 > >C++學習之多型篇(虛擬函式和虛解構函式的實現原理--虛擬函式表)

C++學習之多型篇(虛擬函式和虛解構函式的實現原理--虛擬函式表)

通過下面的程式碼來說明:

#include <iostream>

#include <stdlib.h>
#include <string>
using namespace std;


/**
 *  定義動物類:Animal
 *  成員函式:eat()、move()
 */
class Animal
{
public:
    // 建構函式
    Animal(){cout << "Animal" << endl;}
    // 解構函式
   virtual  ~Animal(){cout << "~Animal" << endl;}
    // 成員函式eat()
void eat(){cout << "Animal -- eat" << endl;}
    // 成員函式move()
     void move(){cout << "Animal -- move" << endl;}
     
    //   int m_iLegs;


    //  short m_hands;
    //  char m_face;
    //  int m_iHead;
     
};


class A
{
    public:
    A(){cout<<"A"<<endl;}
   virtual
 ~A(){cout<<"~A"<<endl;}
};
/**
 * 定義狗類:Dog
 * 此類公有繼承動物類
 * 成員函式:父類中的成員函式
 */
class Dog : public Animal,public A
{
public:
    // 建構函式
Dog(int legs)
    {
        cout << "Dog" << endl;
        m_iLegs = legs;
    }
    // 解構函式
~Dog(){cout << "~Dog" << endl;}
    // 成員函式eat()
     void eat(){cout << "Dog -- eat" << endl;}
    // 成員函式move()
void move(){cout << "Dog -- move" << endl;}
public:
     int m_iLegs;
};


int main(void)
{
    // 通過父類物件例項化狗類
//  Animal * p = new Dog;
    //  Animal animal;
    //  cout<<sizeof(animal)<<endl;
    //  int * q = (int * )&animal;
    //  cout<<*(q)<<endl;
    
    Dog dog(4);
    cout<<sizeof(dog)<<endl;
    
    int * p = (int*)&dog;
    cout<<&dog<<endl;
    cout<<p<<endl;
    p++;
    cout<<p<<endl;
    p++;
   
    cout<<"p++: "<<p<<endl;
    cout<<(unsigned int)(*p)<<endl;
    // cout<< dog.m_iLegs<<endl;
    // cout<< (int *)p<<endl;
    // 呼叫成員函式
    //  p->eat();
    //  p->move();
    // 釋放記憶體
    //  delete p;
    //  p = NULL;
    
return 0;

}

當兩個virtual都不加的時候,輸出結果:

Animal
A
Dog
4
0x7ffc94887060
0x7ffc94887060
0x7ffc94887064
p++: 0x7ffc94887068
2491969640
~Dog
~A
~Animal
分析:此時,由於函式不佔用物件記憶體大小,有專門的程式程式碼區來存放程式的二進位制程式碼,因此Dog物件dog的記憶體大小隻是其成員變數的大小,此時有一個int型的成員變數m_iLegs,因此此時輸出的sizeof(dog)的大小是4個位元組

當加上其中一個virtual以後,比如Animal的解構函式成了虛解構函式,輸出結果:

Animal
A
Dog
16
0x7fffaf8dd880
0x7fffaf8dd880
0x7fffaf8dd884
p++: 0x7fffaf8dd888
4
~Dog
~A
~Animal
分析:此時,Dog從Animal繼承,因為虛解構函式的特性是可以繼承的,所以dog的解構函式也是虛解構函式,雖然沒寫virtual,但是編譯器會自動為其加上virtual。這樣,Dog的物件的記憶體中除了存放了自己的成員變數以為,還存放了一個虛擬函式表指標,在64位機器上,指標大小為8個位元組,又因為高位對齊原則,所以這一次輸出的sizeof(dog)的值得大小是16個位元組,(8(指標)+4(int)+4空記憶體)

如果再將類A的virtual加上,其實不光是解構函式,宣告其他函式為虛擬函式也一樣,這時,輸出的結果如下:

Animal
A
Dog
24
0x7ffe2707d310
0x7ffe2707d310
0x7ffe2707d314
p++: 0x7ffe2707d318
4198544
~Dog
~A
~Animal
分析:此時Dog類的物件中將含有兩個虛擬函式表,輸出的結果將是兩個虛擬函式指標加整型成員變數+4個空記憶體的大小=24個位元組了。

這裡注意兩點:

(1)高位對齊!就是輸出的大小都是高位的整數倍,這個例子中都是8的整數倍;

(2)在物件的記憶體空間中,按照繼承順序,指向繼承來的兩個虛擬函式表的虛擬函式表指標將排在第一位和第二位,佔據前16個位元組,然後才是int型的成員變數佔用四個位元組,所以p要p++四次才能到達int型變數的首地址,才能輸出正確的值4,p++一次前進4個位元組。如下程式碼所示:

  Dog dog(4);
    cout<<sizeof(dog)<<endl;
    
    int * p = (int*)&dog;
    cout<<&dog<<endl;
    cout<<p<<endl;
    p++;
    cout<<p<<endl;
    p++;
      cout<<p<<endl;
    p++;
      cout<<p<<endl;
    p++;
    cout<<"p++: "<<p<<endl;
    cout<<(unsigned int)(*p)<<endl;

輸出結果為:

Animal
A
Dog
24
0x7ffe12b78160
0x7ffe12b78160
0x7ffe12b78164
0x7ffe12b78168
0x7ffe12b7816c
p++: 0x7ffe12b78170
4
~Dog
~A
~Animal

1.總結虛擬函式的實現原理





當類中有虛擬函式或者虛解構函式時,在例項化類的物件時,物件記憶體中除了成員變數的大小,還有一個虛擬函式表指標,而且虛擬函式表指標放在記憶體的最前面,虛擬函式表指標會指向一個虛擬函式表,而以為Shape類中含有虛擬函式,這個虛擬函式表將於Shape類的定義同時出現,在計算機中虛擬函式表也是佔用一定到的記憶體空間的,且虛擬函式表由於一旦產生就具有不變性,所以編譯器就會經量把它放到穩定(或者說是隻讀)的記憶體區。虛擬函式表vtable在Linux/Unix中存放在可執行檔案的只讀資料段中(rodata)。

在上例中虛擬函式表的起始位置是0xCCFF,那麼虛擬函式表指標的值就是0xCCFF,每個類只有一張虛擬函式表,所有類的物件都共用同一張虛擬函式表。所有物件都含有相同的虛擬函式表指標值0xCCFF,以確保所有的物件含有的虛擬函式表指標都指向正確的虛擬函式表。虛擬函式表中存放的是所有的虛擬函式地址。計算時,先找到虛擬函式表指標,通過指標的偏移找到虛擬函式的指標,然後就可以呼叫虛擬函式。

上例圖中,Circle派生自Shape,Circle從父類派生了虛擬函式,於是它也有了自己的虛擬函式表,這兩個表的起始地址是不一樣的,但是表中calcArea()函式的起始地址是一樣的,這也就是說,兩張不同的虛擬函式表中的函式指標可能指向同一個函式。

當Circle類定義了自己的虛擬函式,如下圖所示,



由於此時,Circle類自己定義了calcArea()函式,所以將會覆蓋掉父類的函式。

2.總結虛解構函式的是實現原理:

虛解構函式的特點是當將父類的解構函式宣告為虛解構函式以後,再用父類的指標去指向子類的物件,並用delete去銷燬父類的指標的時候,不會再只調用父類的解構函式,而是會先呼叫子類的解構函式再呼叫父類的解構函式,即會釋放掉子類物件了,不會再因為子類物件得不到釋放而產生記憶體洩露。這種情況也和虛擬函式表有關。實現過程如下:當宣告父類解構函式為虛解構函式以後,在子類和父類的虛擬函式表中將都出現虛解構函式的函式指標,如下兩幅圖所示。當用父類的指標指向子類的物件,用delete Shape釋放物件時,會通過Shape指標找到子類Circle的虛擬函式表指標,從而找到虛擬函式表,從而通過偏移找到虛解構函式的地址,從而呼叫子類Circle的解構函式,然後也會呼叫父類的解構函式。


多型的實現原理如下:當用父類的指標去指向子類物件時,會拿到子類的虛擬函式表指標,然後找到虛擬函式表,通過虛擬函式表指標的偏移,找到要呼叫的虛擬函式的函式指標,從而實現函式的呼叫。注意這裡的偏移必須是和父類的偏移量是一樣的。

與本節內容有關,補充兩個概念:函式的隱藏和覆蓋

函式的隱藏:沒有定義多型的情況下,即沒有加virtual的前提下,如果定義了父類和子類,父類和子類出現了同名的函式,就稱子類的函式把同名的父類的函式給隱藏了。

函式的覆蓋:是針對多型來說的。如果定義了父類和子類,父類中定義了公共的虛擬函式,如果此時子類中沒有定義同名的虛擬函式,那麼在子類的虛擬函式表中將會寫上父類的該虛擬函式的函式入口地址,如果在子類中定義了同名虛擬函式的話,那麼在子類的虛擬函式表中將會把原來的父類的虛擬函式地址覆蓋掉,覆蓋成子類的虛擬函式的函式地址,這種情況就稱為函式的覆蓋。

相關推薦

C++學習虛擬函式函式實現原理--虛擬函式

通過下面的程式碼來說明: #include <iostream> #include <stdlib.h> #include <string> using namespace std; /**  *  定義動物類:Animal  *  成員

C++學習及過載(overload),覆蓋(override),隱藏(hide)的區別

C++程式語言是一款應用廣泛,支援多種程式設計的計算機程式語言。我們今天就會為大家詳細介紹其中C++多型性的一些基本知識,以方便大家在學習過程中對此能夠有一個充分的掌握。   多型性可以簡單地概括為“一個介面,多種方法”,程式在執行時才決定呼叫的函式,它是面向物件程式設計

linux下C 程式設計學習程序程式設計

一、程序概念 程序是作業系統中資源分配的最小單位,而執行緒是排程的最小單位。 一個程序,主要包含三個元素: a)        一個可以執行的程式; b)        和該程序相關聯的全部資料(包括變數,記憶體空間,緩衝區等等); c)        程式的執行上下文(

linux學習進程

通過 passwd 查看 現在 替換 cnblogs exe -1 stdio.h 進程原語 1.fork #include<unistd.h> pid_t fork(void);   fork   子進程復制父進程,子進程和父進程的PID是不一樣的,在

C++學習筆記——

運算子過載 過載為類成員的運算子定義形式 函式型別 oprator 運算子(形參) { ... } //引數個數=原運算元個數-1 (後置++、--除外) 雙目運算子過載規則 如果要過載B為類成員函式,使之能夠實現表示式oprd1 B oprd2,其中oprd1為A類物

JavaScript學習小白五-函式的作用域及建立物件

好好學習 ,天天向上。Are you ready? 一、作用域及作用域鏈 1. 什麼是作用域? 2. JS在ES5這個版本中有哪些作用域? 1》script作用域(全域性作用域) 宣告在全域性作用域的變數,叫全域性變數,同時也是window物件的屬性 宣告在全域性作用域的函式,叫全域性函

JavaScript學習小白四-函式的介紹

好好學習 ,天天向上。Are you ready? 一、什麼是函式? 需要反覆使用的功能程式碼,封裝成一個獨立的模組,這個模組叫函式。 二、函式的分類? 內建函式和自定義函式 三、函式的型別? Function 四、函式的好處? 1. 方便管理 2. 一次封裝,多次使用 五、如何宣告函式

JavaScript學習小白

好好學習 ,天天向上。Are you ready? 一、什麼是迴圈結構? 滿足一定條件 ,(重複)執行一段程式碼。 二、迴圈思想是什麼? 從哪開始迴圈(迴圈起點) 到哪裡結束(迴圈終點) 步進(步長)(促使迴圈結束) 三、實現迴圈的語句有哪些? while 迴圈,當型迴圈 格

JavaScript學習小白

好好學習 ,天天向上。Are you ready? --------------------if swich語句------------------ 一、程式中流程控制有哪些結構? 1. 順序結構 : 從上到下,依次執行每一條語句,不允許跳過任何一條語句。 2. 選擇結構 : 根據條

JavaScript學習小白

好好學習 ,天天向上。Are you ready? 一、JS概述 1. 什麼是JS? Javascript是(基於物件)和(事件驅動)的(客戶端指令碼)語言。 2. 哪一年?哪家公司?誰?第一個名字? 1995 網景 布蘭登 livescript 3. W3C第一套標準:ECMA-262

JavaScript學習小白十-BOM學習

好好學習 ,天天向上。Are you ready? 一、設定或獲取元素節點物件的屬性 1. setAttribute() 2. getattribute() 3. removeAttribute() 二、outerHTML : 獲取元素節點物件的全部內容,包含當前節點 innerTe

JavaScript學習小白九-BOM學習

好好學習 ,天天向上。Are you ready? 一、什麼是BOM? 瀏覽器物件模型。 二、BOM中的頂級物件是什麼? window 三、window下的子物件有哪些? document/history/location/frames/navigator/screen 四、如何跳轉

JavaScript學習小白八-Math及Date用法

好好學習 ,天天向上。Are you ready? 一、Math 1. Math.PI 圓周率 2. 近似值 1> Math.round() 四捨五入 注: 負數 >0.5 進一 <= 0.5 捨去 2> Math.ceil() 向上取整 3> Math

JavaScript學習小白六-陣列的介紹及使用方法

好好學習 ,天天向上。Are you ready? 一、陣列概述 1. 什麼是陣列? 儲存一組或一系列相關資料的容器。 2. 什麼時候使用陣列? 處理資料多的時候 3. 下標: 0 length - 1 4. 陣列元素: 存放在陣列的資料 二、如何宣告陣列? 1. 字面量(json)

Java應用程式開發學習

在Java中,使用關鍵字extends繼承或者關鍵字implements實現,是Java實現多型性的前提。 一、Java多型定義的格式 父類引用指向之類物件稱之為多型,多型的定義格式主要有兩種。 (1)父類名稱 物件名 = new 子類名稱(); (2)介面名稱 物件名 = new

Spring Boot 學習持久層

該系列並非完全原創,官方文件、作者一、前言上一篇《Spring Boot 入門之 Web 篇(二)》介紹了 Spring Boot 的 Web 開發相關的內容,專案的開發離不開資料,因此本篇開始介紹持久層相關的知識。二、整合 JdbcTemplate1、新增依賴在pom.xm

ARM處理器學習--GPIO操作gnu link script

1:主要內容       本文主要介紹了VMA、LMA的相關概念,gnu link script的作用和使用方法。 2:引言       我們程式設計師剛開始學習編寫程式時,都會接觸到一個 " *.C " 檔案要經過編譯、連結等過程才能變成可以執行的程式。至於這裡的連結到底

grbl學習旅---protocol補充

protocol.c和protocol.h是實現控制grbl的方法和程式執行協議。涉及到了system.h;stepper.h;print.h;report.h;system.h 是系統級命令和實時程序。stepper.h是步進電機驅動器,使用步進電機執行planner.c的

C++11執行緒

二, 互斥物件和鎖    互斥(Mutex::Mutual Exclusion)    下面的程式碼中兩個執行緒連續的往int_set中插入多個隨機產生的整數#include <thread> #include <set> #include <r

強化學習最基礎演算法實現及基礎案例學習

    本部落格接著上一篇“強化學習之最基礎篇”而來,是基於上一篇的部落格進一步的探究,因為前一篇部落格完全是對於基本概念的介紹以及基本演算法的熟悉,這一篇便是偏應用,講理論的演算法加以實現,並且跑了一個小遊戲從而感受一下強化學習的魅力。 背景:在PA公司實習