16-虚表

作者: ducktobey | 来源:发表于2019-11-20 19:30 被阅读0次

虚表

在上一篇文章中,对虚函数进行了讨论,Animal的子类继承与Animal,如Cat,在Animal中定义了两个函数,并且在子类中对这些函数进行了重写,然后将创建的对象,通过父类指针来指向该对象,利用父类指针来调用Animal类包含的两个函数,按照默认的情况,通过指针调用函数,最终被调用的是父类中定义的函数,因为在默认情况下,调用函数是只看当前指针的类型。这种情况与平时开发中不常用,更常用的是根据右边真正的对象类型,来调用对象对应的函数。前面的做法是在父类定义定义函数时,使用virtual关键字来修饰,然后将两个函数变为虚函数后,才能实现面向对象的多态。

接下来就探究以下,虚函数的原理是什么,为什么这么神奇。

首先定义如下两个类

struct Animal {
    int m_age;
    virtual void speak() {
        cout << "Animal::speak()" << endl;
    }

    virtual void run() {
        cout << "Animal::run()" << endl;
    }
};

struct Cat : Animal {
    int m_life;
    void speak() {
        cout << "Cat::speak()" << endl;
    }

    void run() {
        cout << "Cat::run()" << endl;
    }
};

int main() {

    cout << sizeof(Cat) << endl;
    getchar();
    return 0;
}

然后现在可以看以下创建出一个Cat对象,需要占用多大的内存空间,如果按照正常的情况,占用的空间是8个字节,现在将程序运行起来后,得到的结果是12个字节。也就是说,如果类中有定义虚函数,那创建出来的对象会多占用4个字节的内存空间。并且发现只要有虚函数,就会多占用4个字节的内存空间,与虚函数的数量无关(≥1),也就意味着,在虚函数的背后,需要这4个字节来实现。没错,这四个字节存储的就是想虚表的地址,首先来了解以下虚函数的原理;

虚函数的实现原理是虚表,这个虚表里面存储这最终需要调用的函数地址,这个虚表也叫虚函数表

接下来,假设通过如下的方式创建一个Cat对象,并用Animal指针来指向该对象

Animal* cat = new Cat();
cat->m_age = 20;
cat->speak();
cat->run();

通过这种方式,最终会调用Cat类中的speak方法和run方法,相信这一点是能理解的。接下来就探究以下原理。下图是在x86环境下的虚表示意图

根据上图与代码,左边的内存地址就是一个cat对象占用的内存空间一共12个字节,其中8个字节用来存放成员变量m_age与m_life,最前面多出来的4个字节就是存放虚表的地址,该地址指向了一张虚表,虚表也叫做虚函数表,因为虚表里面存放的是函数地址。上图就是一个cat对象在内存中的情况。

也就意味着,一旦多了虚函数,Cat对象就会多出4个字节,该4个字节是存放着一个地址值,并且该地址值是虚表的地址值,根据该地址值,就能找到这张虚表,然后利用虚表中存放的函数地址,调用对应的函数。

那现在可以从汇编层面来进行分析,将上面代码运行起来后,得到的汇编为

上图是在有虚函数时,调用speak函数的汇编指令。然后将指令进行剖析,得到下图

接下来在观察run函数是怎么调用的;同样将程序运行起来,得到run函数调用的汇编代码

run函数调用汇编指令剖析

现在已经从汇编层面,证明了在有虚函数时,调用函数时,确实有虚表的存在,并且确实是根据上表中内存示意图的方式,一步一步的调用函数的。接下来再从内存层面来证明虚表中存放的,确实是当前对象的函数地址。

现在将程序打上断点,并运行起来,可以得到堆空间中cat对象的地址值。

拷贝该地址值,然后再内存中搜索该地址值

从该地址值开始的12个字节,就是cat对象的内存,为了证明该内存确实是为cat对象的内存,现在将代码跳过一行,执行cat->m_age,然后得到内存图

看到内存中的值发生了变化,并且16进制的14对应10进制的20,说明该块内存空间是m_age的地址。

到这里已经得到了cat对象的地址,并且可以得到cat对象前4个字节中存储的内容,为68 9b ee 00,由于现在电脑是小端模式,最终的值为00ee9b68,则虚表的地址就位0x00ee9b68,然后再利用该地址,在内存中搜索,找到虚表的存储空间中存储的内容

利用搜索出来的地址,然后将前面8个字节转换为2个地址值。由于是小端模式,所以转换后的地址为0x00ee1460与0x00ee1028。现在将断点从的程序转到汇编代码,并单步调试至调用speak函数的地方,进入函数。可以得到该函数的地址值,得到下图

可以看到,现在汇编代码中的地址值为0x00ee1460与前面计算的地址值相等。所以虚表中前面的4个字节,是cat对象中speak函数的调用地址。现在将断点打在调用run函数的地方,再得到调用run函数的汇编指令。最终得到run函数的调用地址

可以发现,该地址同样与前面计算出来的地址值相等。所以虚表中的后面4个字节,是cat对象中run函数的调用地址。

通过上面的步骤,就证明了虚表的存在,并且虚表中存储的是对象的虚函数地址

虚析构函数

如果存在父类指针指向子类对象的情况,应该将析构函数声明为虚构函数(虚析构函数)

为什么要这样呢?

假设现在上面的代码增加了析构函数

struct Animal {
    int m_age;
    void speak() {
        cout << "Animal::speak()" << endl;
    }

    void run() {
        cout << "Animal::run()" << endl;
    }

    Animal() {
        cout << "Animal::Animal()" << endl;
    }
    ~Animal() {
        cout << "Animal::~Animal()" << endl;
    }
};

struct Cat : Animal {
    int m_life;
    void speak() {
        cout << "Cat::speak()" << endl;
    }

    void run() {
        cout << "Cat::run()" << endl;
    }

    Cat() {
        cout << "Cat::Cat()" << endl;
    }
    ~Cat() {
        cout << "Cat::~Cat()" << endl;
    }
};
int main() {
    Animal* cat = new Cat();
    delete cat;
    getchar();
    return 0;
}

如果Animal中没有虚函数时,将程序运行起来,最终得到的结果为

Animal::Animal()
Cat::Cat()
Animal::~Animal()

发现了一个问题,在创建对象的时候,调用了Animal与Cat的构造函数,但是在析构时,却只调用了Animal的析构函数,Cat的析构函数没有调用。原因是这样的,现在没有虚函数,所以不存在虚表的概念,所以在析构的时候,只会根据指针的类型调用析构函数,在delete时cat为Animal类型,所以只会调用Animal的析构函数。

只有当将Animal的析构函数定义为虚函数时,在delete父类指针时,才会调用子类的析构函数,保证析构的完整性。

virtual ~Animal() {
    cout << "Animal::~Animal()" << endl;
}

纯虚函数

纯虚函数:没有函数体且初始化为0的虚函数,用来定义接口规范

现有以下一段代码

struct Animal {
    virtual void speak() {
        cout << "Animal::speak()" << endl;
    }

    virtual void run() {
        cout << "Animal::run()" << endl;
    }
};

struct Pig : Animal{
    void speak() {
        cout << "Pig::speak()" << endl;
    }

    void run() {
        cout << "Pig::run()" << endl;
    }
};

struct Dog : Animal {
    void speak() {
        cout << "Dog::speak()" << endl;
    }

    void run() {
        cout << "Dog::run()" << endl;
    }
};

struct Cat : Animal {
    void speak() {
        cout << "Cat::speak()" << endl;
    }

    void run() {
        cout << "Cat::run()" << endl;
    }
};

子类都重写了父类中方法的实现,而且父类Animal自己实现的函数都有自己的实现,不过这种在某些情况下有一点不合理,Animal是一种比较抽象的概念,子类Cat,Dog,Pig才是比较具体的概念,这些类有具体的实现,是非常明确的。所以Animal类不应该有实现。

struct Animal {
    virtual void speak() = 0;

    virtual void run() = 0;
};

这种情况下,可以将不需要实现的函数,应该声明为纯虚函数。C++中的纯虚函数类似于Java中的抽象类/接口,类似于Objective-C中的协议

抽象类

抽象类有以下特点

  1. 含有纯虚函数的类,不可以实例化,不可以创建对象
    所以在上面定义的Animal类,是不可以创建对象的。

    Animal anim;//报错
    
  2. 抽象类也可以包含非纯虚函数,成员变量

  3. 如果父类是抽象类,子类没有完全重写纯虚函数,那么子类依然是抽象类

demo下载地址

文章完。

相关文章

  • 16-虚表

    虚表 在上一篇文章中,对虚函数进行了讨论,Animal的子类继承与Animal,如Cat,在Animal中定义了两...

  • 第二周(Geek Band)

    对象模型 1、vptr和vtbl(虚函数与虚表) 调用虚函数vfun,通过虚指针vptr找到虚表vtbl,通过虚表...

  • 虚表

    title: 虚表date: 2019-12-04 11:07:52categories:- SQLtags:- ...

  • 虚表、虚函数

    什么是虚函数? 使用 virtual 关键字修饰的函数即为虚函数,virtual 关键字只能对类中的非静态函数使用...

  • C++——虚函数表,常见问题,RTTI,typecast

    一、虚表 函数指针数组虚表的位置 override就是子类写的虚函数将父类的虚函数覆盖 虚表是在对象生成的时候才有...

  • 16-哈希表

    TreeMap分析 时间复杂度(平均)添加,删除,搜索:O(logn) 特点Key必须具备可比较性元素的分布是有序...

  • Part2_Week2(boolan)

    vptr和vtbl:如果类中包含虚函数,则其对象中包含一个虚指针,虚指针指向一个虚表,虚表指向虚函数的定义。虚函数...

  • 表虚事实

    文/风儿 昨夜,做了一个梦 。 01、 二十多个春冬来,家人从来没有很频繁的出现在我的梦里面。不管是远出还是许久未...

  • 徒有虚表

    老公打来电话说单位发了盒中秋月饼,这本也不稀奇,因为一年一度中秋佳节要来临了,为感谢员工的辛劳和给员工的节日问候,...

  • c++虚函数与虚表初步

    虚指针与虚表 虚表和虚函数是为了实现动态多态的机制,由编译器实现 当一个类本身定义了虚函数,或其父类有虚函数时,编...

网友评论

      本文标题:16-虚表

      本文链接:https://www.haomeiwen.com/subject/wbuiictx.html