目录
面向过程和面向对象
面向对象的三大特性
多态的条件和原理
虚表存放位置、虚表指针初始化时间
析构函数为什么要为虚函数
构造函数为什么不能为虚函数
虚函数和纯虚函数的实现原理
虚函数和纯虚函数的区别
面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。
面向过程强调将程序分解为一系列的过程(函数、过程等),这些过程按照一定的顺序执行,最终完成整个程序的功能。
面向对象(OOP)的,关注的是对象,将一件事拆分成不同的对象,靠对象之间的交互完成。
在面向对象中,程序员通常不需要手动管理程序中的数据和控制流程,而是通过封装、继承、多态等方式来实现代码复用和模块化。
面向对象有三大特性
我们就外卖系统来看看面向过程和面向对象之间的区别:
面向过程更注重步骤和操作,而面向对象更注重将问题抽象为对象,并通过对象之间的交互来解决问题。
让我们考虑一个生活中的例子来说明面向过程和面向对象的区别。
假设你要制作一杯咖啡。
- 面向过程:在面向过程的方式下,你可能会按照一系列的步骤来制作咖啡。你需要准备杯子、煮沸水、研磨咖啡豆、将咖啡粉加入过滤器、倒入热水、搅拌、加入牛奶和糖等。整个过程是基于一系列的步骤和操作。
- 面向对象:在面向对象的方式下,你可以将咖啡制作过程中的对象抽象为类。你可以有一个咖啡机类,它有一个制作咖啡的方法。该方法内部包含了煮沸水、研磨咖啡豆、加入过滤器等操作。你还可以有一个杯子类,它有一个倒入咖啡的方法。你可以创建一个咖啡机对象和一个杯子对象,然后通过调用方法来制作咖啡。
封装:封装是将数据和方法封装在一个单元中,以便控制访问和保护数据的完整性。封装隐藏了实现细节,只暴露必要的接口供外部使用。
例子:考虑一个电视机。电视机封装了内部的电子元件和电路板,对外部用户只提供了开关、音量调节、频道切换等操作的接口。用户无需知道电视机内部的工作原理,只需要通过提供的接口来使用电视机。
继承:继承是一种机制,允许从现有类派生出新类,并继承父类的属性和方法。通过继承,子类可以重用父类的代码,扩展父类的功能,并添加自己的特定行为。
例子:考虑一个动物类的继承关系。有一个父类Animal,它定义了通用的属性和方法,例如名称、年龄和移动方式等。然后可以创建子类,如Dog、Cat和Bird,这些子类继承了Animal类的属性和方法,并可以添加自己特定的属性和方法,如Dog类可以有狗叫的方法,Cat类可以有抓老鼠的方法。
多态:多态是指同一种操作或方法在不同的对象上有不同的行为。多态性将父类类型的变量指向子类对象,并且根据实际对象的类型,在运行时选择合适的方法来执行。多态允许使用一个统一的接口来处理不同的对象类型。即同一段代码可以根据对象的不同而执行不同的操作。多态提高了代码的灵活性和可扩展性。
抢红包,大家都有点击这个动作,但是有人会获得红包,有人却没有,这就有了不同的行为!
虚函数是C++中用于实现多态的机制。
构成多态的条件:
多态构成的两个条件
多态的原理:
- 虚函数的主要作用是实现多态的机制. 关于多态, 简单地说就是用父类型的指针指向其子类对象, 然后通过父类的指针调用实际子类的成员函数, 这种技术可以让父类的指针有多种形态. 如果调用非虚函数, 则无论实际对象是什么类型的, 都执行父类类型所定义的函数. 非虚函数在编译时根据调用该函数的对象的类型而确定. 如果调用虚函数, 则直到运行时才能确定调用哪个函数.
- 虚函数的作用是动态联编, 也就是在程序的运行阶段动态的选择合适的成员函数, 在定义了虚函数后, 可以在子类中对虚函数重新定义. 如果在子类中没有对虚函数重新定义, 则它继承其父类的虚函数.
- 虚函数是通过一张虚函数表来调用的; 实际上虚表当中存储的就是虚函数的地址, 子类虽然继承了父类的虚函数, 但是子类对父类的虚函数进行了重写; 因此, 子类对象的虚表当中存储的是父类的虚函数的地址和重写的虚函数的地址. 这就是为什么虚函数的重写也叫做覆盖, 覆盖就是指虚表中虚函数地址的覆盖, 重写是语法的叫法, 覆盖是原理层的叫法.
那父类和子类的析构函数构成重写的意义何在呢?
试想以下场景:分别new一个父类对象和子类对象,此时没有将析构函数设置为虚函数. 并均用父类指针指向它们,然后分别用delete调用析构函数并释放对象空间。
//分别new一个父类对象和子类对象,并均用父类指针指向它们 Person* p1 = new Person; Person* p2 = new Student; //使用delete调用析构函数并释放对象空间 delete p1; delete p2;
在这种场景下就会导致程序崩溃, delete p1和delete p2 调用的都是父类的析构函数, 而我们所期望的是 p1 调用父类的析构函数, p2 调用的是子类的析构函数, 我们期望的是一种多态行为.
只有父类和子类的析构函数构成了重写, 才能使得 delete 按照我们的预期进行析构函数的调用. 期望一种多态的行为!
1、这是因为虚函数的调用需要一个已经构造完毕的对象,而构造函数本身就是用于创建对象的初始函数。虚函数表和虚函数指针在构造函数阶段还没有建立,因此无法进行虚函数的调用。
2、从存储空间上看, 虚函数表指针是存储在对象的内存空间上的. 如果将构造函数设置为虚函数, 就需要通过虚表来调用, 但是对象还没有实例化, 也就是内存空间还没有, 内存还没有怎么拿到虚表指针呢, 没有虚表指针怎么找到虚函数表呢? 所以构造函数不能是虚函数! (相互矛盾了)
3、虚表是在构造函数初始化列表阶段完成初始化的, 存储在数据段. 所以构造函数不可能成为虚函数
虚函数的实现原理:
纯虚函数的实现原理: