再探虚表

1、是一个类一个虚表 还是一个对象一个虚表?  2、虚表里面是否只有函数指针?

序 :以下测试和结论都是以单继承的基础。

问题1:虚表是什么,存在哪里,与类和其实例化对象有啥关联

            众所周知,虚表实际是一个个函数指针的集合,该集合是个数组  。那它存在哪里,与对象、类是什么关系。今天我们从代码层面上来一探究竟。

//第一个示例,定义个含有虚函数的类型,并用之示例化

class baseClass {
public:
    virtual void fun1() {
        std::cout << "base::fun1()" << std::endl;
    }
    virtual void fun2() {
        std::cout << "base::fun2()" << std::endl;
    }
    void fun3() {
        std::cout << "base::fun3() non-virtual" << std::endl;
    }

protected:
    int _b = 0;
};

int main()
{
    baseClass base1;
    baseClass base2;

   int* pVFT_base2 =   ((int*)&base2);

    getchar();
    return 0;
}

运行后监视两个实例化的对象,及base2首地址的内容

    从图中明确看出 base1 base2 两个对象拥有了一样的虚表,也即两个对象同用了同一张虚表,而名为pVFT_base2的指针为base2的首地址,其里面的内容为虚表的指针的地址。

如此得出结论:

1、虚表含一个函数指针的数组,存在于数据段(可定义个全局变量,查看其地址,会发现和虚表的起始地址很接近)。

2、含虚函数的类的首个4字节的地址存的是一个指针,该指针内容及为虚表的起始地址,(行话是这个指针指向虚表起始地址),这一点铺垫了后面的测试。
3、 一个类会持有一个虚表,与实例化对象无关,同一个类的所有实例化的对象持有的虚表共享同一个虚表,和静态成员变量类似。这一点是多态实现的最重要的借助点。

问题二:多态基于继承关系中虚表实现,那虚表在继承关系中是如何维护的?

在上面的例子里,添加个派生类,对父类不做任何动作,并也用之实例化一对象,代码如下

class baseClass {
public:
    virtual void fun1() {
        std::cout << "base::fun1()" << std::endl;
    }
    virtual void fun2() {
        std::cout << "base::fun2()" << std::endl;
    }
    void fun3() {
        std::cout << "base::fun3() non-virtual" << std::endl;
    }

protected:
    int _b = 0;
};

class deriveClass : public baseClass {
public:

    virtual void fun5() {
        std::cout << "deriveClass::fun5()" << std::endl;
    }

protected:
    int _d = 0;
};

 

int main()
{
    baseClass base1;
    baseClass base2;
    deriveClass derive;

   int* pVFT_base2 =   ((int*)&base2);

    getchar();
    return 0;
}

再讲 示例话的derive加入的监视中,一探究竟。

现在在 deriveClass重载func2接口,再看一遍监视。

从两张图中对比可知。含有虚函数的类被继承后会有如下几个动作

1、派生类会从其父类(为啥是父类,而不是基类),拷贝其全部的虚表元素到自己的虚表里面

2、如果派生类重载 了接口,则会将自己虚表里面对应偏移的接口地址改写成重载后的接口地址


为验证是拷贝的父类还是基类,再加层继承,从deriveClass 派生出ThirsDeriveClass,不做任何实际动作,再实例化一对象。看监视器。

图中看出 fun2 是其父类的地址,而非基类的。上面的结论正确。但是奇怪的问题来了,现在在deriveClass类中添加个虚函数fun5 ,为啥没在虚表里面展示出来。难道还有另一张虚表存在么,这个猜想肯定是错的,单继承的类只有一个虚表,那真相是没展示出来。但func5确确实实存在在虚表里面,而且在虚表中的位置应该紧随继承而来的虚函数后面。

验证方法一:(void**)0x010eed48,10 手动调整监控内容的显示个数。前面的地址为虚表起始地址

方法二:我们个测试看看能不能通过虚表的偏移将func5调用起来。。来验证我们的猜想

class baseClass {
public:
    //方便测试都是返回void 参数void 
    virtual void fun1() {
        std::cout << "base::fun1()" << std::endl;
    }
    virtual void fun2() {
        std::cout << "base::fun2()" << std::endl;
    }
    void fun3() {
        std::cout << "base::fun3() non-virtual" << std::endl;
    }

protected:
    int _b ;
};

class deriveClass : public baseClass {
public:
    virtual void fun2() {
        std::cout << "deriveClass::fun2()" << std::endl;
    }
   virtual  void fun5() {
        std::cout << "deriveClass::fun5()" << std::endl;
    }
protected:
    int _d ;
};

class ThirsDeriveClass : public  deriveClass {
public:
    virtual  void fun5() {
        std::cout << "ThirsDeriveClass::fun5()" << std::endl;
    }
};

void DoShowClassName(baseClass& class1) {
    std::cout << typeid(class1).name() << std::endl;
}

typedef void (__stdcall *PVFT)();  //函数指针
void ImitateCall(baseClass& class1) {
    DoShowClassName(class1);

    //1、取得该对象的地址,转存int(前4个字节)。并取出该值(存储的就是虚表的起始地址)
    //2、 将该地址转换成函数指针 
    PVFT* pVFT = (PVFT*)(*((int*)&class1));
    while (*pVFT) {
        (*pVFT)();
        pVFT += 1;
    }
    std::cout << std::endl;
}

int main() {
    baseClass base1;     
    baseClass base2;
    deriveClass derive;
    ThirsDeriveClass  ThirsDerive;

    int* pVFT_base2 = ((int*)&base2);
    int* pVFT_base1 = ((int*)&base1);

    ImitateCall(base1);

    ImitateCall(ThirsDerive);

  //  ImitateCall(derive);
    getchar();
    return 0;
}

 

运行的结果如图,确实可以通过编译调用到派生类自己的虚函数,故此印证了我们的猜测是正确的。

代码中的主要思路如下两点,那c++的多态是不是这样实现的呢 ,那这里的印证我稍后继续。

1、从当前的指针或是对象的前四个字节(32位机),得到虚表起始指针。而实际上指向虚表的指针是和类型绑定的,故只需要知道当前的类型基本也就获得这个指向虚表的指针了。

2、使用偏移 调用 对应的虚函数。

问题三:那么至此多态的实现,所有的症结就在于怎么识别运行中的对象是什么类型的。

   代码中还使用个判断就讲指针真实对象的类名打印出来了,但是这是咋实现的,这里就涉及到运行时类型识别:RTTI。

RTTI ----运行时类型识别主要由两个操作符实现,讲这两个操作符前需要看个名type_info类,它精确定义随编译器不同而略有差异。但C++标准要求其需要提供一下四个操作.

1、t1 == t2 表示两者是同一类型 

2、!= 

3、t1.name() 返回一个其类型名称的字符串

4、t1.before(t2)  t1 是否在在t2之前。

RTTI ----运行时类型识别涉及的两个操作符:

typeid :用于返回表达式的类型。入参可以是对象,也可以是类型的名字。其结果是一个常量对象的应用。类型是标准库类型type_info。

dynamic_cast运算符 : 用于将基类指针或者引用转换才派生类的指针或者引用。

那是不是这样就是最终的结果呢。我们是不是该结案了呢。其实不然,类型存着哪里有怎么获取呢。

问题4:言归正传 虚表到底是个啥,有个啥

那我们看个多继承的例子:https://www.cnblogs.com/zhehan54/p/5582136.html

结论

     在每个虚表起始地址的前面里面4字节存着的即为该类型继承的父类或是自身的类型的typeinfo,也正是多态类型的推测即是通过基类指针或引用指向的对象(子对象)的虚函数表获得的。 

      在多重继承和虚拟继承的情况下,一个类有n(n>1)个虚函数表,该类的对象也有n个vptr,分别指向这些虚函数表,但是一个类的所有的虚函数表的索引为-1的项的值(type_info对象的地址)都是相等的,即它们都指向同一个type_info对象,这样就实现了无论使用了哪一个基类的指针或引用指向其派生类的对象,都能通过相应的虚函数表获取到相同的type_info对象,从而得到相同的类型信息,而得到了类型信息,也就和获得了虚表地址没区别了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值