C++面向对象
C++面向对象三大特性:封装、继承、多态
封装
封装,即隐藏对象的属性和成员函数实现细节,仅对外公开接口。
- 控制程序对类属性的读取和修改权限
继承
继承,即子类继承父类的成员变量和方法。
继承最大的一个好处就是代码复用。假如两个类有一些相同的属性和方法,我们就可以将这些相同的部分,抽取到父类中,让两个子类继承父类。这样,两个子类就可以重用父类中的代码,避免代码重复写多遍。不过,过度使用继承,继承层次过深,也会导致代码可读性、可维护性变差。
什么是多继承?
子类继承了多个父类,会继承所有父类的属性和成员函数。
什么是虚继承?
当多继承时,如果该派生类的父类是继承于同一基类,该派生类就会存在两份父类的基类的成员,为了解决多继承时的命名冲突和冗余数据问题,C++ 提出了虚继承,使得在派生类中只保留一份间接基类的成员。
-
实例
C++标准库中的 iostream 类就是一个虚继承的实际应用案例。iostream 从 istream 和 ostream 直接继承而来,而 istream 和 ostream 又都继承自一个共同的名为 base_ios 的类,是典型的菱形继承。此时 istream 和 ostream 必须采用虚继承,否则将导致 iostream 类中保留两份 base_ios 类的成员。

多态
什么是多态?
同一个行为,针对不同的对象实例,具有多种不同表现形式或形态的能力。
概念
主要分为静态多态和动态多态,静态多态主要是重载,在编译的时候就已经确定;动态多态是用虚函数机制实现的,在运行期间动态绑定。
- 覆盖是指子类重写从基类继承过来的函数,函数名、返回值、参数列表都必须和基类相同。当子类的对象调用成员函数的时候,如果成员函数有被覆盖则调用子类中覆盖的版本,否则调用从基类继承过来的函数。
- 重载指在相同作用域中存在多个同名的函数,这些函数的参数表不同,编译器根据函数不同的形参表对同名函数的名称做修饰,然后这些同名函数就成了不同的函数。
虚函数实现多态
C++中的虚函数的作用主要是实现了多态的机制。关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。
什么是虚函数?什么是重写?
虚函数是基类中带有 virtual 关键字的成员函数,派生可以进行重写或者覆盖
子类有个和父类完全相同(函数名,形参,返回值都相同,协变和析构函数除外)的虚函数,就称子类虚函数重写父类虚函数
虚函数实现多态的条件?
1) 一是虚函数重写,重写就是用来设置不同状态的
2)二是对象调用虚函数时必须是指针或者引用(只有指针和引用才能获取虚函数指针内容)
什么是隐藏?
当基类没有声明虚函数,但是派生类有重名函数,那么此时就会隐藏基类函数,变成普通成员函数,发生于编译而不是运行时。
虚函数表
虚函数表是指在每个包含虚函数的类中都存在着一个****函数地址的数组。当我们用父类的指针来操作一个子类的时候,这张虚函数表指明了实际所应该调用的函数。
虚函数表的内存模型
虚函数表是指在每个包含虚函数的类中都存在着一个函数地址的数组,C++的编译器保证虚函数表的指针存在于对象实例中最前面的位置。
- 基类

-
子类
如果要生成子类虚表,就要经过三个步骤:(发生于运行时)
- 第一步,将父类虚表内容拷贝到子类虚表上;
- 第二步,将子类重写的虚函数覆盖掉表中父类的虚函数;
- 第三步,如果子类有新增加的虚函数,按声明次序加到最后

继承中的多态
-
单继承无虚函数覆盖
虚函数按声明顺序存放,父类虚函数在子类虚函数前面.
![image-20220713194416111]()
-
单继承有虚函数覆盖
覆盖的 f ( ) 替代原有父类虚函数位置, 没覆盖的不变
![image-20220713194517254]()
-
多继承无虚函数覆盖
每个父类都有自己的虚表,子类成员函数被放入第一个父类表中
![image-20220713194641621]()
-
多继承有虚函数覆盖
三个父类虚表中的 f ( ) 都会被子类函数指针覆盖
![image-20220713194719846]()
-
多继承规则:
多继承子类未重写的虚函数放在第一个继承父类部分的虚函数表中,继承的虚表都会覆盖
-
重复/菱形继承: B类数据重复,具有二义性.
可以通过父类的虚继承解决。
纯虚函数
纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。定义纯虚函数的目的在于,使派生类仅仅只是继承函数的接口。
定义
在基类中实现纯虚函数的方法是在函数原型后加“=0” virtualvoid GetName() =0
抽象类
包含纯虚函数的类是抽象类,抽象类不能定义实例,但可以声明指向实现该抽象类的具体类的指针或引用。
动态绑定
本来就是基类对象执行基类的函数,派生类对象执行派生类函数。如果基类有virtual关键字,则基类中则会有一个指向虚函数表的指针,此时如果派生类定义了同名函数,则虚函数标中的函数指针则定位到了派生类的函数。这就是函数覆盖的现象。至于函数隐藏则更简单了,没有virtual关键字的情况下都是静态联编,自然是基类对象调用基类函数,派生类对象调用派生类函数了。如果有virtual关键字时,但是参数列表不一样,自然会调用派生类的函数了。
基类类调用虚函数时,会通过传递过来的子类对象的虚函数指针找到对应的虚函数地址,进行执行,如果调用的不是虚函数,直接会调用基类的成员函数。
this指针
为什么需要this指针?
c++的数据和操作也是分开存储,并且每一个非内联成员函数只会诞生一份函数实例,也就是说多个同类型的对象会共用一块代码,c++通过提供特殊的对象指针(this指针)区分调用的对象,this指针指向被调用的成员函数所属的对象。
概念
c++规定,this指针是隐含在对象成员函数内(实际存放于形参中)的一种指针。当一个对象被创建后,它的每一个成员函数都含有一个系统自动生成的隐含指针this,用以保存这个对象的地址,也就是说虽然我们没有写上this指针,编译器在编译的时候也是会加上的。
友元
为什么需要友元函数?
在C++中,我们使用类对数据进行了隐藏和封装,类的数据成员一般都定义为私有成员,成员函数一般都定义为公有的,以此提供类与外界的通讯接口。但是,有时需要定义一些函数,这些函数不是类的一部分,但又需要频繁地访问类的数据成员,这时可以将这些函数定义为该函数的友元函数。
友元函数声明
友元函数是可以直接访问类的私有成员的非成员函数。它是定义在类外的普通函数,它不属于任何类,但需要在类的定义中加以声明,声明时只需在友元的名称前加上关键字friend,其格式如下:
friend 类型 函数名(形式参数);
- 友元函数的声明可以放在类的私有部分,也可以放在公有部分,它们是没有区别的,都说明是该类的一个友元函数。
- 一个函数可以是多个类的友元函数,只需要在各个类中分别声明。
- 友元函数的调用与一般函数的调用方式和原理一致。
友元类
友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的隐藏信息(包括私有成员和保护成员)。
当希望一个类可以存取另一个类的私有成员时,可以将该类声明为另一类的友元类。定义友元类的语句格式如下:
friend class 类名;
其中:friend和class是关键字,类名必须是程序中的一个已定义过的类。
面试常考
-
inline函数可以是虚函数吗?
不能,因为inline函数没有地址,无法放到虚函数表中
-
静态成员可以是虚函数吗?
不能, 因为静态成员函数没有 this 指针, 因为有 this 指针才能访问到虚表指针,有虚表指针才能找到虚表从而调用实际应该调用的函数。
-
构造函数可以是虚函数吗? 虚函数指针在什么时候生成的的?
不能,因为对象中的虚表指针是在构造函数初始化列表阶段才初始化的
-
析构函数可以是虚函数吗?什么场景下析构函数是虚函数?
可以, 并且最好把基类的析构函数定义成虚函数。当父类指针指向子类对象时,如果析构函数不是虚函数,析构就只会释放父类对象,造成内存泄漏。(因为析构重名,只能调用一个,调用默认的父类析构函数)。 定义成虚函数后,调用析构时就会取出虚表指针找到实际应该调用的函数。(指针虽然都是父类类型,但是指针内取出的虚表是不一样的,所以析构能调用子类析构)
-
对象访问普通函数快还是虚函数更快?
首先如果是普通对象,是一样快的,如果是指针对象或者是引用对象,则调用的普通函数快,因为普通对象在编译时就确定地址了,虚函数构成多态,运行时调用虚函数需要到虚函数表中去查找。
-
友元函数可以是虚函数吗?
友元不是成员函数,只有成员函数才可以是虚拟的,因此友元不能是虚拟函数。但可以通过让友元函数调用虚拟成员函数来解决友元的虚拟问题。
-
派生类的构造函数和析构函数执行过程?
当派生类的对象生成时,会从最顶层的基类开始逐层往下执行所有基类的构造函数,最后再执行自身的构造函数;当派生类对象消亡时,会先执行自身的析构函数,然后从底向上依次执行各个基类的析构函数。





浙公网安备 33010602011771号