c++ 虚函数,虚表相关总结

 更新时间:2021年03月01日 10:35:59   作者:程序员杨小哥  
这篇文章主要介绍了c++ 虚函数,虚表的的相关资料,帮助大家更好的理解和学习使用c++,感兴趣的朋友可以了解下

面向对象,从单一的类开始说起。

class A
{
private:
    int m_a;
    int m_b;
}; 

这个类中有两个成员变量,都是int类型,所以这个类在内存中占用多大的内存空间呢?

sizeof(A), 8个字节,一个int占用四个字节。下图验证:

这两个数据在内存中是怎样排列的呢?

原来是这样,我们根据debug出来的地址画出a对象在内存的结构图

如果 class A 中包含成员函数呢? A 的大小又是多少?

class A
{
public:
    void func1() {}    
private:
    int m_a;
    int m_b;
}; 

直接告诉你答案,类的成员函数多大? 没人能回答你,并且不是本文的重点,类的成员函数是放在代码区的,不算在类的大小内。

类的对象共享这一段代码,试想,如果每一个对象都有一段代码,光是存储这些代码得占用多少空间?所以同一个类的对象共用一段代码。

共用同一段代码怎么区分不同的对象呢?

实际上,你在调用成员函数时,a.func1() 会被编译器翻译为 A::func1(&a),也就是A* const this, this 就是 a 对象的地址。

所以根据this指针就能找到对应的数据,通过这同一段代码来处理不同的数据。

接下来我们讨论一下继承,子类继承父类,将会继承父类的数据,以及父类函数的调用权。

以下的测试可以验证这个情况。

class A
{
public:
    void func1() { cout << "A func1" << endl; }
private:
    int m_a;
    int m_b;
};

class B : public A
{
public:
    void func2() { cout << "B func2" << endl; }
private:
    int m_c;
};

int main(int argc, char const* argv[])
{
    B b;
    b.func1();
    b.func2();
    return 0;
} 

输出:

// A func1
// B func2 

那么对象b在内存中的结构是什么样的呢?

继承关系,先把a中的数据继承过来,再有一份自己的数据。

每个包含虚函数的类都有一个虚表,虚表是属于类的,而不是属于某个具体的对象,一个类只需要一个虚表即可。同一个类的所有对象都使用同一个虚表。

为了指定对象的虚表,对象内部包含指向一个虚表的指针,来指向自己所使用的虚表。为了让每个包含虚表的类的对象都拥有一个虚表指针,编译器在类中添加了一个指针,*__vptr,用来指向虚表。这样,当类的对象在创建时便拥有了这个指针,且这个指针的值会自动被设置为指向类的虚表。

class A
{
public:
    void func1() { cout << "A func1" << endl; }
    virtual void vfunc1() { cout << "A vfunc1" << endl; }
private:
    int m_a;
    int m_b;
}; 

cout << sizeof(A);, 输出12,A中包括两个int型的成员变量,一个虚指针,指针占4个字节。

a的内存结构如下:

虚表是一个函数指针数组,数组里存放的都是函数指针,指向虚函数所在的位置。

对象调用虚函数时,会根据虚指针找到虚表的位置,再根据虚函数声明的顺序找到虚函数在数组的哪个位置,找到虚函数的地址,从而调用虚函数。

调用普通函数则不像这样,普通函数在编译阶段就指定好了函数位置,直接调用即可。

class A
{
public:
    void func1() { cout << "A func1" << endl; }
    virtual void vfunc1() { cout << "A vfunc1" << endl; }
private:
    int m_a;
    int m_b;
};

class B : public A
{
public:
    void func1() { cout << "B func1" << endl; }
    virtual void vfunc2() { cout << "B vfunc2" << endl; }
private:
    int m_a;
}; 

像这样,B类继承自A类,B中又定义了一个虚函数vfunc2, 它的虚表又是怎么样的呢?

给出结论,虚表如下图所示:

我们来验证一下:

A a;
B b;
void(*avfunc1)() = (void(*)()) *(int*) (*(int*)&a);
void (*bvfunc1)() = (void(*)()) *(int*) *((int*)&b);
void (*bvfunc2)() = (void(*)()) * (int*)(*((int*)&b) + 4);
avfunc1();
bvfunc1();
bvfunc2();

来解释一下代码: void(*avfunc1)() 声明一个返回值为void, 无参数的函数指针 avfunc1, 变量名代表我们想要取A类的vfunc1这个虚函数。

右半部分的第一部分,(void(*)()) 代表我们最后要转换成对应上述类型的指针,右边需要给一个地址。

我们看 (*int(*)&a), 把a的地址强转成int*, 再解引用得到 虚指针的地址。

*(int*) (*(int*)&a) 再强转解引用得到虚表的地址,最后强转成函数指针。

同理得到 bvfunc1, bvfunc2, +4是因为一个指针占4个字节,+4得到虚表的第二项。

覆盖

class A
{
public:
    void func1() { cout << "A func1" << endl; }
    virtual void vfunc1() { cout << "A vfunc1" << endl; }
private:
    int m_a;
    int m_b;
};

class B : public A
{
public:
    void func1() { cout << "B func1" << endl; }
    virtual void vfunc1() { cout << "B vfunc1" << endl; }
private:
    int m_a;
}; 

子类重写父类的虚函数,需要函数签名保持一致,该种情况在内存中的结构为:

多态

父类指针指向子类对象的情况下,如果指针调用的是虚函数,则编译器会将会从虚指针所指的虚函数表中找到对应的地址执行相应的函数。

子类很多的话,每个子类都覆盖了对应的虚函数,则通过虚表找到的虚函数执行后不就执行了不同的代码嘛,表现出多态了嘛。

我们把经过虚表调用虚函数的过程称为动态绑定,其表现出来的现象称为运行时多态。动态绑定区别于传统的函数调用,传统的函数调用我们称之为静态绑定,即函数的调用在编译阶段就可以确定下来了。

那么,什么时候会执行函数的动态绑定?这需要符合以下三个条件。

  • 通过指针来调用函数
  • 指针 upcast 向上转型(继承类向基类的转换称为 upcast)
  • 调用的是虚函数

为什么父类指针可以指向子类?

子类继承自父类,子类也属于A的类型。

最后通过一个例子来体会一下吧:

class Shape
{
public:
    virtual void draw() = 0;
};

class Rectangle : public Shape
{
    void draw() { cout << "rectangle" << endl; }
};

class Circle : public Shape
{
    void draw() { cout << "circle" << endl; }
};

class Triangle : public Shape
{
    void draw() { cout << "triangle" << endl; }
};


int main(int argc, char const *argv[])
{
    vector<Shape*> v;
    v.push_back(new Rectangle());
    v.push_back(new Circle());
    v.push_back(new Triangle());
    for (Shape* p : v) {
        p->draw();
    }
    return 0;
} 

有些话是大白话,哈哈,如果这篇文章写的不错,解决了你的疑惑的话,点个赞再走吧!

不对的地方也请指出来,大家一起学习进步。

以上就是c++ 虚函数,虚表相关总结的详细内容,更多关于c++ 虚函数,虚表的资料请关注脚本之家其它相关文章!

相关文章

  • C语言中getchar和putchar的使用方法详解

    C语言中getchar和putchar的使用方法详解

    我们知道scanf函数可以从键盘输入信息,而printf则可以输出信息,同样地,getchar和putchar也有同样的功能,下面我来给大家介绍putchar和getchar的使用方法,需要的朋友可以参考下
    2023-08-08
  • C++如何用数组模拟链表

    C++如何用数组模拟链表

    大家好,本篇文章主要讲的是C++如何用数组模拟链表,感兴趣的同学赶快来看一看吧,对你有帮助的话记得收藏一下
    2022-01-01
  • C语言中#pragma pack(1)的用法与注意点

    C语言中#pragma pack(1)的用法与注意点

    #pragma用于指示编译器完成一些特定的动作,下面这篇文章主要给大家介绍了关于C语言中#pragma pack(1)的用法与注意点的相关资料,文中通过实例代码介绍的非常详细,需要的朋友可以参考下
    2023-02-02
  • C语言进阶学习之指针

    C语言进阶学习之指针

    关于指针,其是C语言的重点,C语言学的好坏,其实就是指针学的好坏。其实指针并不复杂,学习指针,要正确的理解指针,本片文章能给就来学习一下
    2021-09-09
  • C语言中设置进程优先顺序的方法

    C语言中设置进程优先顺序的方法

    这篇文章主要介绍了C语言中设置进程优先顺序的方法,包括setpriority()函数和getpriority()函数以及nice()函数,需要的朋友可以参考下
    2015-08-08
  • 适合新手小白DEV C++的使用方法

    适合新手小白DEV C++的使用方法

    Dev-C++是一个Windows环境下C/C++的集成开发环境(IDE),它是一款自由软件,遵守GPL,下面这篇文章主要给大家介绍了关于适合新手小白DEV C++的使用方法,文中通过图文介绍的非常详细,需要的朋友可以参考下
    2023-02-02
  • C++ 程序抛出异常后执行顺序说明

    C++ 程序抛出异常后执行顺序说明

    这篇文章主要介绍了C++ 程序抛出异常后执行顺序说明,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-02-02
  • C语言 typedef:给类型起一个别名

    C语言 typedef:给类型起一个别名

    本文主要介绍C语言 typedef,这里整理了相关资料及简单示例代码帮助大家学习理解,有兴趣的小伙伴可以参考下
    2016-08-08
  • C语言数据结构与算法之单链表

    C语言数据结构与算法之单链表

    单链表是一种链式存取的数据结构,用一组地址任意的存储单元存放线性表中的数据元素。本文将为大家介绍C语言中单链表的基本概念与读取数据元素,需要的可以参考一下
    2021-12-12
  • C++使用数组来实现哈夫曼树

    C++使用数组来实现哈夫曼树

    给定N个权值作为N个叶子结点,构造一棵二叉树,若该树的带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree)。哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近
    2022-05-05

最新评论