• 设为首页
  • 点击收藏
  • 手机版
    手机扫一扫访问
    迪恩网络手机版
  • 关注官方公众号
    微信扫一扫关注
    公众号

C++多态与虚函数表

原作者: [db:作者] 来自: [db:来源] 收藏 邀请

转自https://www.cnblogs.com/findumars/p/9845429.html

首先第一点:

为什么运行时多态无法在编译期进行:

比如

class A
{
  virtual void func(){...};  
};
class A1:public A
{
  void func(){...};
};

  对于这样一种最简单的形式,A1派生类对基类A中的func方法进行了重写。

在我们的程序中,假如有一个A*型指针pa:

对于pa->func(),我们并不知道它到底要调用哪个func,比如在某一行:pa=new A();pa->func();我们知道这时是调用的基类的func();

但在下一行,很可能就是pa=new A1();pa->func();这下就是调用派生类A1的func方法了。

但对于编译器来说,这两个pa->func();对它来说它完全看不出任何区别,这样就无法在编译期将这个操作的函数地址定下来,因为func的地址既可能是A里的func的地址,也可能是A1中的func的地址,对于pa->func();这样有两种可能性,语义不详的语句,编译器当然不可能将它翻译成只有一个意思的机器指令,因此编译期是无法进行这种多态行为的。

但对于静态多态:重载与函数模板

对于重载func函数,由于是通过name magling的方式实现的,因此虽然都是func函数,但由于参数不同,实际上在name magling后,他们是两个名字完全不同的函数,这样在编译器看来,是完全可以把他们翻译成不同的机器指令的,因此是在编译期进行的。

对于模板来说,一个Add函数:

因此,对于编译器来说,它也是可以将这些调用翻译成不同的机器指令的。

 

其次第二点:

基类指针只能调用基类的方法(包括派生类重写的方法),而无法调用派生类新增的虚函数方法,尽管新增的虚函数很可能也写在这个基类的虚指针所指向的虚表中。

对于A* a=new B();

a->displaya()是可行的,但a->displayb()是不可行的,基类指针a只能调用A自己的方法和B重写的A中的方法。

这在逻辑上也是合理的,B是A的派生类,B是建立在A之上,再加上一部分特点所形成的的新的类,而A*型指针调用的方法应该是A与B的共性的方法,毕竟B是A的一种而A不是B的一种,A*型指针也正应该只能使用A与B的共性的部分,也就是A的方法。

class A
{
public:
	virtual void displaya()
	{
		cout<<"a"<<endl;
	}
};
class B:public A
{
public:
	virtual void displayb()
	{
		cout<<"b"<<endl;
	}
}; 

  关于运行时的多态的实现,猜测是不是这样进行的呢?(现在只是猜测,书还没看完

还是以上面func()举例子:

对于A* pa=new A();pa->func();

和A* pa=new A1();pa->func();

虽然我们不知道pa到底指向谁,是A对象亦或是A1对象,但我们有一点是确定的,那就是它们的函数都是func函数

这样如果我们将虚指针vptr放置在固定的位置,则pa->func()就会能有两种功能了。

比如pa=new A();pa->func();

这时pa指向A的起始地址,此时虚函数表中的func函数未被重写,因为是A对象,这样pa找到固定位置处的vptr,然后跳转到A的虚函数表,找到func(),然后执行。

对于pa=new A1();pa->func();

这时pa还是指向A的起始地址(因为是先放基类,在存放派生类成员),此时虚函数表中的func已经被重写了,因为指向的是A1类型的对象,这样pa找到固定位置处的vptr,然后跳转到A的虚函数表(单一继承下派生类的虚函数还是写在A的虚表里),找到func,然后执行。

这样pa->func在运行期就可以根据不同的指向对象而执行不同的功能。

class A
{
  virtual void func(){...};  
};
class A1:public A
{
  void func(){...};
};

  

虚函数表:

单继承时的虚函数表:

1、无虚函数覆盖

假如现有单继承关系如下:

class Base
{
public:
virtual void x() { cout << "Base::x()" << endl; }
virtual void y() { cout << "Base::y()" << endl; }
virtual void z() { cout << "Base::z()" << endl; }
};

class Derive : public Base
{
public:
virtual void x1() { cout << "Derive::x1()" << endl; }
virtual void y1() { cout << "Derive::y1()" << endl; }
virtual void z1() { cout << "Derive::z1()" << endl; }
};


在这个单继承的关系中,子类没有重写父类的任何方法,而是加入了三个新的虚函数。Derive类实例的虚函数表布局如图示:

  • Derive class 继承了 Base class 中的三个虚函数,准确的说,是该函数实体的地址被拷贝到 Derive 实例的虚函数表对应的 slot 之中。
  • 新增的 虚函数 置于虚函数表的后面,并按声明顺序存放。

2、有虚函数覆盖

如果在继承关系中,子类重写了父类的虚函数:

class Base
{
public:
virtual void x() { cout << "Base::x()" << endl; }
virtual void y() { cout << "Base::y()" << endl; }
virtual void z() { cout << "Base::z()" << endl; }
};

class Derive : public Base
{
public:
virtual void x() { cout << "Derive::x()" << endl; } // 重写
virtual void y1() { cout << "Derive::y1()" << endl; }
virtual void z1() { cout << "Derive::z1()" << endl; }
};


则Derive类实例的虚函数表布局为:

 

相比于无覆盖的情况,只是把 Derive::x() 覆盖了Base::x(),即第一个槽的函数地址发生了变化,其他的没有变化。

这时,如果通过绑定了子类对象的基类指针调用函数 x(),会执行 Derive 版本的 x(),这就是多态。

 

多重继承时的虚函数表

1、无虚函数覆盖

现有如下的多重继承关系,子类没有覆盖父类的虚函数:

class Base1
{
public:
virtual void x() { cout << "Base1::x()" << endl; }
virtual void y() { cout << "Base1::y()" << endl; }
virtual void z() { cout << "Base1::z()" << endl; }
};

class Base2
{
public:
virtual void x() { cout << "Base2::x()" << endl; }
virtual void y() { cout << "Base2::y()" << endl; }
virtual void z() { cout << "Base2::z()" << endl; }
};

class Derive : public Base1, public Base2
{
public:
virtual void x1() { cout << "Derive::x1()" << endl; }
virtual void y1() { cout << "Derive::y1()" << endl; }
};


对于 Derive 实例 d 的虚函数表布局,如下图:

可以看出:

  • 每个基类子对象对应一个虚函数表
  • 派生类中新增的虚函数放到第一个虚函数表的后面

注意,这也就是为什么《深度探索C++对象模型里》对于多重继承的情况下要不断调整指针的指向的原因:

比如Base2* pb2=new Derive();

由于Base2是与Base1平级的基类,因此pb2的指向要从new Derive()的起始地址调整到Base2 subobject处,因为pb2只能调用Derived与Base2的共有部分的函数,不能调用Base1部分的函数,因此要调整到Base2 subobject处,因为Base2的虚指针在此处,不然将使用Base1的虚指针进行跳转,从而不能调用Base2的方法。

而当delete pb2时又要跳转到new Derive()的起始处,因为派生类对析构函数是进行了重写的,而派生类中重写的函数将写入第一个基类,也就是Base1里的vptr所指向的虚表中,因此要调整到new Derive()首处,这样才能正确的调用析构函数。

2、有虚函数覆盖

将上面的多重继承关系稍作修改,让子类重写基类的 x() 函数:

class Base1
{
public:
virtual void x() { cout << "Base1::x()" << endl; }
virtual void y() { cout << "Base1::y()" << endl; }
virtual void z() { cout << "Base1::z()" << endl; }
};

class Base2
{
public:
virtual void x() { cout << "Base2::x()" << endl; }
virtual void y() { cout << "Base2::y()" << endl; }
virtual void z() { cout << "Base2::z()" << endl; }
};

class Derive : public Base1, public Base2
{
public:
virtual void x() { cout << "Derive::x()" << endl; } // 重写
virtual void y1() { cout << "Derive::y1()" << endl; }
};


相比于无覆盖的情况,只是将Derive::x()覆盖了Base1::x()Base2::x()而已,你可以自己写测试代码测试一下,这里就不再赘述了。

注:若虚函数是 private 或 protected 的,我们照样可以通过访问虚函数表来访问这些虚函数,即上面的测试代码一样能运行。

附:编译器对指针的调整

在多重继承下,我们可以将子类实例绑定到任一父类的指针(或引用)上。以上述有覆盖的多重继承关系为例:

Derive b;
Base1* ptr1 = &b; // 指向 b 的初始地址
Base2* ptr2 = &b; // 指向 b 的第二个子对象
  • 因为 Base1 是第一个基类,所以 ptr1 指向的是 Derive 对象的起始地址,不需要调整指针(偏移)。
  • 因为 Base2 是第二个基类,所以必须对指针进行调整,即加上一个 offset,让 ptr2 指向 Base2 子对象。
  • 当然,上述过程是由编译器完成的。
Base1* b1 = (Base1*)ptr2;  
b1->y(); // 输出 Base2::y()
Base2* b2 = (Base2*)ptr1;
b2->y(); // 输出 Base1::y()

这里,由于ptr2指向的是Base2子对象的地址,因此尽管b1是Base1型指针,同时也将ptr2转换成了Base1,但ptr2的地址仍然是Base2的地址,因此调用y()也将调用Base2中的y();
同理,由于ptr1指向的是Base1的地址,因此尽管b2是Base2型指针,同时也将ptr1转换成了Base2,但ptr1的地址仍然是Base1的地址,因此调用y()也将调用Base1中的y()
其实,通过某个类型的指针访问某个成员时,编译器只是根据类型的定义查找这个成员所在偏移量,用这个偏移量获取成员。由于 ptr2 本来就指向 Base2 子对象的起始地址,所以b1->y()调用到的是Base2::y(),而 ptr1 本来就指向 Base1 子对象的起始地址(即 Derive对象的起始地址),所以b2->y()调用到的是Base1::y()
 
                    
            
                

鲜花

握手

雷人

路过

鸡蛋
该文章已有0人参与评论

请发表评论

全部评论

专题导读
上一篇:
C#操作MongoDB发布时间:2022-07-13
下一篇:
C#_Kernelbase.dll模块故障——是我不懂你发布时间:2022-07-13
热门推荐
阅读排行榜

扫描微信二维码

查看手机版网站

随时了解更新最新资讯

139-2527-9053

在线客服(服务时间 9:00~18:00)

在线QQ客服
地址:深圳市南山区西丽大学城创智工业园
电邮:jeky_zhao#qq.com
移动电话:139-2527-9053

Powered by 互联科技 X3.4© 2001-2213 极客世界.|Sitemap