【QandA C++】面向过程、面向对象、多态的原理、虚函数表、虚表指针、虚析构、虚构造、虚函数、纯虚函数等重点知识汇总

目录

面向过程和面向对象

面向对象的三大特性

多态的条件和原理

虚表存放位置、虚表指针初始化时间

析构函数为什么要为虚函数

构造函数为什么不能为虚函数

虚函数和纯虚函数的实现原理

虚函数和纯虚函数的区别


面向过程和面向对象

面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。

面向过程强调将程序分解为一系列的过程(函数、过程等),这些过程按照一定的顺序执行,最终完成整个程序的功能。

面向对象(OOP)的,关注的是对象,将一件事拆分成不同的对象,靠对象之间的交互完成。

在面向对象中,程序员通常不需要手动管理程序中的数据和控制流程,而是通过封装、继承、多态等方式来实现代码复用和模块化。

面向对象有三大特性

我们就外卖系统来看看面向过程和面向对象之间的区别:

  • 面向过程,我们的关注点应该是用户下单、骑手接单以及骑手送餐这三个过程。
  • 面向对象,那我们的关注点应该就是客户、商家以及骑手这三个类对象之间的关系。

面向过程更注重步骤和操作,而面向对象更注重将问题抽象为对象,并通过对象之间的交互来解决问题。

让我们考虑一个生活中的例子来说明面向过程和面向对象的区别。

假设你要制作一杯咖啡。

  1. 面向过程:在面向过程的方式下,你可能会按照一系列的步骤来制作咖啡。你需要准备杯子、煮沸水、研磨咖啡豆、将咖啡粉加入过滤器、倒入热水、搅拌、加入牛奶和糖等。整个过程是基于一系列的步骤和操作。
  2. 面向对象:在面向对象的方式下,你可以将咖啡制作过程中的对象抽象为类。你可以有一个咖啡机类,它有一个制作咖啡的方法。该方法内部包含了煮沸水、研磨咖啡豆、加入过滤器等操作。你还可以有一个杯子类,它有一个倒入咖啡的方法。你可以创建一个咖啡机对象和一个杯子对象,然后通过调用方法来制作咖啡。

面向对象的三大特性

封装:封装是将数据和方法封装在一个单元中,以便控制访问和保护数据的完整性。封装隐藏了实现细节,只暴露必要的接口供外部使用。

例子:考虑一个电视机。电视机封装了内部的电子元件和电路板,对外部用户只提供了开关、音量调节、频道切换等操作的接口。用户无需知道电视机内部的工作原理,只需要通过提供的接口来使用电视机。

继承:继承是一种机制,允许从现有类派生出新类,并继承父类的属性和方法。通过继承,子类可以重用父类的代码,扩展父类的功能,并添加自己的特定行为。

例子:考虑一个动物类的继承关系。有一个父类Animal,它定义了通用的属性和方法,例如名称、年龄和移动方式等。然后可以创建子类,如Dog、Cat和Bird,这些子类继承了Animal类的属性和方法,并可以添加自己特定的属性和方法,如Dog类可以有狗叫的方法,Cat类可以有抓老鼠的方法。

多态多态是指同一种操作或方法在不同的对象上有不同的行为。多态性将父类类型的变量指向子类对象,并且根据实际对象的类型,在运行时选择合适的方法来执行。多态允许使用一个统一的接口来处理不同的对象类型。即同一段代码可以根据对象的不同而执行不同的操作。多态提高了代码的灵活性和可扩展性。

抢红包,大家都有点击这个动作,但是有人会获得红包,有人却没有,这就有了不同的行为!

多态的条件和原理

虚函数是C++中用于实现多态的机制。

构成多态的条件:

多态构成的两个条件

  1. 一是完成虚函数的重写,必须完成虚函数的重写是因为我们需要完成子类虚表当中虚函数地址的覆盖
  2. 二是必须使用父类的指针或者引用去调用虚函数。

多态的原理:

  1. 首先,定义一个父类,其中包含一个或多个虚函数。
  2. 子类重写虚函数
  3. 当通过基类的指针或引用调用虚函数时,实际上是通过虚表指针来找到虚函数地址,然后调用相应的函数。
  4. 虚表指针在对象的内存布局中作为一个隐藏的成员存在,并且对于每个对象只有一个虚表指针。
  5. 虚表指针指向类的虚函数表,根据对象的实际类型来查找相应的虚函数地址,实现了运行时的多态性和动态绑定。
  1. 虚函数的主要作用是实现多态的机制. 关于多态, 简单地说就是用父类型的指针指向其子类对象, 然后通过父类的指针调用实际子类的成员函数, 这种技术可以让父类的指针有多种形态. 如果调用非虚函数, 则无论实际对象是什么类型的, 都执行父类类型所定义的函数. 非虚函数在编译时根据调用该函数的对象的类型而确定. 如果调用虚函数, 则直到运行时才能确定调用哪个函数.
  2. 虚函数的作用是动态联编, 也就是在程序的运行阶段动态的选择合适的成员函数, 在定义了虚函数后, 可以在子类中对虚函数重新定义. 如果在子类中没有对虚函数重新定义, 则它继承其父类的虚函数.
  3. 虚函数是通过一张虚函数表来调用的; 实际上虚表当中存储的就是虚函数的地址, 子类虽然继承了父类的虚函数, 但是子类对父类的虚函数进行了重写; 因此, 子类对象的虚表当中存储的是父类的虚函数的地址和重写的虚函数的地址. 这就是为什么虚函数的重写也叫做覆盖, 覆盖就是指虚表中虚函数地址的覆盖, 重写是语法的叫法, 覆盖是原理层的叫法.

虚表存放位置、虚表指针初始化时间

  1. 虚函数表(vtable)是在构造函数初始化列表阶段初始化的, 存放在数据段
  2. 虚函数则位于代码段
  3. 对于有虚函数的类,对该类进行实例化时,在构造函数执行时会对虚表指针进行初始化,并且存在对象内存布局的最前面。
  4. 虚表指针(vptr)的位置取决于对象在哪.

析构函数为什么要为虚函数

  1. 如果父类的析构函数为虚函数, 此时子类析构函数只要定义, 无论是否加 virtual, 都与父类的析构函数构成重写.
  2. 虚析构可以保证当我们 new 一个子类, 然后用父类的指针指向这个子类对象, 释放父类指针时可以释放掉子类的空间, 可以防止内存泄漏!
  3. 析构函数如果没有虚函数的动态绑定功能, 就只根据指针的类型来进行调用, 而不是指针绑定的对象来进行调用, 所以没有虚函数的析构函数只是调用父类的析构函数

那父类和子类的析构函数构成重写的意义何在呢?

试想以下场景:分别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、虚表是在构造函数初始化列表阶段完成初始化的, 存储在数据段. 所以构造函数不可能成为虚函数

虚函数和纯虚函数的实现原理

虚函数的实现原理:

  1. 虚函数的主要作用是实现多态的机制. 关于多态, 简单地说就是用父类型的指针指向其子类对象, 然后通过父类的指针调用实际子类的成员函数, 这种技术可以让父类的指针有多种形态. 如果调用非虚函数, 则无论实际对象是什么类型的, 都执行父类类型所定义的函数. 非虚函数在编译时根据调用该函数的对象的类型而确定. 如果调用虚函数, 则直到运行时才能确定调用哪个函数.
  2. 虚函数的作用是动态联编, 也就是在程序的运行阶段动态的选择合适的成员函数, 在定义了虚函数后, 可以在子类中对虚函数重新定义. 如果在子类中没有对虚函数重新定义, 则它继承其父类的虚函数.
  3. 虚函数是通过一张虚函数表来调用的; 实际上虚表当中存储的就是虚函数的地址, 子类虽然继承了父类的虚函数, 但是子类对父类的虚函数进行了重写; 因此, 子类对象的虚表当中存储的是父类的虚函数的地址和重写的虚函数的地址. 这就是为什么虚函数的重写也叫做覆盖, 覆盖就是指虚表中虚函数地址的覆盖, 重写是语法的叫法, 覆盖是原理层的叫法.

纯虚函数的实现原理:

  1. 纯虚函数是在父类中声明的函数, 它在父类中没有定义. 但要求任何子类都要定义自己实现方法
  2. 在父类中实现纯虚函数的方法是在虚函数原型后加上"=0"
  3. 在很多情况下, 父类本身生成对象是不合理的, 例如: 动物作为一个父类可以派生出老虎, 狮子等子类, 但动物本身生成对象明显不合常理. 为了解决这个问题, 就要将函数定义为纯虚函数, 编译器要求在子类中必须予以重写以实现多态性
  4. 含有纯虚函数的类称为抽象类, 它不能生成对象. 必须在子类中也重新声明该函数(不要带=0), 否则该子类也不能实例化
  5. 纯虚函数的意义在于让所有的子类对象都可以执行纯虚函数的操作.
  6. 纯虚函数在类的虚表中对应的表项被赋值为0, 也就是指向了一个不存在的函数. 由于编译器绝对不允许有调用一个不存在的函数的可能, 所以该类不能生成对象. 在它的子类中, 除非重写此函数, 否则也不能生成对象.

虚函数和纯虚函数的区别

  1. 虚函数和纯虚函数可以定义在同一个类中, 含有纯虚函数的类被称为抽象类, 而只含有虚函数的类不能被称为抽象类
  2. 纯虚函数只有声明没有实现, 虚函数既有声明也有实现
  3. 虚函数的定义形式为: virtual flying(){ }; 纯虚函数的定义形式为: virtual flying() = 0 { };
  4. 虚函数可以有默认的实现,如果派生类没有对虚函数进行重写,将会使用基类中的默认实现。
  5. 纯虚函数没有默认的实现,派生类必须对纯虚函数进行重写并提供具体的实现。
  6. 含纯虚函数的类被称为抽象类,抽象类无法被实例化,只能用作基类。派生类必须实现抽象类中的所有纯虚函数,才能成为具体类。
  7. 总结来说,虚函数允许在基类和派生类之间实现多态性,而纯虚函数则更加强调接口的定义和派生类的实现。虚函数可以有默认实现,而纯虚函数必须在派生类中进行重写。纯虚函数的存在使得基类成为抽象类,不能被实例化,只能用作派生类的基类。

你可能感兴趣的:(c++,开发语言)