C++语言基础篇(三)

22、多态的实现

多态一般是指继承加虚函数实现的多态,对于重载来说,实际上基于的原理是,编译器为函数⽣成符号表时的不同规则,重载只是⼀种语⾔特性,与多态⽆关,与⾯向对象也⽆关,但这⼜是 C++中增加的新规则,所以也算属于 C++,所以如果⾮要说重载算是多态的⼀种,那就可以说: 多态可以分为静态多态和动态多态

静态多态其实就是重载,因为静态多态是指在编译时期就决定了调⽤哪个函数,根据参数列表来决定;

动态多态是指通过⼦类重写⽗类的虚函数来实现的,因为是在运⾏期间决定调⽤的函数,所以称为动态多态,

⼀般情况下我们不区分这两个时所说的多态就是指动态多态。

动态多态的实现与虚函数表,虚函数指针相关。

扩展: ⼦类是否要重写⽗类的虚函数?⼦类继承⽗类时, ⽗类的纯虚函数必须重写,否则⼦类也是⼀个虚类不可实例化。 定义纯虚函数是为了实现⼀个接⼝,起到⼀个规范的作⽤,规范继承这个类的程序员必须实现这个函数。

23、虚函数相关(虚函数表,虚函数指针),虚函数的实现原理

首先我们来说一下,C++中多态的表象,在基类的函数前面加上关键字virtual关键字,在派生类中,重写该函数,运行时会根据对象的实际类型来调用相应的函数。如果对象的类型是派生类,就调用派生类(子类)的函数。如果是基类,就调用基类的函数。

实际上,当⼀个类中包含虚函数时,编译器会为该类⽣成⼀个虚函数表,保存该类中虚函数的地址,同样,派⽣类继承基类,派⽣类中⾃然⼀定有虚函数,所以编译器也会为派⽣类⽣成⾃⼰的虚函数表。当我们定义⼀个派⽣类对象时,编译器检测该类型有虚函数,所以为这个派⽣类对象⽣成⼀个虚函数指针,指向该类型的虚函数表,这个虚函数指针的初始化是在构造函数中完成的。

后续如果有⼀个基类类型的指针,指向派⽣类,那么当调⽤虚函数时,就会根据所指真正对象的虚函数表指针去寻找虚函数的地址,也就可以调⽤派⽣类的虚函数表中的虚函数以此实现多态。

24、编译器处理虚函数表应该如何处理

对于派生类来说,编译器建立虚函数表的过程其实一共是三个步骤:

  • 拷⻉基类的虚函数表,如果是多继承,就拷⻉每个有虚函数基类的虚函数表。
  • 当然还有⼀个基类的虚函数表和派⽣类⾃身的虚函数表共⽤了⼀个虚函数表,也称为某个基类为派⽣类的主基类
  • 查看派⽣类中是否有重写基类中的虚函数, 如果有,就替换成已经重写的虚函数地址;
    查看派⽣类是否有⾃身的虚函数,如果有,就追加⾃身的虚函数到⾃身的虚函数表中。

Derived *pd = new D(); B *pb = pd; C *pc = pd; 其中 pb, pd, pc 的指针位置是不同的,要注意的是派⽣类的⾃身的内容要追加在主基类的内存块后。
在这里插入图片描述

25、析构函数一般写成虚函数的原因

直观的讲,是为了降低内存泄漏的可能性。举例来说,一个基类的指针指向一个派生类的对象,在使用完毕准备销毁时,如果基类的析构函数没有定义成虚函数,那么编译器根据指针类型就会认为当前对象的类型是基类,调用基类的析构函数该对象的析构函数的函数地址早就被绑定为基类的析构函数)仅执⾏基类的析构,派⽣类的⾃身内容将⽆法被析构,造成内存泄漏。如果基类的析构函数定义成虚函数,那么编译器就可以根据实际对象,执⾏派⽣类的析构函数,再执⾏基类的析构函数,成功释放内存。

26、构造函数为什么一般不定义为虚函数

  • 虚函数调用只需知道“部分的”信息,即只需要知道函数接口,而不需要知道对象的具体类型。但是,我们要创建一个对象的话,只需要知道对象的完整信息的。特别是,需要知道要创建对象的确切类型,因此,构造函数不应该被定义成虚函数;
  • ⽽且从⽬前编译器实现虚函数进⾏多态的⽅式来看,虚函数的调⽤是通过实例化之后对象的虚函数表指针来找到虚函数的地址进⾏调⽤的,如果说构造函数是虚的,那么虚函数表指针则是不存在的,⽆法找到对应的虚函数表来调⽤虚函数,那么这个调⽤实际上也是违反了先实例化后调⽤的准则。

27、构造函数或析构函数中调用虚函数会怎么样

实际上是不应该在构造函数或析构函数中调⽤虚函数的,因为这样的调⽤其实并不会带来所想要的效果。

举例来说就是,有⼀个动物的基类,基类中定义了⼀个动物本身⾏为的虚函数 action_type(),在基类的构造函数中调⽤了这个虚函数。

派⽣类中重写了这个虚函数,我们期望着根据对象的真实类型不同,⽽调⽤各⾃实现的虚函数,但实际上当我们创建⼀个派⽣类对象时,⾸先会创建派⽣类的基类部分,执⾏基类的构造函数,此时,派⽣类的⾃身部分还没有被初始化,对于这种还没有初始化的东⻄, C++选择当它们还不存在作为⼀种安全的⽅法。

也就是说构造派⽣类的基类部分是,编译器会认为这就是⼀个基类类型的对象,然后调⽤基类类型中的虚函数实现,并没有按照我们想要的⽅式进⾏。即对象在派⽣类构造函数执⾏前并不会成为⼀个派⽣类对象。

在析构函数中也是同理,派⽣类执⾏了析构函数后,派⽣类的⾃身成员呈现未定义的状态,那么在执⾏基类的析构函数中是不可能调⽤到派⽣类重写的⽅法的。所以说,我们不应该在构造函数或析构函数中调⽤虚函数,就算调⽤⼀般也不会达到我们想要的结果。

28、析构函数的作用,如何起作用

构造函数只是起初始化值的作⽤,但实例化⼀个对象的时候,可以通过实例去传递参数,从主函数传递到其他的函数⾥⾯,这样就使其他的函数⾥⾯有值了。规则,只要你⼀实例化对象,系统⾃动回调⽤⼀个构造函数,就是你不写,编译器也⾃动调⽤⼀次。

析构函数与构造函数的作⽤相反,⽤于撤销对象的⼀些特殊任务处理,可以是释放对象分配的内存空间;特点:析构函数与构造函数同名,但该函数前⾯加~。

析构函数没有参数,也没有返回值,⽽且不能重载,在⼀个类中只能有⼀个析构函数。 当撤销对象时,编译器也会⾃动调⽤析构函数。 每⼀个类必须有⼀个析构函数,⽤户可以⾃定义析构函数,也可以是编译器⾃动⽣成默认的析构函数。⼀般析构函数定义为类的公有成员。

29、构造函数的执行顺序 ? 析构函数的执行顺序?

构造函数顺序

  • 基类构造函数。如果有多个基类,则构造函数的调⽤顺序是某类在类派⽣表中出现的顺序,⽽不是它们在成员初始化表中的顺序。
  • 成员类对象构造函数。如果有多个成员类对象则构造函数的调⽤顺序是对象在类中被声明的顺序,⽽不是它们出现在成员初始化表中的顺序。
  • 派⽣类构造函数。

析构函数顺序

  • 调⽤派⽣类的析构函数;
  • 调⽤成员类对象的析构函数;
  • 调⽤基类的析构函数。

30、纯虚函数(应用于接口继承和实现继承)

实际上,纯虚函数的出现就是为了让继承可以出现多种情况:

  • 有时我们希望派⽣类只继承成员函数的接⼝
  • 有时我们⼜希望派⽣类既继承成员函数的接⼝,⼜继承成员函数的实现,⽽且可以在派⽣类中可以重写成员函数以实现多态
  • 有的时候我们⼜希望派⽣类在继承成员函数接⼝和实现的情况下,不能重写缺省的实现。

其实,声明⼀个纯虚函数的⽬的就是为了让派⽣类只继承函数的接⼝,⽽且派⽣类中必需提供⼀个这个纯虚函数的实现,否则含有纯虚函数的类将是抽象类,不能进⾏实例化。

对于纯虚函数来说,我们其实是可以给它提供实现代码的,但是由于抽象类不能实例化,调⽤这个实现的唯⼀⽅式是在派⽣类对象中指出其 class 名称来调⽤。

31、静态绑定和动态绑定的介绍

说起静态绑定和动态绑定,我们首先要知道静态类型和动态类型,静态类型就是它在程序中被声明时所采⽤的类型,在编译期间确定。动态类型则是指“⽬前所指对象的实际类型”,在运⾏期间确定。

静态绑定,⼜名早绑定,绑定的是静态类型,所对应的函数或属性依赖于对象的静态类型,发⽣在编译期间。

动态绑定,⼜名晚绑定,绑定的是动态类型,所对应的函数或属性依赖于动态类型,发⽣在运⾏期间。

⽐如说, virtual 函数是动态绑定的,⾮虚函数是静态绑定的,缺省参数值也是静态绑定的。这⾥呢,就需要注意,我们不应该重新定义继承⽽来的缺省参数,因为即使我们重定义了,也不会起到效果。因为⼀个基类的指针指向⼀个派⽣类对象,在派⽣类的对象中针对虚函数的参数缺省值进⾏了重定义, 但是缺省参数值是静态绑定的,静态绑定绑定的是静态类型相关的内容,所以会出现⼀种派⽣类的虚函数实现⽅式结合了基类的缺省参数值的调⽤效果,这个与所期望的效果不同。

32、深拷贝和浅拷贝的区别

当出现类的等号赋值时,会调⽤拷⻉函数,在未定义显示拷⻉构造函数的情况下, 系统会调⽤默认的拷⻉函数-即浅拷⻉,它能够完成成员的⼀⼀复制。当数据成员中没有指针时,浅拷⻉是可⾏的。

但当数据成员中有指针时,如果采⽤简单的浅拷⻉,则两类中的两个指针指向同⼀个地址,当对象快要结束时,会调⽤两次析构函数,⽽导致指野指针的问题。

所以,这时必需采⽤深拷⻉。深拷⻉与浅拷⻉之间的区别就在于深拷⻉会在堆内存中另外申请空间来存储数据,从⽽也就解决来野指针的问题。简⽽⾔之,当数据成员中有指针时,必需要⽤深拷⻉更加安全。

猜你喜欢

转载自blog.csdn.net/qq_43679351/article/details/124971945