一、虚函数表
首先引入一道笔试题,sizeof(Base)的值是多少?
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b;
};
int main()
{
Base b;
cout << sizeof(b) << endl;
system("pause");
return 0;
}
输出结果:
第一次我也感到迷惑,在32位平台下为什么会是8个字节呢?不知道有没有童鞋和我一样迷惑~~~
首先引入一个概念,虚函数表指针。下面我调试一下上述代码,通过监视窗口看一下对象b中的成员:
通过调试窗口,我们可以看出对象b中,除了有成员_b之外,还有一个_vfptr,我们把对象中的这个指针叫作虚函数表指针(v代表virtual, f代表function)。一个含有虚函数的类中至少有一个都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。
下面我们再来看一段代码:
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
virtual void Func2()
{
cout << "Func2()" << endl;
}
virtual void Func3()
{
cout << "Func3()" << endl;
}
private:
int _b = 1;
};
class Derive :public Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _d = 2;
};
int main()
{
Base b;
cout << "Base:"<<sizeof(b) << endl;
Derive d;
cout << "Derive:"<<sizeof(d) << endl;
system("pause");
return 0;
}
输出结果为:
可以看出对象b中虽然有三个虚函数,但是只有一个虚函数表指针,也就是一份虚表,下面我们再来通过监视窗口看一下底层原理:
另外需要注意的是同类型对象只有一张虚表。
通过监视窗口,可以总结出来以下几点:
1、派生类对象d中也有一个虚表指针,d对象由两部分组成,一部分是父类继承下来的成员,虚表指针也被继承下来了;
2、基类对象和派生类对象虚表是不一样的,通过监视窗口可以看到Func()1完成了重写,所以d的虚表中存的是重写的Func()1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表虚函数中的。重写是语法的叫法,覆盖是原理层的叫法。
3、需要注意的是Func()2继承下来之后是虚函数,所以放进了虚表,Func()3也继承下来了,但是不是虚函数,所以不会放进虚表。
4、虚函数本质上是一个存虚函数指针的指针数组,这个数组在最后面放了一个Nullptr指针。
5、总结以下派生类的虚表构成:(1)先将基类中的虚表内容拷贝一份到派生类基表中
(2)如果派生类重写了基类中某个虚函数,派生类会用自己的虚函数去覆盖虚表中基类的虚 函数;
(3)派生类自己增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
6、这里还需要注意一个问题,虚函数存在哪?虚表存在哪?
答案是因为虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样,都是存在代码段的,只是它的指针存在了虚表中。另外对象中存的是虚表指针,不是虚函数。
下面用一段来验证一下虚表存在哪?
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
virtual void Func2()
{
cout << "Func2()" << endl;
}
void Func3()
{
cout << "Func3()" << endl;
}
private:
int _b = 1;
};
int main()
{
Base bb;
cout << (void*)(*((int*)&bb)) << endl;
//栈
int a = 0;
//堆
int *b = new int;
//静态区
static int c = 1;
//代码段
char* d = "zzzz";
printf("0x%p\n", &a);
printf("0x%p\n", b);
printf("0x%p\n", &c);
printf("0x%p\n", d);
system("pause");
return 0;
}
输出结果:
可以看出对象bb的地址位于*d的下面,所以推测出虚表存在于代码段中。
二、多态的原理
首先来看一段代码:
class Person
{
public:
virtual void BuyTicket()
{
cout << "买票--全价" << endl;
}
};
class Student :public Person
{
public:
virtual void BuyTicket()
{
cout << "买票--半价" << endl;
}
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person Mike;
Func(Mike);
Student Jhon;
Func(Jhon);
system("pause");
return 0;
}
输出结果:
由输出结果可知,根据对象的定义顺序,可得
1、当p 指向Mike对象时,p->BuyTicket在Mike的虚表中找到虚函数是Person::BuyTicket;
2、当p 指向Jhon对象时,p->BuyTicket在Jhon的虚表中找到虚函数是Student::BuyTicket;
3、由此可以看出当不同对象去实现同一行为时,产生了不同的形态;
4、要实现多态,有两个条件:一是虚函数覆盖,一个是对象的的指针或 引用掉用虚函数;
5、满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象中取的,不满足多态的函数调用是在编译时确定好的。下面看一段汇编代码理解一下这一点:
(1)满足多态的函数调用
由汇编代码可以看出,满足多态的函数调用时,并不知道对象p是什么类型的,而是在程序运行期间根据拿到的p的类型来该调用哪个的函数。
(2)普通函数调用
由汇编代码可以看出,普通函数的调用在程序最开始就确定了对象的类型。
三、动态绑定与静态绑定
1、静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为(编译器绑定),也称为静态多态。比如:函数重载、上述的普通函数的调用。
2、动态绑定又称后期绑定(晚绑定),是在程序运行期间(运行时绑定),根据具体拿到的类型确定具体的程序行为,调用具体的函数,也称为动态多态。