十分钟精通C++虚函数表

C++实现多态,非常重要的概念,就是虚表指针和虚表。多态,就是父类(基类、接口类)指向子类(派生类、实现类)对象,是面向对象设计的基本特质之一。
ps:本文所有例子,运行在x64环境下,编译器 g++ -std=c++11

  1. 从一个简单的例子开始认识虚指针
class Base {
    
    
   public:
    Base() {
    
    }
    ~Base() {
    
    }
    virtual void function() {
    
    }
    //virtual void function2() {}
};

sizeof(Base)我们发现结果是8,其实这是一个指针的长度。
那么如果是两个虚函数呢?答案也是8。占据这块内存的实际是虚表指针_vptr。

int main() {
    
    
    Base b;
    cout << sizeof(Base) << endl;//8
    cout << sizeof(b._vptr) << endl;//8
    cout << b._vptr << endl;
    cout << typeid(b._vptr).name() << endl;
    return 0;
}

虚表指针指向了一个虚表,虚表中存放着类中的虚函数。
如果类中存在虚函数,那么构造对象的时候,就会生一个虚表指针指向虚表,虚表中存放虚函数。
所以,要注意的一点,在类的构造函数中不要调用虚函数,这是一个危险的动作,因为很可能它还未初始化完毕。

  1. 子类中的虚表指针
    子类会继承父类的虚标指针吗? 答案是会,_vptr就像一个隐藏的指针一样会被继承下去。
class Derived : public Base {
    
    
   public:
    virtual void function() {
    
    }
    virtual void function3() {
    
    }
};
int main() {
    
    
    Derived d;
    if (d.Base::_vptr == d._vptr) {
    
    
        std::cout << "yes" << std::endl;  // yes
    }
    return 0;
}

如果是多继承呢? 子类会继承多个虚表指针。我们通过下面的例子可以看到,_vptr继承自第一个基类。

class Base2 {
    
    
   public:
    virtual void function4() {
    
    }
};

class Derived : public Base, public Base2 {
    
    
   public:
    virtual void function() {
    
    }
    virtual void function3() {
    
    }
};
int main() {
    
    
    Derived d;
    Derived d1;
    if (d.Base::_vptr == d._vptr) {
    
    
        std::cout << "yes" << std::endl;  // yes
    }
    if (d1._vptr == d._vptr) {
    
    
        std::cout << "yes" << std::endl;  // yes
    }

    cout << sizeof(d) << endl;  // 16
    return 0;
}

_vptr继承自第一个基类????这个说法很不严谨。如上所示,实际上d和d1的_vptr内容一样的,所有Derived对象共享_vptr所指向的地址,即虚函数表

另外,那么虚表指针_vptr存放在哪里?作为第一个元素,存放在类对象的最前面。因为要考虑继承,所以最好的位置,就是首位。

    cout << &(d._vptr) << endl;
    cout << &d << endl;//地址相同
  1. 虚函数表
    那么怎么通过虚表指针访问虚函数表呢?这个其实很简单,但很容易给复杂化。
#include <iostream>
#include <typeinfo>
using namespace std;

class Base {
    
    
   private:
   public:
    Base() {
    
    }
    ~Base() {
    
    }
    virtual void function() {
    
     std::cout << "Base::function" << std::endl; }
    virtual void function1(int num) {
    
    
        std::cout << "Base::function1 - " << num << std::endl;
    }
};

class Derived : public Base {
    
    
   public:
    void function() override {
    
     std::cout << "Derived::function" << std::endl; }
    virtual void function2() {
    
     std::cout << "Derived::function3" << std::endl; }

   private:
    virtual void function3() {
    
     std::cout << "Derived::function5" << std::endl; }
};

//一个自定义类型,这个类型用来定义一类函数,
//一类入参为空,范围值为void的函数
using func_type = void (*)();
using func_type_num = void (*)(Derived *, int);
// 相当于下面这种c语言的写法
// typedef void (*Fun)(void);

int main() {
    
    
    Derived *d2 = new Derived();

    printf("d2->_vptr:\t%x\n", d2->_vptr);           //这是虚指针
    func_type *vt_tbale = (func_type *)(d2->_vptr);  //做虚表类型转换

    for (int i = 0; i < 4; i++) {
    
    
    
        if (i == 1) {
    
    
            func_type_num f = (func_type_num)vt_tbale[i];
            f(d2, 100);
            printf("The %dst func addr\t:%x\n", i, f);
        } else {
    
    
            func_type f = vt_tbale[i];
            //这是虚函数表中第i个函数的地址
            f();
            printf("The %dst func addr\t:%x\n", i, f);
        }
    }

    return 0;
}

outputs:

d2->_vptr:	400dd0
Derived::function
The 0st func addr	:400be6
Base::function1 - 100
The 1st func addr	:400bac
Derived::function3
The 2st func addr	:400c10
Derived::function5
The 3st func addr	:400c3a

首先,我们把虚函数表看做成一个函数数组,那么 func_type *vt_tbale = (func_type *)(d2->_vptr); 转换一下虚函数指针类型方便我们后续操作。后面再通过下标就可以访问继承过来的函数。实际上,函数继承的是访问权限。
另外, func_type_num是带参数函数的一个类型,第一位必须是Derived *, 实际上类的成员函数写成一般函数,第一个参数也是类指针类型,其实就是 this指针 的类型。当然如果是

virtual void function1(int num) const{
    
    } 
这种就要改写成 
using func_type_num = void (*)(const Derived *, int);

那么一个问题来了,私有虚函数可不可以通过虚表访问?我们把function3设为私有之后,得出的答案是可以。所以, 虚函数私有,非常不安全!!!

  1. 再探多继承问题
  • 如果这个基类是继承顺序中的第一个基类,那么继承下来的虚函数通过_vptr来管理。
  • 其他基类的虚函数,通过继承下来的d.Base2::_vptr来管理。
  • 如果派生类中,重载了某个基类的某个虚函数,那么这个虚函数通过_vptr来管理。
    已验证过,大家尤其要注意第三条。
  1. 总结
    另外,网上博客有很多下面的这种的写法, 我起初看的时候,真的很迷糊,C风格的指针转换搞得很浆糊。直到我脱离开这种思维,侯捷老师是真的大神。这块知识他虽然没有细讲,但他讲vptr时那张ppt图给了我很深的印象。
{
    
    
    typedef void (*Fun)(void);
    Base b;
    Fun pFun = NULL;
    cout << "虚函数表地址:" << (int *)(&b) << endl;
    cout << "虚函数表 — 第一个函数地址:" << (int *)*(int *)(&b) << endl;
    // Invoke the first virtual function
    pFun = (Fun) * ((int *)*(int *)(&b));
    pFun();
    (Fun) * ((int *)*(int *)(&b) + 0);  // Base::f()
    (Fun) * ((int *)*(int *)(&b) + 1);  // Base::g()
    (Fun) * ((int *)*(int *)(&b) + 2);  // Base::h()
}

标题,确实写得很标题党,但确实是走了很多坑,才写出上面这些简单的例子。
中间如有错误,欢迎大家指正。

最后三个误区再强调一遍

  • 在类的构造函数中不要调用虚函数,这是一个危险的动作
  • 所有同类型对象共享虚函数表
  • 虚函数私有,非常不安全

猜你喜欢

转载自blog.csdn.net/niu91/article/details/109697261