C++中虚函数的诞生,就是为了多态的实现。当子类对父类的虚函数进行了重写,在父类指针调用重写的虚函数时,如果父类指针(或引用)指向了父类的对象,则调用父类的虚函数,如果父类指针(或引用)指向了子类对象,则调用子类的虚函数。
要想了解多态的实现,就必须要知道虚函数表的构成。
【注:文章中代码测试环境为Win7 64位 VS2013】
首先,我们讨论单个含有虚函数的类,即不存在继承关系。
当我们的类中含有虚函数时,类实例化出来的对象,他的成员除了自己的成员变量外,还会多出一个指针。这个指针我们称为虚表指针,他所指向的是我们类对象所维护的虚表。虚表中保存的是类中所有虚函数的地址。代码如下:
class B
{
public:
B()
:_val(1){}
virtual void fun1()
{
cout << "void B::fun1()" << endl;
}
virtual void fun2()
{
cout << "void B::fun2()" << endl;
}
void fun3()
{
cout << "void B::fun3()" << endl;
}
private:
int _val;
};
int main()
{
B b;
cout << sizeof(b) << endl;//输出结果为8
system("pause");
return 0;
}
然后我们切换到监视窗口,<如下>可以发现,对象b实际上维护了两个成员,"__vfptr"和"_val",在内存窗口中"&b",得到的是 0012cc74 ,即 __vfptr,下一个是 00000001,即我们的成员 _val。这也解释了为什么 sizeof(b) 的输出结果是8。不管有多少虚函数,类中只保留了一个虚表指针,添加再多的虚函数,也不会改变sizeof(b)的值
另外,可以看到的是虚表只保存虚函数地址,非虚函数,依旧是属于整个类而非对象。
我们再次查看 __vfptr 指向的空间
前两个是我虚函数的地址,也就是说,他们之间存在关系如下
为了验证,这里我重新封装一个函数指针,通过偏移,看能不能输出cout里面的内容。代码如下:
typedef void(*p_fun)();
void Print_fun(p_fun* ppfun)
{
for (int i = 0;/* ppfun[i] != NULL*/i<2; i++)
{
ppfun[i]();
}
}
void test()
{
B b;
Print_fun((p_fun*)(*(int*)&b));
}
测试是可以输出我们函数内容的。多说一句,虚表中前面保存的都是虚函数的地址,最后结束项在不同编译器下是不一样的,在VS2013环境下,最后一项保存的地址是不可访问的,VS2008环境下,最后是以0x00000000结尾,即是NULL。所以打印函数Print_fun()中for循环条件我做了修改。当然可以更加直接的这样调用函数。
// ((p_fun*)(*(int*)&b))[0]();
// ((p_fun*)(*(int*)&b))[1]();
接下来,我们看看包含虚函数重写的单继承中的虚表
这里给出单继承的测试代码
class A
{
public:
A()
:_a_val(1){}
virtual void fun1()
{
cout << "void A::fun1()" << endl;
}
virtual void fun2()
{
cout << "void A::fun2()" << endl;
}
protected:
int _a_val;
};
class B:public A
{
public:
B()
:_b_val(2){}
virtual void fun1()
{
cout << "virtual B::fun1()" << endl;
}
virtual void fun3()
{
cout << "virtual B::fun3()" << endl;
}
virtual void fun4()
{
cout << "virtual B::fun4()" << endl;
}
protected:
int _b_val;
};
void test()
{
B b;
}
代码说明:父类 A 包含两个虚函数 fun1() 、fun2(),一个成员变量 _a_val ,构造成员变量为 1 ;子类 B 共有继承了 A ,重写函数 fun1() ,同时添加两个自己的虚函数 fun3()、fun4(),成员变量 _b_val ,构造为 2 。
接下来切换到调试窗口<如下图>,可以看到类B实例化对象 b 实际上维护了三个成员,"__vfptr"、"_a_val"、"_b_val",在内存窗口中“&b”,得到的是0x009bcd48,对应到监视窗口,即 __vfptr ,接下来是0x00000001,0x00000002,即成员变量_a_val,_b_val。如果在这里去sizeof(b),得到的结果应该是12。
接下来查看 __vfptr 指向的空间
监视中看到的是只有两个函数地址,但内存窗口中可以看到,前四个在内存中是在一起的,或者说很近。为了确认,使用刚刚的打印函数,不过需要改变一下循环次数,受编译器的限制,这里只能手动修改循环次数,看最多打印多少次是正常结束,而非程序崩溃。
typedef void(*p_fun)();
void Print_fun(p_fun* ppfun)
{
for (int i = 0;/* ppfun[i] != NULL*/i<4; i++)
{
ppfun[i]();
}
}
void test()
{
B b;
// ((p_fun*)(*(int*)&b))[0]();
// ((p_fun*)(*(int*)&b))[1]();
// ((p_fun*)(*(int*)&b))[2]();
// ((p_fun*)(*(int*)&b))[3]();
Print_fun((p_fun*)(*(int*)&b));
}
测试得到,最多可以打印四次,打印结果如下:
*可以看到fun1()函数被子类 B 重写,fun2()函数继承自父类。得到结果监视窗口未显示虚函数fun3()和fun4()地址,但实际上子类新创建的虚函数地址也会保存到虚表当中,而且在单继承过程中,子类的虚函数和父类的虚函数是保存在同一虚表当中,并未对子类的虚函数创建独立的虚表。
即有下图关系:
接下来的多继承中的对象模型
首先给出测试代码,如下
class A
{
public:
A()
:_a_val(1){}
virtual void test1()
{
cout << "A::test1()" << endl;
}
virtual void test2()
{
cout << "A::test2()" << endl;
}
protected:
int _a_val;
};
class B
{
public:
B()
:_b_val(2){}
virtual void test1()
{
cout << "B::test1()" << endl;
}
virtual void test3()
{
cout << "B::test3()" << endl;
}
protected:
int _b_val;
};
class C
{
public:
C()
:_c_val(3){}
virtual void test1()
{
cout << "C::test1()" << endl;
}
virtual void test4()
{
cout << "C::test4()" << endl;
}
protected:
int _c_val;
};
class D:public A,public B,public C
{
public:
D()
:_d_val(4){}
virtual void test1()
{
cout << "D::test1()" << endl;
}
virtual void test5()
{
cout << "D::test5()" << endl;
}
protected:
int _d_val;
};
void test()
{
D d;
}
接下来切换到调试窗口<如下图>,可以看到类 D 实例化对象 d 这里维护了七个成员,由于继承了三个类,因此这里有三个虚表指针"__vfptr"、同时包含继承自三个类的成员变量"_a_val"、"_b_val"、"_c_val"和自己本身的成员变量"_d_val"。在内存窗口中“&d”,得到的是0x013bdd04,对应到监视窗口,即继承的第一个类的虚表指针 __vfptr ,接下来是0x00000001,即成员变量_a_val,接下来依次类推,得到第二个类的虚表指针,和继承自第二个类的成员变量,第三个类的虚表指针,和继承自第三个类的成员变量,最后一项是子类 D 的成员变量。如果在这里去 sizeof(b),得到的结果应该是28。
不过这里有个问题,是子类 D 的虚函数地址在哪里。。这里我们打开多个内存窗口,同时把各个虚表指针指向的内容列出来。<如图>
除此之外,还应该可以看到,子类每继承一个含有虚函数的父类,就会多一个虚表指针,可能会同时维护多个虚表。
换句话说,存在如下图对应关系。
多提一点,子类继承了多个父类,父类虚表的地址不一定是连续的
这里依旧使用函数指针的方式去调用我的成员函数来加以验证。代码如下
typedef void(*p_fun)();
void Print_fun(p_fun* ppfun)
{
for (int i = 0; ppfun[i] != NULL; i++)
{
ppfun[i]();
}
}
void test()
{
D d;
Print_fun((p_fun*)(*(int*)&d));
cout << "-----------------------------------------" <<endl;
Print_fun((p_fun*)(*((int*)&d+2)));
cout << "-----------------------------------------" << endl;
Print_fun((p_fun*)(*((int*)&d+4)));
cout << "-----------------------------------------" << endl;
}
打印结果如下:
由打印结果可见,子类专有的虚函数test5()的函数地址放在了第一个继承的虚表中,test1()函数均被子类 D 重写。
我们通过虚函数表理解C++ 中的对象模型,了解多态实际上是用虚函数实现覆盖,但通过上面的测试,可以发现,实现多态的同时,无疑会带来效率的下降(通过两次指针解引用才可以访问)。
除此之外应该看到的一点是,多态实现的过程是不安全的,尽管虚函数表的内容我们不能够随意修改,但永远可以被直接访问,这是不安全的一种直接表现。
关于菱形继承的对象模型和菱形虚拟继承的对象模型,会在下一篇中提到。
------------------------------------------muhuizz------------------------------------------
亿速云「云服务器」,即开即用、新一代英特尔至强铂金CPU、三副本存储NVMe SSD云盘,价格低至29元/月。点击查看>>
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。