C++ 笔记 —>深度剖析多态(虚函数表)下

【摘要】在这篇文章中,我引出了虚基表的概念,但是由于篇幅原因,我没有深度去探索它,于是我分成了上下两篇。在这篇博客中,我会将继承时容易出现的几种情况都一一列出来并且进行虚表的分析,包括虚表的打印等等。

上一篇博客的链接:https://blog.csdn.net/zb1593496558/article/details/80113176
这篇博客会讲到的有以下几点。
1.承接上文,先介绍一下虚表
2.单继承下虚表的使用
3.多继承下虚表的使用
4.最复杂的情况(菱形继承下虚表的使用)
注:
菱形继承由于增加了程序的复杂程度,其实用到的并不多,在这里我只是想要给你们讲一下复杂情况下的虚表是如何实现的,万一以后用得到呢
.承接上文,先介绍一下虚表

1.当你在一个类中声明了至少一个虚函数时,类中就会产生一个对应的 vfptr 指针,用来指向生成的这张有且仅有一张的虚表,你可以这么想,这张表就像是每个人的档案袋,不管有什么东西我都可以放进去,但是我只有这一个,没办法再增加一个。

2.虚表里面存着的是虚函数的地址,因此我们通过这个 vfptr 指针可以找到虚表的首地址,找到地址之后,由于虚函数在虚表中的存储都是按顺序存储的,这样也方便我们对函数进行调用。

3.如果一张虚表中存在多个函数的地址,那么通常编译器的做法是,对象需要调用第几个函数就在首地址的基础上每次加 4 (为什么是4大家应该都清楚吧,我这是32位的机器,所以一个地址占用四个字节)

4.虚表的实现离不开 this 指针,因此,像 static 函数就无法使用 virtual 来实现多态。


5.虚表的缺点(这可能是你们不想看到的,但是事实就是它存在缺点)这也可以说是动态多态和普通函数的一个缺点对比。列入下面几点。
1.普通函数由于在编译时已经确定了该调用那个函数,因此它的代码会很少,而动态多态因为是在运行时才确定的,所以代码会相应的加长,怎么知道的呢
(一会我用反汇编给你们验证)
2.虚函数的结构更复杂,也会增加内存大小:这是肯定的啊,你在类中增加了一个指针,一个32位的机器,一个指针大小是4个字节,放在64位的机器一个指针就是8个字节了,我们平时当然不会实现那么多的类,但是要是在实际工作中,定义1000甚至上万个类时,那么消耗也就会增大很多了。
3.效率会变低:这点你们应该也可以理解。虚函数由于多了一些操作(先是用一个指针去寻找虚表的地址,然后再根据虚表的首地址去间接地调用虚函数)这一系列的操作本身就会使得代码效率变低(针对普通函数而言)
有这么多缺点,但是我们还是需要用到它,为什么呢?

   我的理解是这样的。在C++ 中,我们有时候必须用到多态,这对于程序员来说或许代码会变得简洁易懂,也更方便维护,虽然对于编译器可能负担会增大,但是,现在机器的运行速度着实很快了,这点消耗我们
是可以完全忽略的,并且,作为C++ 的四大特性之一,实现它本来就会有所争议,我们只能建议说是在适当的地方使用它而不要满篇幅地滥用。
说着介绍一下,又说了这么多,啰嗦了,那么下面我们就深入内部一探究竟吧。
注(我上面所说的都是自己的总结,对于刚开始学C++的同学来说很好理解,希望你们可以细看一下,毕竟这些都是我花了一周才总结出来的)
.单继承下虚表的使用
单继承的情况,我也就不多说了,直接上代码吧。
class A
{
public:
	 virtual void f1()
	{
		cout<<"A::f1"<<endl;
	}

	
private:
	int _a;
};

class B:public A
{
public:
	virtual void f1()
	{
		cout<<"B::f1"<<endl;
	}
	
private:
	int _b;
};

接下来我们就来看看虚函数在内部到底是如何调用的?
首先,我们根据监视窗口中B::f1的地址,转到反汇编,然后输入这个地址,发现这个地址并没有执行call指令而是 jmp指令,具体分析看图呗。

虚表的真实情况我们已经算是初步了解了。那么我们来研究一些比较难得情况吧。多继承下虚表的使用,这一次,我们需要做的是,把虚表真实的打印出来,方便我们更加直观的理解虚表的内部结构。
 打印虚表的代码如下(其实打印虚表就是打印一下虚表中的函数地址,为了方便分析,我们每次调用的函数都输出一段话,这样就可以分清楚到底是哪个函数了。
代码如下:
 #include<iostream>
using namespace std;

typedef void(*VFunc)();//函数指针

class A
{
public:
    virtual void fun()
    {
        printf("A::virtual void fun(int n)\n");
    }
private:
    int _a;
};

void PrintVTable(int* vtable)
{
    int i = 0;
    //打印虚表,最后一个元素是0,因此不打印
    //并将每个元素(即虚函数指针)通过VFunc调用一次
    for (i = 0; vtable[i] != 0; i++)
    {
        printf("vtable[%d]:%p\n", i, vtable[i]);
        VFunc f = (VFunc)vtable[i];
        f();
    }
    //验证虚函数表的最后一个元素为0
    printf("vtable[%d]:%p\n", i, vtable[i]);
}

int main()
{
    A a;
    PrintVTable((int*)(*(int*)&a));
    return 0;
}       
结果如下

从这张虚表中,我们就可以看出来函数 fun 在表中的存放是第一个,现在我们把代码稍微改变一下,在类A中加入一个虚函数fun 1(),这时候我们可以得到些什么呢?
很明显,两个函数是按照定义的先后顺序存放在表中的,并且都是存放在一张表中的,这也验证了之前所说的一个类中不管定义多少个虚函数都只用同一张表。
接下来,我们看一下最复杂的情况,菱形继承的虚表内部结构。
照例先把代码放这里
class A
{
public:
    virtual void fun()
    {
        printf("A::virtual void fun(int n)\n");
    }

private:
    int _a;
};

virtual class B:public A
{
public:
    virtual void fun()
    {
        printf("B::virtual void fun(int n)\n");
    }

private:
    int _a;
};

virtual class C:public A
{
public:
    virtual void fun()
    {
        printf("C::virtual void fun(int n)\n");
    }

private:
    int _a;
};


class D:public B,public C
{
public:
    virtual void fun()
    {
        printf("D::virtual void fun(int n)\n");
    }
    virtual void fun1()
	{
		printf("D::virtual void fun1(int n)\n");
	}
private:
    int _a;
};

结果如下

结构体系如下
从这两张图中我们可以发现,由于菱形继承的特点,B C D 三个类都继承了 A的虚表,但是由于函数重写,表中都没有A 中的函数,而是各自的函数。
总结:
现在我们已经知道虚函数的多态性就是通过虚表中的函数指针进行指向实现的,并且相同的类对象共用一张表,我们还应该直到虚表的底层是如何实现的,这就告诉我们,必须深入的探索模型,只有懂得了底层的机制,我们才可以真正的理解和使用好这门语言。我还有一点小建议就是学习一点简单的汇编语言,这对我们分析代码也有好处。好啦,今天就先说这么多啦,欢迎私信讨论哦,我经常在的。

                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  
                                                                                                                                           

猜你喜欢

转载自blog.csdn.net/zb1593496558/article/details/80141106