请别人做网站有风险吗,网站论文首页布局技巧,做网站视频上传到哪儿,广州海珠区培训机构网站建设一、虚函数
1. 概念 多态指当不同的对象收到相同的消息时#xff0c;产生不同的动作 编译时多态#xff08;静态绑定#xff09;#xff0c;函数重载#xff0c;运算符重载#xff0c;模板。运行时多态#xff08;动态绑定#xff09;#xff0c;虚函数机制。为了实现…一、虚函数
1. 概念 多态指当不同的对象收到相同的消息时产生不同的动作 编译时多态静态绑定函数重载运算符重载模板。运行时多态动态绑定虚函数机制。为了实现C的多态C使用了一种动态绑定的技术。这个技术的核心是虚函数表下文简称虚表。本文介绍虚函数表是如何实现动态绑定的。
C多态实现的原理 当类中声明虚函数时编译器会在类中生成一个虚函数表虚函数表是一个存储成员函数地址的数据结构虚函数表是由编译器自动生成与维护的 virtual成员函数会被编译器放入虚函数表中存在虚函数表时每个对象中都有一个指向虚函数表的指针2. 类的虚表
每个包含了虚函数的类都包含一个虚表。 我们知道当一个类A继承另一个类B时类A会继承类B的函数的调用权。所以如果一个基类包含了虚函数那么其继承类也可调用这些虚函数换句话说一个类继承了包含虚函数的基类那么这个类也拥有自己的虚表。
我们来看以下的代码。类A包含虚函数vfunc1vfunc2由于类A包含虚函数故类A拥有一个虚表。
class A {
public:virtual void vfunc1();virtual void vfunc2();void func1();void func2();
private:int m_data1, m_data2;
}; 虚表是一个指针数组其元素是虚函数的指针每个元素对应一个虚函数的函数指针。需要指出的是普通的函数即非虚函数其调用并不需要经过虚表所以虚表的元素并不包括普通函数的函数指针。 虚表内的条目即虚函数指针的赋值发生在编译器的编译阶段也就是说在代码的编译阶段虚表就可以构造出来了。 3. 虚表指针
虚表是属于类的而不是属于某个具体的对象一个类只需要一个虚表即可。同一个类的所有对象都使用同一个虚表。 为了指定对象的虚表每个对象的内部包含一个虚表的指针来指向自己所使用的虚表。为了让每个包含虚表的类的对象都拥有一个虚表指针编译器在类中添加了一个指针*__vptr用来指向虚表。这样当类的对象在创建时便拥有了这个指针且这个指针的值会自动被设置为指向类的虚表。 上面指出一个继承类的基类如果包含虚函数那个这个继承类也有拥有自己的虚表故这个继承类的对象也包含一个虚表指针用来指向它的虚
4. 动态绑定
class A
{
public:virtual void vfunc1();virtual void vfunc2();void func1();void func2();
private:int m_data1, m_data2;
};class B : public A
{
public:virtual void vfunc1();void func1();
private:int m_data3;
};class C: public B
{
public:virtual void vfunc2();void func2();
private:int m_data1, m_data4;
};
类A是基类类B继承类A类C又继承类B。类A类B类C其对象模型如下图3所示。 由于这三个类都有虚函数故编译器为每个类都创建了一个虚表即类A的虚表A vtbl类B的虚表B vtbl类C的虚表C vtbl。类A类B类C的对象都拥有一个虚表指针*__vptr用来指向自己所属类的虚表。
类A包括两个虚函数故A vtbl包含两个指针分别指向A::vfunc1()和A::vfunc2()。 类B继承于类A故类B可以调用类A的函数但由于类B重写了B::vfunc1()函数故B vtbl的两个指针分别指向B::vfunc1()和A::vfunc2()。 类C继承于类B故类C可以调用类B的函数但由于类C重写了C::vfunc2()函数故C vtbl的两个指针分别指向B::vfunc1()指向继承的最近的一个类的函数和C::vfunc2()。
虽然图3看起来有点复杂但是只要抓住“对象的虚表指针用来指向自己所属类的虚表虚表中的指针会指向其继承的最近的一个类的虚函数”这个特点便可以快速将这几个类的对象模型在自己的脑海中描绘出来。[非虚函数的调用不用经过虚表故不需要虚表中的指针指向这些函数。
【注意】非虚函数的调用不用经过虚表故不需要虚表中的指针指向这些函数。
下面以代码说明代码如下
#include iostream
using namespace std;class Base {
public:virtual void f() { cout Base::f endl; }virtual void g() { cout Base::g endl; }virtual void h() { cout Base::h endl; }
};typedef void(*Fun)(void); //函数指针
int main()
{Base b;// 这里指针操作比较混乱,在此稍微解析下:// *****printf(虚表地址:%p\n, *(int *)b); 解析*****:// 1.b代表对象b的起始地址// 2.(int *)b 强转成int *类型,为了后面取b对象的前四个字节,前四个字节是虚表指针// 3.*(int *)b 取前四个字节,即vptr虚表地址//// *****printf(第一个虚函数地址:%p\n, *(int *)*(int *)b);*****:// 根据上面的解析我们知道*(int *)b是vptr,即虚表指针.并且虚表是存放虚函数指针的// 所以虚表中每个元素(虚函数指针)在32位编译器下是4个字节,因此(int *)*(int *)b// 这样强转后为了后面的取四个字节.所以*(int *)*(int *)b就是虚表的第一个元素.// 即f()的地址.// 那么接下来的取第二个虚函数地址也就依次类推. 始终记着vptr指向的是一块内存,// 这块内存存放着虚函数地址,这块内存就是我们所说的虚表.//printf(虚表地址:%p\n, *(int *)b);printf(第一个虚函数地址:%p\n, *(int *)*(int *)b);printf(第二个虚函数地址:%p\n, *((int *)*(int *)(b) 1));Fun pfun (Fun)*((int *)*(int *)(b)); //vitural f();printf(f():%p\n, pfun);pfun();pfun (Fun)(*((int *)*(int *)(b) 1)); //vitural g();printf(g():%p\n, pfun);pfun();
}
输出结果 通过这个示例我们可以看到我们可以通过强行把b转成int *取得虚函数表的地址然后再次取址就可以得到第一个虚函数的地址了也就是Base::f()这在上面的程序中得到了验证把int* 强制转成了函数指针。通过这个示例我们就可以知道如果要调用Base::g()和Base::h()其代码如下
(Fun)*((int*)*(int*)(b)0); // Base::f()
(Fun)*((int*)*(int*)(b)1); // Base::g()
(Fun)*((int*)*(int*)(b)2); // Base::h() 二、一般继承
下面再让我们来看看继承时的虚函数表是什么样的。假设有如下所示的一个继承关系
class Base
{
public:virtual void f() { cout Base::f() endl; }virtual void g() { cout Base::g() endl; }virtual void h() { cout Base::h() endl; }
};class Derive : public Base
{
public:virtual void f1() { cout Base::f1() endl; }virtual void g1() { cout Base::g1() endl; }virtual void h1() { cout Base::h1() endl; }
}; 请注意在这个继承关系中子类没有重载任何父类的函数。那么在派生类的实例中其虚函数表如下所示
对于实例Derive d; 的虚函数表如下 我们可以看到下面几点
虚函数按照其声明顺序放于表中。父类的虚函数在子类的虚函数前面。三、一般继承有虚函数覆盖
覆盖父类的虚函数是很显然的事情不然虚函数就变得毫无意义。下面我们来看一下如果子类中有虚函数重载了父类的虚函数会是一个什么样子假设我们有下面这样的一个继承关系。
class Base
{
public:virtual void f() { cout Base::f() endl; }virtual void g() { cout Base::g() endl; }virtual void h() { cout Base::h() endl; }
};class Derive : public Base
{
public:virtual void f() { cout Base::f1() endl; }virtual void g1() { cout Base::g1() endl; }virtual void h1() { cout Base::h1() endl; }
}; 为了让大家看到被继承过后的效果在这个类的设计中我只覆盖了父类的一个函数f()。那么对于派生类的实例其虚函数表会是下面的一个样子 我们从表中可以看到下面几点
覆盖的f()函数被放到了虚表中原来父类虚函数的位置。没有被覆盖的函数依旧。
这样我们就可以看到对于下面这样的程序
Base *b new Derive();
b-f();
由b所指的内存中的虚函数表的f()的位置已经被Derive::f()函数地址所取代于是在实际调用发生时是Derive::f()被调用了。这就实现了多态。 四、单一的一般继承 下面我们假设有如下所示的一个继承关系 注意在这个继承关系中父类子类孙子类都有自己的一个成员变量。而了类覆盖了父类的f()方法孙子类覆盖了子类的g_child()及其超类的f()。
测试代码
#includeiostream
using namespace std;class Parent {
public:int iparent;Parent(): iparent(10) {}virtual void f() { cout Parent::f() endl; }virtual void g() { cout Parent::g() endl; }virtual void h() { cout Parent::h() endl; }
};class Child : public Parent {
public:int ichild;Child(): ichild(100) {}virtual void f() { cout Child::f() endl; }virtual void g_child() { cout Child::g_child() endl; }virtual void h_child() { cout Child::h_child() endl; }
};class GrandChild : public Child {
public:int igrandchild;GrandChild(): igrandchild(1000) {}virtual void f() { cout GrandChild::f() endl; }virtual void g_child() { cout GrandChild::g_child() endl; }virtual void h_grandchild() { cout GrandChild::h_grandchild() endl; }
};int main()
{typedef void(*Fun)(void);GrandChild gc;int** pVtab (int**)gc;cout [0] GrandChild::_vptr- endl;for (int i 0; (Fun)pVtab[0][i] ! NULL; i) {Fun pFun (Fun)pVtab[0][i];cout [ i ] ;pFun();}cout [1] Parent.iparent (int)pVtab[1] endl;cout [2] Child.ichild (int)pVtab[2] endl;cout [3] GrandChild.igrandchild (int)pVtab[3] endl;
}
输出结果 使用图片表示如下 可见以下几个方面 虚函数表在最前面的位置。成员变量根据其继承和声明顺序依次放在后面。在单一的继承中被overwrite的虚函数在虚函数表中得到了更新。参考资料
c中虚基类表和虚函数表的布局