在我们深入学习C++的过程中,虚函数和多态是我们需要一直关注的要点,这两个要点能够检验我们是否深入了解C++。今天,我们就站在编译器的角度,探讨虚函数表和多态这两个话题。
首先,我们看第一个问题,虚函数表和虚函数表指针的概念是什么?。我们先创建一个空类A,再创建一个A类对象a,接着计算一下对象a的大小:
C++
class A {
}
A a;
cout << sizeof(a) << endl; //1
大家可以自行测试,测试结果是对象a的sizeof值是1。所以,对于一个空类对象,我们不能认为它的大小为0,只要它占用内存空间,sizeof值至少为1。因为一个空类对象在内存中至少要把位置该占住了,所以大小至少为1。
下面,我们往类A中添加两个函数func1()和func2(),这时在来计算对象a的大小。
C++
class A
{
public:
void func1(){ }
void func2(){ }
}
cout << "sizeof(a)=" << sizeof(a) << endl; //1
计算结果仍然是1。这说明,类A中的普通函数不占用类对象的内存空间。
这时,我们往类A中再添加一个虚函数,这时再执行程序,计算sizeof(a)的值:
C++
class A
{
public:
void func1(){ }
void func2(){ }
public:
virtual void vfunc(){ }
}
cout << "sizeof(a)=" << sizeof(a) << endl; //4
我们发现计算结果由1变成了4,就是因为虚函数的加入引起了这样的变化。
当一个或多个函数加入到类中后,编译器会向类中插入一个看不见的成员变量,这种看不见的成员变量叫做虚函数表指针。在类中如下所示:
C++
class A
{
public:
void *vptr; //虚函数表指针(virtual table pointer)
}
这个虚函数表指针就是4个字节,而这4个字节就是占用类对象的内存空间的。
下面,第二个问题,我们说一说虚函数表的生成时机和生成原因。在类A中至少存在1个或多个虚函数时,在编译期间,编译器会为类A生成一个虚函数表(virtual table),简称vbtl。这个虚函数表会一直伴随类A,在经过编译、链接,这个类A和伴随类A的虚函数表会都会保存到可执行文件中,在可执行文件执行过程中,也会加载到内存中。
接着,第三个问题,我们来讲虚函数表指针被赋值的时机以及虚函数表和虚函数表指针之间的关系。
对于这种有虚函数的类A,在编译期间,编译器会向类A的构造函数中安插为vptr赋值的语句。
C++
A() {
vptr = &A::vftable; //编译器在编译期间做的
}
在程序运行起来之后,当创建一个类A对象的时候,会执行类A的构造函数,因为构造函数中有为vptr赋值的语句,从而使vptr指向类A中的vbtl。
下面,我们来看第四个问题,类对象在内存中的布局。为了说明问题,我们在往类A中添加一些成员。
C++
class A
{
public:
void func1(){ }
void func2(){ }
public:
virtual void vfunc1(){ }
virtual void vfunc1(){ }
virtual ~A(){ }
private:
int m_a;
int m_b;
}
cout << "sizeof(a)=" << sizeof(a) << endl; //12
内存分布如下图所示,因为普通成员函数不占用类对象的内存空间,所以对选哪个a的大小是12个字节。
最后,我们讲一下第五个问题,虚函数的工作原理以及多态性的体现。常规的多态性的理解,父类中有一个虚函数,子类中也有一个同名的虚函数,当通过父类指针new一个子类对象时,或者通过父类引用来绑定一个子类对象的时候,如果用这个父类指针来调用这个虚函数,那么调用其实是子类的虚函数。多态性往深入来说,可以从代码实现上和表现形式上来说,但又有一点是相同的,那就是多态必须存在虚函数,没有虚函数,绝不可能存在多态。类中定义了虚函数,并且我们要调用这个虚函数,那才存在多态性的可能。
从代码实现上来看一看多态性的体现。当调用虚函数的时候,我们可以看一看调用路线,看是不是利用vptr找到vtbl,然后通过查询vtbl扎到虚函数表的入口并执行。如果调用虚函数走的是这个路线,那就是多态。
看下面的代码中函数调用是不是多态。
C++
class Base
{
public:
virtual void myvirfunc() {}
}
Base* pa = new Base();
pa->myvirfunc(); //这是不是多态,是多态的
Base base;
base.myvirfunc(); //这个就不是多态
Base &ybase = &base;
&ybase->myvirfunc(); //这个也是多态
可以通过汇编代码来看虚函数时如何具体调用的了。
从表现形式上看多态性的具体体现。在写代码时需遵循以下几点:
- 程序中即存在父类也存在子类,父类中必须含有虚函数,子类中也必须重写父类中的虚函数。
- 父类指针指向子类对象,或者父类引用绑定(指向)子类对象。
- 当通过父类的指针或引用,调用子类中重写的虚函数时,就能看出多态性的表现了。 示例代码如下:
C++
class Derive:public Base
{
public:
virtual void myvirfunc() { }
}
//父类指针指向子类对象
Derive derive;
Base* pbase = &derive;
pbase->myvirfunc(); //Derive::myvirfunc()
//或者
Base* pbase2 = new Derive(); //释放内存请自行释放,在这里没演示
pbase2->myvirfunc();
//父类引用绑定(指向)子类对象
Derive derive2;
Base& yinbase = derive2;
yinbase.myvirfunc(); //Derive::myvirfunc()
假设父类有虚函数f、g、h,子类重写了虚函数g,那么它们的内存布局如下所示:
至此就是文章的全部内容,感谢您的阅读!