虚函数看起来是个玄之又玄的东西,但其实特别简单!了解了虚函数的内部实现,关于虚函数的各种问题都不在话下啦!

1. 知识储备

阅读这篇文章,你需要事先了解以下几个概念:

  • 什么是继承?

  • 什么是虚函数?

    在C++中,在基类的成员函数声明前加上关键字 virtual 即可让该函数成为 虚函数,派生类中对此函数的不同实现都会继承这一修饰符。

  • 为什么需要虚函数?

这涉及到面向对象程序设计中多态动态绑定的概念。

  • 进程的内存分布

如果你已经完全了解上述概念,那么这篇文章很适合你去深入了解虚函数~

2. C++中类的memory Layout

为了更好地理解虚函数的内部实现,我们首先需要知道,C++的类中成员函数和成员变量在内存中的空间分配。

1. 我们从最普通的一个类说起~
class X {    intx;    floatxx;    static int  count;public:    X() {}    ~X() {}    void printInt() {}    void printFloat() {}  static void printCount() {}};

如果我们在这个程序中定义了这个类的一个对象,那么这个类的内存分布如下图所示:

类的非静态成员变量会被保存在栈上,类的静态成员变量被保存在数据段,而类的成员函数被保存在代码段。

 class X { intx; floatxx; static int  count; public: X() {} ~X() {}void printInt() {}void printFloat() {}static void printCount() {}};
2. 含有虚函数的基类的内存分布

如果一个类中含有虚函数,那么为了实现动态绑定,编译器会在原来的代码中插入(augment)一个新的成员变量--一个成员指针 vptr, 这个指针指向一张包含所有虚函数的函数指针表 vtable. 当我们调用虚函数时,实际上是通过vptr这个指针来调用函数指针表vtable里面的某个函数指针来实现函数调用。

一般而言,这张vtable会在数据段,是静态的,每一个类仅有一张表。但是这不是死规定,这是由编译器的实现决定的。vptr这个指针和成员变量一致,存在在堆栈段,是每一个对象都会有的。

vtable中的第一个entry包含了当前类及其基类的相关信息,其余entry是函数指针。

现在来看一个例子~

class X {    int         x;    float       xx;    static int  count;public:    X() {}    virtual ~X() {}    virtual void printAll() {}    void printInt() {}    void printFloat() {}    static void printCount() {}};
3. 含有虚函数的子类的内存分布

此时,基类的成员变量和成员函数相当于派生类的子对象,也就是说派生类会继承基类的vptr。这时会先为基类的成员函数和成员对象分配内存空间,然后再为派生类的自己的成员变量和成员函数分配空间。vptr会指向Y这个类的vtable

如果派生类写了一个不在基类里的新的虚函数,那么这个vtable会多出一行,行内的内容是指向这个新虚函数的函数指针。

class X {    int     x;public:    X() {}    virtual ~X() {}    virtual void printAll() {}};
class Y : public X {    int     y;public:    Y() {}    ~Y() {}    void printAll() {}};
4. 含有虚函数、有多继承的子类的内存分布

有多个基类的派生类会有多个vptr, 用来指向继承自不同基类的vtable。也就是说,每一个有虚函数的基类都会有一个虚函数指针表。

我们来看一个Z类继承自X类和Y类的例子。

class X {public:    int     x;    virtual ~X() {}    virtual void printX() {}};
class Y {public:    int     y;    virtual ~Y() {}    virtual void printY() {}};
class Z : public X, public Y {public:    int     z;    ~Z() {}    void printX() {}    void printY() {}    void printZ() {}};

3. 虚函数的内部实现

了解了虚函数在内存中的分配方式后,理解虚函数的实现以及动态绑定就变得非常简单了。

这里以多继承的子类的代码为例,上代码~

class X {public:    int     x;    virtual ~X() {}    virtual void printX() { cout<<"printX() in X"<<endl; }};class Y {public:    int     y;    virtual ~Y() {}    virtual void printY() { cout<<"printY() in Y"<<endl; }};class Z : public X, public Y {public:    int     z;    ~Z() {}    void printX() { cout<<"printX() in Z"<<endl; }    void printY() { cout<<"printY() in Z"<<endl; }    void printZ() { cout<<"printZ() in Z"<<endl; }};int main(){  Y *y_ptr = new Z();  y_ptr->printY(); // OK  y_ptr->printZ(); // Not OK, Y类的虚函数表中没有printZ()函数    y_ptr->y = 3; // OK  y_ptr->z = 3;// not OK, Y类的空间中没有变量z}

所以在上述代码中,y_ptr指向的是在Z类对象中的子对象,即Y类对象在Z类中函数与变量。

注意⚠️ 此时y_ptr中的_vptr指向的是Z类对象的vtable

y_ptr->printY()这行代码,其实会被编译器翻译成如下伪代码

((y_ptr->_vptr)->_vtbl[2])();

其中y_ptr->_vptr指向Y类对象的vptr指针,vptr指针再指向虚函数表中对应的函数指针项,即((y_ptr->_vptr)->_vtbl[2]), 最后通过函数指针来实现函数调用。

由于这个_vptr指向的是Z类对象的虚函数表,所以调用的printY()函数实际上是Z类中实现的printY(),即输出"printY() in Z"。 动态绑定就这样实现了。

4. 用几个问题加深理解

沿用3中的例子,我们来看接下来的几个问题。

Q1. 如果将Z类对象赋值给Y类变量,动态绑定还会发生吗?

即如下代码中,输出是"printY() in Z"还是"printY() in Y"

Z zz;Y yy = zz;yy.printY();

答案是不会发生,输出的结果是"printY() in Y"

首先我们需要明确一个很重要的概念,对_vptr这个指针的赋值操作是在构造类对象的过程中发生的。换一句话说,当一个类的实例被创建的时候_vptr被赋值,指向该类的vtable。一旦类的实例被创建,一个类对象里面的_vptr永远不会变,永远都会指向所属类型的虚函数表。

不论是赋值操作还是赋值构造时, 只会处理成员变量,即把zz中的成员变量赋值给yy, 但是_vptr还是指向Y类的虚函数表。

Q2. 如果在基类中不声明某个函数是虚函数,在子类中重写了这个函数,动态绑定还会发生吗?

即如下代码中,输出是"printX() in Z"还是"printX() in X"

class X {public:    int     x;    virtual ~X() {}    void printX() { cout<<"printX() in X"<<endl; }};class Z : public X {public:    int     z;    ~Z() {}    void printX() { cout<<"printX() in Z"<<endl; }    void printZ() { cout<<"printZ() in Z"<<endl; }};int main(){  X *x_ptr = new Z();  x_ptr->printX(); // OK}

答案是不会发生,输出的结果是"printX() in X"。没有声明为虚函数的函数,不会被放入虚函数表中,即vtable不会保存该函数的函数指针。这时,动态绑定肯定不会发生了。

5. 总结

  1. 一般而言,虚函数表是属于一个类的(one vtable per class), 位于静态数据区,而虚函数表指针_vptr是属于一个类的对象的(one vptr per object).
  2. 一个由多继承关系的类会有多个虚函数指针。
  3. 虚函数指针的赋值操作是在构造类对象的过程中发生的,之后的赋值操作不会改变vptr的值
  4. C++标准没有定义动态绑定的具体实现方式,只是陈述了动态绑定的行为。具体的实现与编译器相关。


©著作权归作者所有:来自51CTO博客作者李季谦千的原创作品,如需转载,请注明出处,否则将追究法律责任

更多相关文章

  1. js第一课 引入方式 参数 函数
  2. Python数据分析相关面试题!Python学习教程
  3. js引入方式、变量与常量的声明与使用方式、函数与高阶函数,实例演
  4. 调用函数以及常用模块
  5. 20210104 递归
  6. 20210108 装饰器详解
  7. 20210111 装饰器之案例剖析
  8. JavaScript基础之 变量与常量的声明、函数 箭头函数
  9. 0331作业

随机推荐

  1. Android获取所有安装APP信息的详细代码
  2. android从网站获取json接口数据并放置到s
  3. Android(安卓)之Notification 用法
  4. android 退出机制
  5. Android(安卓)常用开发框架
  6. Android(安卓)CDMA分支
  7. Android 2.2 单点触摸支持的问题
  8. cocos creator 编译通不过, 出现 The "and
  9. 安卓版本和Api Level
  10. android > 布局文件 > 背景圆角