第2章 C++相关知识(三)
2.21 虚函数与纯虚函数
在 C++ 中,虚函数和纯虚函数是实现多态的关键。虚函数允许通过基类指针或引用调用派生类中重写的函数,纯虚函数则用于定义接口,强制派生类提供特定的实现。
虚函数(Virtual Function)
虚函数是基类中声明的函数,允许派生类重写该函数。通过基类的指针或引用调用时,实际执行的是派生类的版本。虚函数实现了动态多态,即在运行时,根据对象的实际类型调用对应的函数。
语法:
Shape 类定义了虚函数 draw()。
Circle 和 Square 类重写了 draw() 函数。
基类指针 shape1 和 shape2 分别指向 Circle 和 Square 对象,并通过指针调用 draw() 函数,实际调用的是派生类的重写函数。
纯虚函数(Pure Virtual Function)
纯虚函数是没有函数体的虚函数,其声明中包含 = 0,表示该函数是纯虚函数。纯虚函数用于在基类中定义接口,要求派生类提供具体实现。
语法:
纯虚函数没有函数体,因此不能直接调用。在基类中声明纯虚函数后,基类变成抽象类,抽象类不能实例化对象,只能作为基类供派生类继承。
抽象类和派生类
- 包含纯虚函数的类是抽象类,抽象类不能被实例化。
- 派生类必须重写所有纯虚函数,才能实例化对象。如果派生类没有重写所有纯虚函数,派生类仍然是抽象类。
Shape 类是一个抽象类,因为它包含纯虚函数 draw()。
Circle 和 Square 类重写了 draw() 函数。
不能直接实例化 Shape 类对象,因为它包含纯虚函数。
在 main() 中,创建了 Circle 和 Square 类的对象,并通过基类指针调用重写的 draw() 函数。
总结:
- 虚函数允许基类指针调用派生类的函数,实现在运行时动态选择调用的函数,提供了多态性。
- 纯虚函数用于定义接口,要求派生类实现,基类成为抽象类,不能实例化。
- 抽象类是包含至少一个纯虚函数的类,它不能实例化对象,只能作为基类供派生类继承。
- 纯虚函数的作用是为派生类提供统一的接口规范,强制派生类实现特定的行为。
2.22 虚函数表(VTable)与虚函数指针(VPtr)
在 C++ 中,为了支持运行时的多态性,编译器通过虚函数表(VTable)和虚函数指针(VPtr)来实现虚函数的调用。以下是这些概念的详细解释:
虚函数表(VTable)
虚函数表是一个类的内部结构,它存储了类中所有虚函数的地址。对于每个包含虚函数的类,编译器会为它生成一个虚函数表。虚函数表本质上是一个指针数组,每个元素是指向类中虚函数实现的指针。
每个类:对于每个包含虚函数的类,编译器会为其生成一个虚函数表。
存储函数地址:虚函数表中的每个元素指向相应虚函数的实现代码。
与对象类型相关:每个类的虚函数表是特定于该类的,因此不同类的虚函数表是不同的。
虚函数指针(VPtr)
每个对象(对于包含虚函数的类)会有一个额外的指针,称为虚函数指针(VPtr),它指向该对象所属的虚函数表。通过虚函数指针,程序能够动态查找和调用正确的虚函数。
VPtr存在于对象内部:VPtr存在于对象的内存布局中,指向该对象的虚函数表。
动态调用:当通过基类指针调用虚函数时,编译器会利用VPtr查找虚函数表,然后根据表中的函数地址来调用对应的虚函数。
如何工作:
编译时:编译器为每个类生成虚函数表,存储类中所有虚函数的地址。
运行时:当创建一个对象时,该对象的虚函数指针(VPtr)会指向类的虚函数表。
当通过基类指针或引用调用虚函数时,程序首先通过VPtr查找虚函数表,找到正确的函数地址,然后调用该虚函数。
虚函数表的创建:
基类 Animal 定义了一个虚函数 sound()。
派生类 Dog 和 Cat 重写了 sound() 函数。
编译器为 Animal、Dog 和 Cat 类各自生成了一个虚函数表,分别存储对应类的 sound() 函数地址。
虚函数指针:
当通过基类指针(animal1 和 animal2)调用 sound() 时,程序会使用虚函数指针(VPtr)来查找对象的虚函数表。
对于 animal1,虚函数指针会指向 Dog 类的虚函数表,因此调用的是 Dog 类中的 sound() 函数。
对于 animal2,虚函数指针会指向 Cat 类的虚函数表,因此调用的是 Cat 类中的 sound() 函数。
多态:
通过基类指针调用虚函数,实现了动态多态。具体调用哪个版本的虚函数(Dog 或 Cat)取决于对象的实际类型。
总结:
虚函数表(VTable):每个包含虚函数的类都有一个虚函数表,存储类中所有虚函数的地址。
虚函数指针(VPtr):每个对象会有一个虚函数指针,指向该对象所属类的虚函数表。
运行时多态:通过基类指针或引用调用虚函数时,程序使用虚函数指针(VPtr)查找虚函数表,从而动态调用正确的虚函数。
虚函数表和虚函数指针的作用:使得通过基类指针可以实现对派生类重写函数的调用,从而实现了多态性,提升了程序的灵活性和扩展性。
2.23 虚函数可以内联吗?
在 C++ 中,虚函数和内联函数是两种不同的优化技术,它们有不同的工作原理。
虚函数的工作原理
虚函数在运行时通过虚函数表(VTable)进行动态绑定。每个包含虚函数的类会有一个虚函数表,该表存储了虚函数的地址。通过基类指针或引用调用虚函数时,编译器会使用虚函数表来查找实际的函数地址,并在运行时调用正确的虚函数。
- 虚函数表(VTable):每个类有一个虚函数表,表中存储虚函数的地址。
- 虚函数指针(VPtr):每个对象有一个指向虚函数表的指针(VPtr),用来访问虚函数表。
由于虚函数的调用依赖于运行时的虚函数表,编译器无法在编译时确定调用哪个具体的函数。这就导致虚函数调用是在运行时动态决定的。
内联函数的工作原理
内联函数(Inline Function)是由编译器在编译阶段将函数调用的代码直接插入到调用处的函数。这种机制可以减少函数调用的开销,提高性能。内联函数要求编译器能够在编译时确定函数的实现,因此只能在编译期间进行替换。
- 内联函数:通常用inline关键字声明,编译器在编译时将函数体直接插入到调用点,从而消除了函数调用的开销。
虚函数和内联函数的冲突
- 动态绑定:虚函数调用的决定是由运行时的虚函数表来执行的,而内联函数要求在编译时知道函数的实现。由于虚函数的调用在编译期无法确定具体的函数,因此无法进行内联优化。
- 运行时调用 vs. 编译时替换:虚函数的调用发生在运行时,通过虚函数表查找函数地址。而内联函数是通过在编译时替换函数调用来消除调用开销,这两者的机制是不兼容的。
因此,虚函数不能直接被标记为内联函数,因为编译器无法在编译时确定具体调用哪个版本的虚函数,而内联要求在编译时已经确定函数体。
结论:虚函数不能是内联函数
由于虚函数的调用需要动态绑定,编译器无法在编译时确定函数体,因此无法在编译时进行内联优化。因此,虚函
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
作者简介:仅用大半年时间0基础天坑急转嵌入式开发,逆袭成功拿下华为、vivo、小米等15个offer,面试经验60+,收藏20+面经,分享自己的求职历程与学习心得。 专栏内容:最新求职与学习经验,详细讲解了嵌入式开发的学习路径、项目经验分享、简历优化技巧、面试心得及实习经验,从测评,笔试,技术面,HR面,AI面,主管面,谈薪一站式服务,助你突破技术瓶颈、打破信息差,争取更多大厂offer。