(一图胜千言)虚函数实现机制(Vptr, Vtbl)

1. 摘要

  • 讲解C++中虚函数的实现机制,主要是Vptr和Vtbl的讲解,有了虚函数才可以拥有像多态这种强大的功能。
  • 虚函数主要是出现在类的继承体系中。

2.虚指针vptr和虚表vtbl

在这里插入图片描述

虚指针及虚表的概念(来自参考资料5)
首先要清楚,所谓指针其实质就是一个内存地址值,形如0x12345678;其次,要知道,函数名本身就是一个地址。
虚指针:其实就是一个地址值,以该地址为起始地址的一片内存单元存放着各虚函数的入口地址,这一片内存单元合起来就称为虚函数表(想象一下:一片内存单元存着许多函数地址,想执行哪个虚函数就来这片内存单元查找该虚函数的入口地址,就像查表一样,故称虚函数表)。经过以上解释,可以发现,所谓虚指针,就是个指向指针的指针。

2.1. 从上图可以看出:

  • 基类和派生类的第一个条目存放的是虚指针,虚指针指的是虚表vtbl的起始地址,虚表中存放的是虚函数的入口地址,可以通过函数指针获取到对应的函数去执行。
  • 相关的代码见下节。

3. 相关代码

#include <iostream>

No virtual class/
class Animal {
public:
    void eat() {
        std::cout << "I'm eating generic food." << std::endl;
    }
    void shout() {
        std::cout << "I'm shouting genericly." << std::endl;
    }
};

class Cat : public Animal {
public:
    void eat() {
        std::cout << "I'm eating a rat." << std::endl;
    }
    void shout() {
        std::cout << "I'm shouting with meow" << std::endl;
    }

};

void distinguish(Animal* obj) {
    obj->eat();
    obj->shout();
}

 class with virtual/
class Animal_V {
public:
    virtual void eat() {
        std::cout << "I'm eating generic food." << std::endl;
    }

    virtual void shout() {
        std::cout << "I'm shouting genericly." << std::endl;
    }
    virtual ~Animal_V() = default;
};

class Cat_V : public Animal_V {
public:
    void eat() override {
        std::cout << "I'm eating a rat." << std::endl;
    }
    void shout() override {
        std::cout << "I'm shouting with meow" << std::endl;
    }

    ~Cat_V() override = default;
};

void distinguish(Animal_V* obj) {
    obj->eat();
    obj->shout();
}

int main(int argc, char* argv[]) {

    auto *animal = new Animal;
    auto *cat = new Cat;

    std::cout << "------------------Normal use------------------" << std::endl;
    animal->eat();
    animal->shout();
    cat->eat();
    cat->shout();

    std::cout << "------------------Use same interface without vitual------------------" << std::endl;
    distinguish(animal);
    distinguish(cat);

    std::cout << "==================Divider==================" << std::endl;
    auto *animal_V = new Animal_V;
    auto *cat_V = new Cat_V;

    std::cout << "------------------Normal use------------------" << std::endl;
    animal_V->eat();
    animal_V->shout();
    cat_V->eat();
    cat_V->shout();

    std::cout << "------------------Use same interface with vitual------------------" << std::endl;
    distinguish(animal_V);
    distinguish(cat_V);
    
    //由于sizeof(your class)会涉及到内存对齐,所以得到的字节数可能不是你想的数字,比如int a,b;和int a;可能都是占8个字节。
    std::cout << "------------------Compare sizeof------------------" << std::endl;
    std::cout << "Animal'size is: " << sizeof(Animal) << std::endl;
    std::cout << "Cat'size is: " << sizeof(Cat) << std::endl;
    std::cout << "Animal_V'size is: " << sizeof(Animal_V) << std::endl;
    std::cout << "Cat_V'size is: " << sizeof(Cat_V) << std::endl;

    std::cout << "------------------The address of virtual function------------------" << std::endl;
    std::cout << "Animal_V::eat -> " << (void*)(&Animal_V::eat) << std::endl;
    std::cout << "Animal_V::shout -> " << (void*)(&Animal_V::shout) << std::endl;
    std::cout << "Cat_V::eat -> " << (void*)(&Cat_V::eat) << std::endl;
    std::cout << "Cat_V::shout -> " << (void*)(&Cat_V::shout) << std::endl;

    std::cout << "------------------The address of vptr&vtbl------------------" << std::endl;
    Animal_V obj;
    std::cout << "VPTR's address:" << (long*) (&obj+0) << std::endl; //由于我的电脑是64位系统,指针为8个地址,所以使用long类型的指针获取虚指针以及下面的虚表
    std::cout << "VTBL's address:" << (long*) (*(long*)(&obj+0)) << std::endl;
    std::cout << "The entry address of the first virtual function: " << (void*) (*((long*)*(long*)(&obj+0)+0)) << std::endl;
    std::cout << "The entry address of the second virtual function: " << (void*) (*((long*)*(long*)(&obj+0)+1)) << std::endl;
    // long* vptr_addr = (long*) &obj;
    // long* vtbl_addr = (long*) *vptr_addr;
    // std::cout << "The entry address of the first virtual function: " << (long*) *(vtbl_addr + 0) << std::endl;
    // std::cout << "The entry address of the second virtual function: " << (long*) *(vtbl_addr + 1) << std::endl;

    std::cout << "------------------Invoke virtual function by pFun------------------" << std::endl;
    typedef void(*pFun)(void);
    pFun Fun = nullptr;
    Fun = reinterpret_cast<pFun>(*((long*)*(long*)(&obj + 0) + 0));
    Fun();
    Fun = reinterpret_cast<pFun>(*((long*)*(long*)(&obj + 0) + 1));
    Fun();

    delete animal;
    delete cat;
    delete animal_V;
    delete cat_V;

    return 0;
}

程序的输出结果为:

------------------Normal use------------------
I'm eating generic food.
I'm shouting genericly.
I'm eating a rat.
I'm shouting with meow
------------------Use same interface without vitual------------------
I'm eating generic food.
I'm shouting genericly.
I'm eating generic food.
I'm shouting genericly.
==================Divider==================
------------------Normal use------------------
I'm eating generic food.
I'm shouting genericly.
I'm eating a rat.
I'm shouting with meow
------------------Use same interface with vitual------------------
I'm eating generic food.
I'm shouting genericly.
I'm eating a rat.
I'm shouting with meow
------------------Compare sizeof------------------
Animal'size is: 1
Cat'size is: 1
Animal_V'size is: 8
Cat_V'size is: 8
------------------The address of virtual function------------------
Animal_V::eat -> 0x4018c6
Animal_V::shout -> 0x4018f2
Cat_V::eat -> 0x40191e
Cat_V::shout -> 0x40194a
------------------The address of vptr&vtbl------------------
VPTR's address:0x7ffcbb9e5bc8
VTBL's address:0x401e40
The entry address of the first virtual function: 0x4018c6
The entry address of the second virtual function: 0x4018f2
------------------Invoke virtual function by pFun------------------
I'm eating generic food.
I'm shouting genericly.

可以看出使用virtual后即使使用的是同一个接口,会根据对象的不同自动变换,对于各自的接口会在虚表中找到函数的入口地址,这种多态的方式叫“动态绑定”。
注意:多态的实现是通过指针和引用;而对象的转换只会造成对象切割,不能实现多态。

4.参考资料

  1. Why do we need virtual functions in C++?
  2. [面试经]VPTR和VTBL
  3. c++对象切割(Object Slicing)
  4. C++ 之 多态
  5. 虚指针、虚表及内存布局(不错)
  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值