目录
一、虚函数表
1、虚函数表的定义
2、虚函数表特性
3、虚表的打印
二、多态的原理
三、多态的相关问题
1、指针偏移问题
2、输出的程序是什么?
3、输出的程序是什么?
【前言】
上一篇我们学习了多态的基础知识,这一篇我将带着大家深入多态学习,了解多态的原理。【多态的基本介绍】
一、虚函数表
1、虚函数表的定义
// 这里常考一道笔试题:sizeof(Base)是多少?
class Base
{
public:virtual void Func1(){cout << "Func1()" << endl;}private:int _b = 1;char _ch;
};int main()
{cout << sizeof(Base) << endl;Base bb;return 0;
}
很多人会因为内存对齐认为答案是8,其实不然,答案是12.
这是因为类里面,除了_bb成员,还多一个__vfptr 放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表,存的是虚函数的地址。指针(v 代表 virtual,f 代表 function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表,本质是一个虚函数指针数组,一般情况这个数组最后面放了一个nullptr。
2、虚函数表特性
❓派生类的虚表是如何形成的呢?
派生类对象中有一个虚表指针,是由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的,另一部分是自己的成员。
a.先将基类中的虚表内容拷贝一份到派生类虚表中
b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
d.要理解将子类赋值给父类对象时,切片过程中,子类的虚表并没有拷贝切过去。这个过程是不会拷贝子类的虚表的。因为如果拷贝子类的虚表赋值给父类了,那么当指向的对象是这个父类时,到这个父类的虚表里找,找的那就是子类的虚函数了,而不是父类的虚函数
❓多态是如何利用虚函数表实现指向父类调用父类函数,指向子类调用子类函数的呢?
当指向父类对象时,就会去父类对象的虚表里找虚函数,当指向子类对象时,就会去子类对象的虚表里找虚函数。
中间发生了切割,本质上都是指向了父类数据,看到的还是父类对象。因为派生类继承不仅继承父类的所有数据,也将父类的虚表继承下来了。派生类会将重写的虚函数地址覆盖原来的基类的虚函数。这样就可以实现指向父类调用父类函数,指向子类调用子类函数。
❓同种类型的函数会被放在同一个虚函数表,同类型的对象会指向同一个虚表
Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函 数,所以不会放进虚表。
❓我们知道要达到多态,有两个条件,一个是虚函数覆盖,一个是对象的指针或引用调用虚函数。反思一下为什么一定要满足这个条件呢?为什么函数直接调用不行呢?
当使用指针或者引用时,父类的对象会指向父类的虚表,子类会继承父类的接口并进行重写指向子类的虚表。这样就可以实现指向父类调用父类函数,指向子类调用子类函数。
对于对象来说,指向父类调用父类函数,指向子类时会面临一个问题,父类的虚表会不会被子类拷贝,如果不拷贝,父类成员的虚表里面永远只有父类的虚函数,这显然是不行的。如果拷贝,虚表指向不明确,是原本父类的虚表还是子类拷贝过来的虚表。所以对象的切片只拷贝成员,不拷贝虚表。
❓虚函数存在哪的?虚表存在哪的?
虚函数表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。
❓虚表是什么阶段生成的?对象中虚表指针什么时候初始化?
虚表是在编译过程中生成的,因为编译过程中会生成地址。虚表指针是在构造函数中通过初始化列表中初始化。
❓理解虚函数为什么要重写?
只有虚函数重写,派生类的虚表里才可以存真正派生类虚函数,因为这个虚表是从父类继承下来的,里面都是父类的虚函数地址。而只有派生类虚函数重写后,才可以将重写的虚函数地址覆盖上去。这样就可以做到指向父类调用父类虚表中对应的虚函数,指向子类,调用子类虚表中对应的虚函数。
3、虚表的打印
有时候监视器窗口虚表不一定全部显示出来,所以我们可以写一个打印虚表的代码,便于我们自己观察。
虚表是一个函数指针数组,但是函数指针的类型比较复杂,所以我们重定义一下函数指针,增强代码可读性,需要注意:typedef void(*)() VF_PTR 函数指针定义名字需要放在中间
不同对象虚表里面对象不一样多,vs编译器在虚表最后都会放置一个空,所以我们可以利用这个原理实现for循环。
二、多态的原理
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 johnson;Func(johnson);return 0;
}
当p是指向mike对象时,p->BuyTicket在mike的虚表中找到虚函数是Person::BuyTicket。当p是指向johnson对象时,p->BuyTicket在johson的虚表中找到虚函数是Student::BuyTicket。 这样就实现出了不同对象去完成同一行为时,展现出不同的形态。
我们需要知道满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的。不满足多态的函数调用时编译时确认好的。
三、多态的相关问题
1、指针偏移问题
class Base1 { public: int _b1; };class Base2 { public: int _b2; };class Derive : public Base1, public Base2 { public: int _d; };int main(){Derive d;Base1* p1 = &d;Base2* p2 = &d;Derive* p3 = &d;return 0;}A:p1 == p2 == p3 B:p1 < p2 < p3 C:p1 == p3 != p2 D:p1 != p2 != p3
【答案】选C
2、输出的程序是什么?
class A{public:virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl;}virtual void test(){ func();}};class B : public A{public:void func(int val=0){ std::cout<<"B->"<< val <<std::endl; }};int main(int argc ,char* argv[]){B*p = new B;p->test();return 0;}A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确
【答案】选B
首先判断是否形成多态:
1、虚函数的重写--三同(函数名、参数、返回值)2、父类指针或者引用调用。
是多态。p调用 test函数,test 调用 func函数,func 函数参数的类型是A* this,this 调用class B的 func,派生类继承父类的成员函数,也会继承成员函数的缺省参数,所以val=1.
3、输出的程序是什么?
class A
{
public:virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
};
class B : public A
{
public:void func(int val = 0) { std::cout << "B->" << val << std::endl; }virtual void test() { func(); }
};
int main(int argc, char* argv[])
{B* p = new B;p->test();return 0;
}A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确
【答案】选D
首先判断是否形成多态:
1、虚函数的重写--三同(函数名、参数、返回值)2、父类指针或者引用调用。
不是多态,test 直接在class B调用func函数。