- 💂 个人主页:努力学习的少年
- 🤟 版权: 本文由【努力学习的少年】原创、在CSDN首发、需要转载请联系博主
- 💬 如果文章对你有帮助、欢迎关注、点赞、收藏(一键三连)和订阅专栏哦
目录
1. 为什么多态函数的参数是基类的指针或者引用 ,参数是基类对象不行?
2.对象中的虚表指针是在什么阶段的初始化呢?虚表又是在什么阶段生成的呢?
4. sizeof(Base1)和sizeof(Base2)是多少?
一. 多态的含义
多态的含义提供同一个接口可以用多种方法进行调用的机制,从而可以通过相同的接口访问不同的函数。具体的说,就是同一个函数名称,作用在不同的对象上将产生不同的行为。
- 静态多态: 在程序编译期间确定了程序的行为,也称为,比如:函数重载。
- 动态多态(动态绑定):即运行时的多态,在程序执行期间(非编译期)判断所引用对象的实际类型,根据其实际类型调用相应的方法。
二. 多态的作用
事实上,多态也是人类思维方式的一种直接模拟,比如,一个对象中有很多求两个数最大值的行为,虽然可以针对不同的数据类型,写很多不同的名称的函数来实现,但它们功能几乎完全相同,这时候就可以采用多态的特征,用统一的标识来完成这些功能。
总的来说,多态性提供了把接口与实现分开的另一种方法,提高了代码的组织性和可读性,更重要的是提高了软件的可扩展性。
三. 静态的多态
函数重载就是一种简单的静态的多态。
静态多态是编译器在编译期间完成的,编译器会根据实参类型来选择调用合适的函数,如果有合适的函数可以调用就调,没有的话就会发出警告或者报错。
四. 虚函数和动态的多态
虚函数是实现运行时多态的一种重要方式,是重载的另一种方式,实现的是动态的重载,即函数调用与函数体之间的联系是在运行时建立的,也就是动态联编。
动态的多态是在不同继承关系的类对象中,去调用同一函数,产生了不同的行为(重点)。
1.动态多态的定义和实现
- 1. 调用的函数的参数必须时基类的指针或者引用调用虚函数
- 2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
2. 定义虚函数
虚函数的定义是在基类中进行的,即把基类中需要定义为虚函数的成员函数声明为virtual,在基类中某个成员函数被声明为虚函数后,就可以在派生类中被重新定义,在派生类中重新定义,其函数原型,包括返回类型,函数名,参数个数和类型,参数的顺序都必须与基类中的原型完全一致。
- 在类的非静态成员函数前加上virtual就是虚函数。
- 只有类的非静态成员函数才可以被定义为虚函数。
Animal对象和Fish对象调用breathe函数时,产生了不同的结果
3. 虚函数重写的特殊例子
正常情况下,派生类重载基类的虚函数时,派生类的函数原型,包括返回类型,函数名,参数个数和类型,参数的顺序都必须与基类中的原型完全一致,但是有如下几个特殊例子。
1. 斜变
派生类重写基类的虚函数时,派生类的虚函数与基类返回值类型不同。即基类虚函数返回基类的对象的指针或引用,派生类返回派生类的指针或引用时,这时候称为斜变(了解)
2. 析构函数的重写(基类与派生类的析构函数名不同)
- 如果基类的析构函数是虚函数,那么无论派生类的析构函数是否定义为虚函数,则派生类的析构函数重写了基类的析构函数。
- 原理是在编译阶段,编译器对所有类的析构函数名称做了处理,统一命名为destructor,这才能构成函数重载的条件。
- 如果基类的析构函数为虚函数,则当派生类未定义析构函数是,编译器所生成的析构函数也为虚函数。
3.派生类的重写虚函数时,最前面可以不用加上virtual
4. 纯虚函数与抽象类
概念:
在虚函数的后面写上=0,则这个函数称为纯虚函数,包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能定义出具体的对象,派生类继承后也不能定义出对象,只有派生类重写纯虚函数才能定义出对象。为什么会有有抽象类呢?
其实抽象类是专门为我们现实生活中比较抽象的事物而设定的,例如动物,植物,车等等,这些名词都比较抽象,你总不能定义出一个动物,植物的对象,这些对象在我们现实生活中是很抽象的。所以抽象类一般可以定义在这些比较抽象的名词,然后再由派生类去延伸更具体的事物,例如动物底下有人,狗,猫等。再例如车再具体有奔驰 宝马,本田等。
五. 动态多态的原理
- 事实上,当将基类的成员函数breathe()声明为virtual即虚函数时,编译器在编译的阶段发现Animal类中有虚函数时。此时编译器就会为每个包含虚函数的类创建一个虚表,该表是一个一维数组,在这个数值中存放每个虚函数的地址,这个数组最后面放着一个nullptr。
- 如果一个类包含一张虚表,那么该类定义出来的对象中包含一个虚表指针。
- 下面的代码Animal类和Fish类都包含一个虚函数breathe(),因此编译器会为这两个类分别建立一个虚表。
- 当我们创建 a 和 f时,对象a中就会包含指向Animal类的虚表的指针,对象f中就会包含一个指向Fish类的虚表的指针。
- 由于不同的对象看到的虚表是不一样的,所以函数在调用的时候,会根据虚表中虚函数指针找到相对应的虚函数。Animal类的虚表中的存放的是Animal类的虚函数指针,Fish类的虚表中存放的是Fish类的虚函数指针。
- 同类型的对象指向的是同一个虚表.a1,a2,a3中的_vfptr中的值相同,指向同一个虚表。
Animal a1;
Animal a2;
Animal a3;
- 如果一个类中定义多个虚函数,那么虚表中就有多个虚函数的地址。由于Fish类没有重写sleep()的虚函数,Fish类中就存放Animal类的sleep()虚函数指针.(如果派生类没有重写基类的虚函数,那么派生类中虚表包含的是基类的虚函数指针)
1.写一个程序打印虚表中的虚函数的地址。
思路:
- 取出b的地址,强制转换为int*,
- 然后解引用就取出b最前面的4个字节,这个值就是指向虚表的指针
- 再转换成VFUNC*类型,因为虚表是存储VFUNC类型数组。
- 虚表指针传递给PrintVTable进行打印虚表
- 需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的-生成-清理解决方案,再编译就好了。
六. 多继承的虚函数表
一个派生类如果继承多个有虚函数的基类,那么该派生类就会存在多个虚函数表。例如:
- Derive中继承了Base1和Base2,由于Base1和Base2都有虚函数,所以Derive会存放两个虚表指针指向两个虚表,其中一个虚表是存放Base1类的虚函数的地址或者Derive重写Base1虚函数的地址,另一个虚表是存放Base2类的虚函数或者Derive重写Base2虚函数的地址。
- 由于Derive没有重写Base1和Base2中的虚函数,所以虚函数表1和虚函数表2分别存放的是Base1和Base2中的虚函数地址.
- 多继承的派生类的未重写的虚函数放在第一继承基类的虚函数表中
总结:
- 当类中的定义虚函数的时候,相对应的对象它们的多了一个成员_vfptr指针,这个指针指向的是函数指针数组(虚函数表),这张表是用来放虚函数的地址的,每个虚函数的地址存放在这张表是固定的。
- 当派生类去重写基类的虚函数的时候,则派生类的虚函数表会将相对应的虚函数的地址给修改为派生类重写的虚函数的地址。
七. 笔试题
1. 为什么多态函数的参数是基类的指针或者引用 ,参数是基类对象不行?
- 如果是调用是基类对象,那么当派生类給传值給多态函数的时候,只会将派生类的成员赋值給临时对象,不会把虚表指针給赋值过去,因为基类对象包含的虚表指针都是一样的。不会因为你是派生类构造出来的基类,虚表指针就是派生类虚表。因此传值是不会构成多态的。
多态函数的参数是传对象:
- 如果是参数是基类指针,那么通过派生类的指针构造出来的基类的指针,能够访问到派生类的虚表的指针。
2.对象中的虚表指针是在什么阶段的初始化呢?虚表又是在什么阶段生成的呢?
- 对象中的虚表指针是在构造函数的初始化列表阶段开始初始化的,虚表是在编译的时候就已经生成了。
3.虚表是存在进程地址空间中的哪个区域的?(栈,堆等)
- 思路:打印各个段的数据存放的地址,然后打印虚表地址,进行比对就可以得出虚表在哪个区域。虚表的地址存放在虚表指针中,拿到虚表指针就可以打印出虚表地址。
- 结果得出,虚表地址的代码段数据是最相近的,所以在vs编译器中,虚表是存在于代码段的。
4. sizeof(Base1)和sizeof(Base2)是多少?
- Base1对象的大小是4个字节,Base2对象的大小是8个字节,因为Base2中存在虚函数,所以Base2的对象最前面包含一个4个字节的虚表指针。
5.函数重载,重写,隐藏(重定义)的对比
函数重载
- 1.两个函数必须在同一个类域中。
- 2.函数名相同,参数类型/个数/顺序不同。
重写
- 1.两个函数分别在基类和派生类的类域中.
- 2.函数名/参数/返回值都必须相同(斜变除外)
- 3.重写的两个函数是为虚函数。
重定义
- 1.两个函数分别在基类和派生类的类域中。
- 2.函数名相同
- 3.重定义的两个函数不构成重载,构成隐藏。
6. inline函数可以是虚函数吗?
- 可以,不过编译器就忽略inline属性,这个函数就不再是inline,因为虚函数要放到虚表中去。
7. 静态成员没有this可以是虚函数吗?
- 不可以,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问到虚函数表,所以静态成员函数无法放进虚函数表。
8. 构造函数可以是虚函数吗?
不能,因为虚函数表指针是在构造函数的初始化阶段才进行初始化的
答案:B
- 解析:p是B类的指针,当他调用test函数时,test函数的参数中有一个隐藏的B类的this指针,当test函数去调用func函数时,自然会去调用B类的func,因为func重写了A类的虚函数,重写虚函数只是重写了函数内部的实现,不会对重写函数的声明,也就是void func(int val=1)不会被重写,因此B类的func函数是void func(int val=1){ std::cout<<"B->"<< val <<std::endl; },输出结果是B->1;