【C++语言】继承和多态常见的面试问题
一、概念考察
1. 什么是多态
多态是面向对象编程中的一个核心概念,他允许不同的对象对同一消息(方法调用)做出响应,即同一个接口可以被不同的底层实现所使用。多态分为两种主要类型:
-
静态多态(Static Polymorphism):
-
也称为编译时多态。
-
通过函数重载(Function Overloading)和运算符重载(Operator Overloading)实现。
-
在编译时根据函数的参数列表或运算符的操作数类型来确定调用的具体函数版本。
-
-
动态多态(Dynamic Polymorphism):
-
也称为运行时多态。
-
通过虚函数(Virtual Functions)和继承实现。
-
在运行时根据对象的实际类型来调用相应的函数版本。
-
2. 什么是重载、重写、重定义
(1)重载(Overloading)
-
定义:在同一个作用域中,允许定义多个同名函数或方法,只要它们的参数列表不同(参数类型、参数个数或参数顺序不同)。
-
特点:
-
重载发生在同一个类中或同一命名空间中。
-
编译器根据参数列表来区分不同的重载版本。
-
(2)重写(Override)
-
定义:在派生类中重新实现从基类继承的虚函数(Virtual Function),以提供特定的行为。
-
特点:
-
重写必须在继承体系中发生。
-
被重写的函数必须是虚函数。
-
派生类的重写函数与基类的虚函数具有相同的函数签名(名称、参数列表和常量修饰符)。
-
(3)重定义(Hide/Shadow)
-
定义:在派生类中定义了一个与基类同名的函数(无论是否是虚函数),从而隐藏了基类中的同名函数。
-
特点:
-
重定义不需要基类的函数是虚函数。
-
派生类的函数会隐藏基类中所有同名函数(包括重载版本)。
-
3. 多态的实现原理
多态的实现原理主要依赖于 虚函数表(Virtual Table,简称vtable) 和 虚函数指针(Virtual Pointer,简称vptr)。
(1)虚函数表(vtable)
-
每个包含虚函数的类都有一个虚函数表(vtable)。
-
vtable是一个静态数组,存储了该类中所有虚函数的地址。
-
在运行时,通过对象的虚函数指针(vptr)来访问vtable,从而调用正确的虚函数版本。
(2)虚函数指针(vptr)
-
每个对象都有一个虚函数指针(vptr),指向其所属类的虚函数表(vtable)。
-
当通过基类指针或引用调用虚函数时,运行时会通过vptr找到对应的vtable,再通过vtable找到正确的虚函数地址。
4. inline函数可以是虚函数吗
可以。
-
解释:
inline
和virtual
是两个不同的概念,它们并不冲突。-
inline
用于建议编译器将函数的代码直接插入到调用点,以减少函数调用的开销。 -
virtual
用于实现动态多态,允许派生类重写函数。
-
不过,需要注意的是,即使函数被声明为inline
,编译器也可能根据实际情况决定是否真正内联展开。对于虚函数来说,由于需要通过vtable进行动态绑定,内联展开的机会可能较少。
5. 静态成员可以是虚函数吗
不可以。
-
解释:
-
静态成员函数属于类本身,而不是类的某个对象。
-
虚函数的调用依赖于对象的vptr和vtable,而静态成员函数没有vptr。
-
因此,静态成员函数不能被声明为虚函数。
-
6. 构造函数可以是虚函数吗
不可以。
-
解释:
-
构造函数用于初始化对象,而虚函数的调用依赖于对象的vptr和vtable。
-
在构造对象时,对象的vptr尚未初始化,因此无法通过vptr调用虚函数。
-
另外,构造函数的目的是创建对象,而虚函数的目的是在运行时动态选择函数版本,这两者的目的不一致。
-
7. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数??
(1)析构函数可以是虚函数吗?
可以。
在C++中,析构函数可以被声明为虚函数。实际上,在某些情况下,将析构函数声明为虚函数是非常重要的。
(2)什么场景下需要将析构函数声明为虚函数?
当类被设计为基类,并且可能通过基类指针或引用删除派生类对象时,析构函数必须是虚函数。否则,可能会导致资源泄漏或其他未定义行为。
8. 对象访问普通函数快还是虚函数更快?
普通函数的访问速度通常比虚函数更快。
-
普通函数:
-
在编译时,函数调用地址已经确定,因此调用普通函数时不需要额外的查找操作。
-
编译器可以直接生成调用指令,效率较高。
-
-
虚函数:
-
虚函数的调用需要通过对象的虚函数表(vtable)进行动态绑定。
-
在运行时,程序需要通过对象的虚函数指针(vptr)找到对应的vtable,再通过vtable找到虚函数的地址,然后调用该函数。
-
这个过程涉及到额外的指针查找操作,因此效率相对较低。
-
总结:
-
如果对性能要求极高,且不需要动态多态,建议使用普通函数。
-
如果需要动态多态,必须使用虚函数,即使它会带来一些性能开销。
9. 虚函数是在什么阶段生成的,存在哪里的??
虚函数的生成和存储机制如下:
(1)生成阶段
-
虚函数的生成发生在 编译阶段。
-
当编译器遇到一个类中定义了虚函数时,它会为该类生成一个虚函数表(vtable)。
-
vtable是一个静态数组,存储了该类中所有虚函数的地址。
(2)存储位置
-
每个包含虚函数的类都有一个 虚函数表(vtable)。
-
每个对象都有一个 虚函数指针(vptr),指向其所属类的vtable。
-
vtable存储在程序的 只读数据段 中,因为vtable的内容在运行时不会改变。
-
vptr存储在对象的内存中,指向对应的vtable。
10. C++菱形继承的问题?虚继承的原理?
(1)C++菱形继承的问题
菱形继承是指一个类(D)同时继承了两个子类(B和C),而这两个子类又共同继承自同一个基类(A)。这种结构会导致以下问题:
-
数据冗余:D类会通过B和C继承两份A类的成员变量和方法。
-
二义性:当D类的对象访问A类的成员时,编译器无法确定应该使用哪一份A类的成员。
(2)虚继承的原理
虚继承是为了解决菱形继承中的数据冗余和二义性问题而引入的一种继承方式。通过虚继承,基类在派生类中只有一份实例,无论该基类被继承了多少次。
-
实现原理:
-
虚继承时,派生类不会直接包含基类的成员,而是通过一个特殊的指针(称为“虚基类指针”)来间接访问基类的成员。
-
虚基类指针存储在派生类对象的内存中,指向实际的基类对象。
-
当访问虚基类的成员时,程序会通过虚基类指针找到唯一的基类对象。
-
11. 什么是抽象类?抽象类的作用?
(1)什么是抽象类?
抽象类(Abstract Class)是一个不能被实例化的类,它通常包含一个或多个纯虚函数(Pure Virtual Function)。纯虚函数是一个没有实现的虚函数,其作用是为派生类提供一个接口规范。
(2)抽象类的作用
-
提供接口规范:抽象类定义了一组接口(纯虚函数),派生类必须实现这些接口。这确保了派生类具有一致的行为。
-
实现多态:抽象类通常用于定义一个通用的接口,通过基类指针或引用调用派生类的实现,从而实现动态多态。
-
防止实例化:抽象类不能被直接实例化,这避免了创建没有实际意义的对象。
二、选择题
继承的面向对象方法可以让你变得富有
动态绑定是面向对象涉及语言中的一种机制
继承允许我们覆盖重写父类的实现细节,父类的实现对于子类是可见的
内联函数不能是虚函数