目录
1. 为什么多态函数的参数是基类的指针或者引用 ,参数是基类对象不行?
2.对象中的虚表指针是在什么阶段的初始化呢?虚表又是在什么阶段生成的呢?
4. sizeof(Base1)和sizeof(Base2)是多少?
一.多态的含义
通俗的说:多态就是同一个函数名称,作用在不同的对象上将产生不同的行为。
举一个例子
比如 买票这个行为 ,当 普通人 买票时,是全价买票; 学生 买票时,是半价买票; 军人 买票时是优 先买票。也就是不同的人去买票有不同的待遇。
多态的作用:多态性提供了把接口与实现分开的另一种方法,提高了代码的组织性和可读性,更重要的是提高了软件的可扩展性。
二.多态的分类
静态多态: 在程序编译期间确定了程序的行为,也称为,比如:函数重载。
动态多态(动态绑定):即运行时的多态,在程序执行期间(非编译期)判断所引用对象的实际类型
三.静态多态
函数重载就是简单的静态多态
静态多态是编译器在编译期间完成的
四.重载、覆盖(重写)、隐藏(重定义)的对比
同名成员变量也是隐藏关系。
五.虚函数的多态和动态多态
虚函数是实现运行时多态的一种重要方式,是重载的另一种方式,实现的是动态的重载,即函数调用与函数体之间的联系是在运行时建立的,也就是动态多态。
动态的多态是在不同继承关系的类对象中,去调用同一函数,产生了不同的行为(重点)。
1.动态多态的定义和实现
一个函数构成多态的 两个条件 :
- 1. 调用的函数的参数必须时基类的指针或者引用调用虚函数
- 2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
2.定义虚函数
- 在类的非静态成员函数前加上virtual就是虚函数。
- 只有类的非静态成员函数才可以被定义为虚函数。
3. 虚函数重写的特殊例子
1.斜变
派生类重写基类的虚函数时,派生类的虚函数与基类返回值类型不同。即基类虚函数返回基类的对象的指针或引用,派生类返回派生类的指针或引用时,这时候称为斜变(了解)。
2. 析构函数的重写
1.如果基类的析构函数是虚函数,那么无论派生类的析构函数是否定义为虚函数,则派生类的析构函数重写了基类的析构函数。
2.原理是在编译阶段,编译器对所有类的析构函数名称做了处理,统一命名为destructor,这才能构成函数重载的条件。
3.如果基类的析构函数为虚函数,则当派生类未定义析构函数时,编译器所生成的析构函数也为虚函数。
3.派生类的重写虚函数时,最前面可以不用加上virtual
六. 动态多态的原理
1.事实上,当将基类的成员函数breathe()声明为virtual即虚函数时,编译器在编译的阶段发现Animal类中有虚函数时。此时编译器就会为每个包含虚函数的类创建一个虚表,该表是一个一维数组,在这个数值中存放每个虚函数的地址,这个数组最后面放着一个nullptr。
2.如果一个类包含一张虚表,那么该类定义出来的对象中包含一个虚表指针。
3.下面的代码Animal类和Fish类都包含一个虚函数breathe(),因此编译器会为这两个类分别建立一个虚表。
- 当我们创建 a 和 f时,对象a中就会包含指向Animal类的虚表的指针,对象f中就会包含一个指向son类的虚表的指针。
由于不同的对象看到的虚表是不一样的,所以函数在调用的时候,会根据虚表中虚函数指针找到相对应的虚函数。Animal类的虚表中的存放的是Animal类的虚函数指针,Fish类的虚表中存放的是Fish类的虚函数指针。
同类型的对象指向的是同一个虚表.a1,a2,a3中的_vfptr中的值相同,指向同一个虚表
如果一个类中定义多个虚函数,那么虚表中就有多个虚函数的地址。由于Fish类没有重写sleep()的虚函数,Fish类中就存放Animal类的sleep()虚函数指针.(如果派生类没有重写基类的虚函数,那么派生类中虚表包含的是基类的虚函数指针)
1.写一个程序打印虚表中的虚函数的地址。
1.取出b的地址,强制转换为int*,
2.然后解引用就取出b最前面的4个字节,这个值就是指向虚表的指针
3.再转换成VFUNC*类型,因为虚表是存储VFUNC类型数组。
4.虚表指针传递给PrintVTable进行打印虚表
5.需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的-生成-清理解决方案,再编译就好了。
七. 多继承的虚函数表
一个派生类如果继承多个有虚函数的基类,那么该派生类就会存在多个虚函数表。例如:
Derive中继承了Base1和Base2,由于Base1和Base2都有虚函数,所以Derive会存放两个虚表指针指向两个虚表,其中一个虚表是存放Base1类的虚函数的地址或者Derive重写Base1虚函数的地址,另一个虚表是存放Base2类的虚函数或者Derive重写Base2虚函数的地址。
由于Derive没有重写Base1和Base2中的虚函数,所以虚函数表1和虚函数表2分别存放的是Base.和Base2中的虚函数地址。
多继承的派生类的未重写的虚函数放在第一继承基类的虚函数表中
总结:
1.当类中定义虚函数的时候,相对应的对象它们的多了一个成员_vfptr指针,这个指针指向的是函数指针数组(虚函数表),这张表是用来放虚函数的地址的,每个虚函数的地址存放在这张表是固定的。
2.当派生类去重写基类的虚函数的时候,则派生类的虚函数表会将相对应的虚函数的地址给修改为派生类重写的虚函数的地址。
八. 笔试题
1. 为什么多态函数的参数是基类的指针或者引用 ,参数是基类对象不行?
如果是调用是基类对象,那么当派生类給传值給多态函数的时候,只会将派生类的成员赋值給临时对象,不会把虚表指针給赋值过去,因为基类对象包含的虚表指针都是一样的。不会因为你是派生类构造出来的基类,虚表指针就是派生类虚表。因此传值是不会构成多态的。
多态函数的参数是传对象:
如果是参数是基类指针,那么通过派生类的指针构造出来的基类的指针,能够访问到派生类的虚表的指针
2.对象中的虚表指针是在什么阶段的初始化呢?虚表又是在什么阶段生成的呢?
对象中的虚表指针是在构造函数的初始化列表阶段开始初始化的,虚表是在编译的时候就已经生成了。
3.虚表是存在进程地址空间中的哪个区域的?(栈,堆等)
结果得出,虚表地址的代码段数据是最相近的,所以在vs编译器中,虚表是存在于代码段的。
4. sizeof(Base1)和sizeof(Base2)是多少?
Base1对象的大小是4个字节,Base2对象的大小是8个字节,因为Base2中存在虚函数,所以Base2的对象最前面包含一个4个字节的虚表指针。
5.函数重载,重写,隐藏(重定义)的对比
6. inline函数可以是虚函数吗?
可以,不过编译器就忽略inline属性,这个函数就不再是inline,因为虚函数要放到虚表中去。
7. 静态成员没有this可以是虚函数吗?
不可以,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问到虚函数表, 所以静态成员函数无法放进虚函数表。
8. 构造函数可以是虚函数吗?
不能,因为虚函数表指针是在构造函数的初始化阶段才进行初始化的