以c的視角來理解c++的多態

1.概述

c++是一門混合型編程語言,即支持面向對象有支持面向過程,其中又以面向對象爲主。c++的三大主要特性:「繼承」,「封裝」,「多態」中,又以「多態」最難以理解,本文將經過c的視角來詮釋c++的多態。ios

2.動手前的預備知識點

2.1 你知道不一樣類型的指針意味着什麼?

不一樣的指針類型,意味這對同一塊內存起始地址的不一樣解析方式,下面咱們舉一個栗子。c++

  • 代碼 test4.c編程

#include <stdint.h>
#include <string.h>
#include <malloc.h>
#include <stdio.h>

int32_t g_int = 10;

int main()
{
    //申請5個字節大小的堆內存
    void * p = malloc(5);
    //每一個字節的值設置爲90
    memset(p, 90, 5);        
    
    //聲明一個char指針pc指向分配的內存的起始地址
    char * pc = (char *)p;
    //聲明一個int32_t指針pint指向分配的內存的起始地址
    int32_t * pint = (int32_t *)p;
    
    //從起始地址開始取1個字節的內存去解析成char變量
    if (*pc == 90) 
    {
        printf("char yes\n");
    }
    
    //從起始地址開始取4個字節的內存去解析成int32_t變量
    if (*pint == 90 + (90 << 8) + (90 << 16) + (90 << 24))
    {
        printf("int yes\n");
    }
    
    return 0;
}
  • 運行結果編程語言

clipboard.png

  • 詳細圖解ide

clipboard.png

2.2 c文件編譯成一個目標文件,目標文件裏包含什麼?

  • 至少應該包含函數符號,全局變量(若是文件中有定義全局變量)。函數

  • 經過gcc和nm命令咱們能夠看到用上面的測試代碼生成的test4.o這個目標文件中包含有一個全局變量g_int,一個有定義的函數符號main,三個未定義的函數符號malloc,memset,puts。測試

  • 命令執行結果編碼

clipboard.png

3. c++靜態多態

3.1實際編碼操做

  • 代碼test5.cppspa

#include <iostream>
using namespace std;

void fun1()
{
    cout << "fun1 call" << endl;
}

void fun1(int a)
{
    cout << "fun1 a call" << endl;
}

int main()
{
    fun1();
    fun1(10);
    return 0;
}
  • 運行結果操作系統

clipboard.png

3.2 實現的原理

  • 經過重載(overload)的特性來實現,在編譯階段就決定要調用那個函數,故稱爲靜態多態。

  • c++編譯器在編譯代碼時,會對函數符號重簽名(c編譯器不會),當c++編譯器遇到重載調用時則直接調用重簽名後的函數,使用nm命令查看可執行文件的符號咱們看到兩個被重簽名的符號

3.3以c的視角理解

#include <stdio.h>

void _Z4fun1v()
{
    printf("fun1 call\n");
}

void _Z4fun1i(int a)
{
    printf("fun1 a call\n");
}

int main()
{
    _Z4fun1v(); //對應以前的void fun1();
    _Z4fun1i(10); //對應以前的void fun1(int a);
    return 0;
}

4.c++動態多態

4.1實際編碼操做

  • 代碼test6.cpp

#include <iostream>
using namespace std;

class Base
{
public:
    virtual void sleep()
    {
        cout << "Base sleep" << endl;
    }
    virtual void eat()
    {
        cout << "Base eat" << endl;
    }
    virtual void run()
    {
        cout << "Base run" << endl;
    }
};

class Animal : public Base
{
public:
    size_t age;
    
    void sleep()
    {
        cout << "Animal sleep" << endl;
    }
    void eat()
    {
        cout << "Animal eat" << endl;
    }
    void run()
    {
        cout << "Animal run" << endl;
    }
};

/*
    定義一個函數指針類型,類型爲 void () (Animal * );
    用於指向虛函數sleep,eat,run;
    這裏之因此多出一個Animal * 參數是由於c++類的非靜態成員函數,
    編譯器會默認在參數列表開頭加入指向類指針的參數
 */
typedef void (* pFun)(Animal * animal);

int main()
{
    Animal dargon;
    Animal dog;
    
    Base * pBase = &dargon;
    Base & pRe = dog;
    
    //經過基類的指針指向派生類對象來實現動態多態
    pBase->sleep();
    //經過基類的引用指向派生類對象來實現動態多態
    pRe.sleep();
    
    /*
        取出Animal的虛表指針.
        (size_t *)&dargon -> dargon起始地址轉換爲size_t *
        *(size_t *)&dargon -> dargon起始地址開始取sizeof(size_t)個字節解析成size_t(虛表指針的值)
        (size_t *)*(size_t *)&dargon -> 把這個值轉換成size_t *類型
    
        ps: size_t在32位機是4個字節,在64位機是8個字節,指針變量的大小和size_t的大小是一致的。
     */
    size_t * vptable_dargon = (size_t *)*(size_t *)&dargon;
    size_t * vptable_dog = (size_t *)*(size_t *)&dog;
    
    cout << "size_t size = " << sizeof(size_t) << endl;
    
    //一個類公用一個虛表指針
    if (vptable_dog == vptable_dargon)
    {
        cout << "vptable value is equal" << endl;
    }
    
    //遍歷虛表指針
    while (*vptable_dargon)
    {
        //取出每一個虛表函數
        pFun fun = (pFun)(*vptable_dargon);
        //調用每一個虛表函數
        fun(&dargon);
        vptable_dargon++;
    }
    
    return 0;
}
  • 運行結果

clipboard.png

4.2 實現原理

  • 經過c++的重寫(override)的特性來實現,只有在運行時才知道真正調用是什麼那個函數,故稱爲動態多態。

  • c++爲有虛函數的每一個類添加了一個虛函數表(類的靜態變量),並在每一個類對象的起始地址處嵌入一個虛表指針指向它,再經過這個虛表指針來實現運行時的多態。

clipboard.png

4.3 以c的視角理解

  • 代碼test7.cpp

#include <stdio.h>
#include <malloc.h>
#include <string.h>

//全局靜態的虛表指針,模擬類的靜態虛表指針
static size_t * pBaseVptable = NULL;
static size_t * pAnimalVptable = NULL;

struct Base
{
    size_t * vptable;    //模擬虛表指針
};

struct Animal
{
    struct Base base;    //模擬Animal繼承Base
    size_t age;
};

typedef void (* pFun)(Base * pBase);

void baseSleep(Base * pBase)
{
    printf("Base sleep\n");
}
void baseEat(Base * pBase)
{
    printf("Base eat\n");
}
void baseRun(Base * pBase)
{
    printf("Base run\n");
}

void animalSleep(Animal * pAnimal)
{
    printf("Animal age[%d] sleep\n", pAnimal->age);
}
void animalEat(Animal * pAnimal)
{
    printf("Animal age[%d] eat\n", pAnimal->age);
}
void animalRun(Animal * pAnimal)
{
    printf("Animal age[%d] run\n", pAnimal->age);
}

//Base結構體初始化
void baseInit(Base * pBase)
{
    pBase->vptable = pBaseVptable;
}

//Animal結構體初始化
void AnimalInit(Animal * pAnimal)
{
    pAnimal->base.vptable = pAnimalVptable;
}

/*
    虛表指針初始化
 */
void vptableInit()
{
    pBaseVptable = (size_t *)malloc(sizeof(size_t) * 4);
    pAnimalVptable = (size_t *)malloc(sizeof(size_t) * 4);
    
    memset(pBaseVptable, 0x0, sizeof(size_t) * 4);
    memset(pAnimalVptable, 0x0, sizeof(size_t) * 4);
    
    //Base類全局虛表初始化
    pBaseVptable[0] = (size_t)&baseSleep;
    pBaseVptable[1] = (size_t)&baseEat;
    pBaseVptable[2] = (size_t)&baseRun;
    
    //Animal類全局虛表初始化
    pAnimalVptable[0] = (size_t)&animalSleep;
    pAnimalVptable[1] = (size_t)&animalEat;
    pAnimalVptable[2] = (size_t)&animalRun;
}

void callVirtualFun(Base * pBase, int index)
{
    pFun fun = (pFun)pBase->vptable[index];
    fun(pBase);
}

int main()
{
    //虛表初始化
    vptableInit();
    Base * pBase = NULL;
    Animal * pAnimal = (Animal *)malloc(sizeof(Animal));
    
    //模擬對象初始化
    AnimalInit(pAnimal);
    pAnimal->age = 99;
    
    //模擬基類指針指向派生類
    pBase = (Base *)pAnimal;
    
    //模擬調用虛函數
    callVirtualFun(pBase, 0);
    callVirtualFun(pBase, 1);
    callVirtualFun(pBase, 2);
    
    return 0;
}
  • 運行結果

clipboard.png

5.編譯運行環境

  • 操做系統

[root@iZ940zytujjZ ~]# uname -m -s

Linux x86_64

  • 編譯器

[root@iZ940zytujjZ ~]# gcc --versiongcc (GCC) 4.4.7 20120313 (Red Hat 4.4.7-4)[root@iZ940zytujjZ ~]# g++ --versiong++ (GCC) 4.4.7 20120313 (Red Hat 4.4.7-4)

相關文章
相關標籤/搜索