C++面向对象总结:虚指针与虚函数表,干货又来了
最近在逛B站的时候发现有候捷老师的课程,如获至宝。因此,跟随他的讲解又复习了一遍关于C++的内容,收获也非常的大,对于某些模糊的概念及遗忘的内容又有了更深的认识。
以下内容是关于虚函数表、虚函数指针,而C++中的动态绑定实现和这两个内容是分不开的。
当一个类在实现的时候,如果存在一个或以上的虚函数时,那么这个类便会包含一张虚函数表。而当一个子类继承并重写了基类的虚函数时,它也会有自己的一张虚函数表。
当我们在设计类的时候,如果把某个函数设置成虚函数时,也就表明我们希望子类在继承的时候能够有自己的实现方式;如果我们明确这个类不会被继承,那么就不应该有虚函数的出现。
下面是某个基类A的实现:
从下图中可以看到该类在内存中的存放形式,对于 虚函数的调用是通过查虚函数表来进行的 ,每个虚函数在虚函数表中都存放着自己的一个地址,而如何在 虚函数表中进行查找,则是通过虚指针来调用 ,在内存结构中它一般都会放在类最开始的地方,而对于普通函数则不需要通过查表操作。这张 虚函数表是 什么时候被创建的呢?它是 在编译的时候产生 ,否则这个类的结构信息中也不会插入虚指针的地址信息。
以下例子包含了继承关系:
以上三个类在内存中的排布关系如下图所示:
对于非虚函数,三个类中虽然都有一个叫 func2 的函数,但他们彼此互不关联,因此都是各自独立的,不存在重载一说,在调用的时候也不需要进行查表的操作,直接调用即可。
由于子类B和子类C都是继承于基类A,因此他们都会存在一个虚指针用于指向虚函数表。注意,假如子类B和子类C中不存在虚函数,那么这时他们将共用基类A的一张虚函数表,在B和C中用虚指针指向该虚函数表即可。但是,上面的代码设计时子类B和子类C中都有一个虚函数 vfunc1 ,因此他们就需要各自产生一张虚函数表,并用各自的虚指针指向该表。由于子类B和子类C都对 vfunc1 作了重载,因此他们有三种不同的实现方式,函数地址也不尽相同,在使用的时候需要从各自类的虚函数表中去查找对应的 vfunc1 地址。
对于虚函数 vfunc2 ,两个子类都没有进行重载操作,所以基类A、子类B和子类C将共用一个 vfunc2 ,该虚函数的地址会分别保存在三个类的虚函数表中,但他们的地址是相同的。
从上图可以发现,在类对象的头部存放着一个虚指针,该虚指针指向了各自类所维护的虚函数表,再通过查找虚函数表中的地址来找到对应的虚函数。
对于类中的数据而言,子类中都会包含父类的信息。如上例中的子类C,它自己拥有一个变量 m_data1 ,似乎是和基类中的 m_data1 重名了,但其实他们并不存在联系,从存放的位置便可知晓。
首先来说一说静态绑定: 静态绑定是指在 程序编译 过程中,把函数(方法或者过程)调用与响应调用所需的代码结合的过程(如何理解呢?)
来看一段代码:
可以看到调用的却是派生类的函数。
在没有加 virtual 关键字的时候,通过基类指针指向派生类对象时, 基类指针只能访问派生类的成员变量,但是不能访问派生类的成员函数。 这是因此在系统编译过程中,已经将area()函数和shape类绑定在一起了。
而动态绑定是在加了 virtual 关键字以后,派生类中的成员函数在重写的时候会自动生成自己的虚函数表(单独的一个地址),并通过虚指针指向该地址。
即:shape指针->vptr->Rectangle::area()
通过以上内容,我们可以知道在使用基类指针调用虚函数的时候,它能够根据所指的类对象的不同来正确调用虚函数。而这些能够正常工作,得益于虚指针和虚函数表的引入,使得在程序运行期间能够动态调用函数。
动态绑定有以下三项条件要符合:
使用指针进行调用
指针属于up-cast后的
调用的是虚函数
静态绑定,他们是类对象直接可调用的,而不需要任何查表操作,因此调用的速度也快于虚函数。
写在最后:其实每个人都有自己的选择,学编程,每一种编程语言的存在都有其应用的方向,选择你想从事的方向,去进行合适的选择就对了!对于准备学习编程的小伙伴,如果你想更好的提升你的编程核心能力(内功)不妨从现在开始!
编程学习书籍分享:
编程学习视频分享:
整理分享(多年学习的源码、项目实战视频、项目笔记,基础入门教程)
欢迎转行和学习编程的伙伴,利用更多的资料学习成长比自己琢磨更快哦!
对于C/C++感兴趣可以关注小编在后台私信我:【编程交流】一起来学习哦!可以领取一些C/C++的项目学习视频资料哦!已经设置好了关键词自动回复,自动领取就好了!
C++|虚函数多态机制的虚函数表及其指针的内存布局及手动引用
继承机制下的虚函数及动态机制才是真正的面向对象技术,否则充其量也只是基于对象技术。
继承允许把对象作为它自己的类型或它的基类类型处理。这个能力很重要,因为它允许很多类型(从同一个基类派生的)被等价地看待就像它们是一个类型,允许同一段代码同样地工作在所有这些不同类型上。虚函数反映了一个类型与另一个类似类型之间的区别,只要这两个类型都是从同一个基类派生的。这种区别是通过其在基类中调用的函数的表现不同来反映的。例如,我们可以定义一个基类(包含虚函数)指针或引用做参数,那这个参数便可以是这个基类继承链上的任一子类对象,在基类扩充子类时,这一函数的代码不会受到影响。
拥有虚函数的类会有一个虚表,而且这个虚表存放在类定义模块的数据段中。模块的数据段通常存放定义在该模块的全局数据和静态数据,这样我们可以把虚表看作是模块的全局数据或者静态数据。
类的虚表会被这个类的所有对象所共享。类的对象可以有很多,但是他们的虚表指针都指向同一个虚表,从这个意义上说,我们可以把虚表简单理解为类的静态数据成员。值得注意的是,虽然虚表是共享的,但是虚表指针并不是,类的每一个对象有一个属于它自己的虚表指针。
虚表中存放的是虚函数的地址。
虚函数表其实质是一个虚函数指针数组。每一个包含虚函数表的类的对象中会由编译器增加一个指向虚函数指针数组的指针。
派生对象数组定义:
基类对象指针指向派生类对象,派生类对象内的虚函数表指针指向具体的虚函数:
ref
Bruce Eckel《Thinking in C++, 2nd ed. Volume 1》
-End-
C++|类的分类:类指针、类函数的类及是否包含指针成员的类
C++的类是抽象数据类型(Abstract Data Type,ADT)的最佳实践,实现了数据和操作数据的函数的封装,接口实现和使用的分离。类的分类可以让我们对C++的类有一个更深刻的认识。
Object Based : 面对的是单一class 的设计。
Object Oriented : 面对的是多重classes 的设计,classes 和classes 之间的关系,如对象的继承、组合等。
在C和C++中,对于数据的初始化和堆内存的释放对于程序的安全性来说至关重要,在C++的类中,通过构造函数和析构函数可以自动化完成这一过程。基于析构函数是否需要释放堆内存,类可以分为包含指针成员的类和不含指针成员的类,对于包含指针成员的类,通常会有堆内存的分配,由这个指针成员指向一块堆内存,所以在析构函数中对其释放就显得很重要,如string类。当然,不是说没有指针成员或不需要释放堆内存就不需要析构函数了,在特定场合,如一个类的对象计数,当析构时,也可以对一个计数的数据进行“–”操作。
即使程序员没有忘记释放堆内存,但因为潜在的一些异常会可能会改变程序既定的流程,从而可能使free()或delete语句没有机会执行。
2.1 Class without pointer member(s):complex类
上述complex类的数据成员并没有指针成员,所以也没有堆内存申请的动作,由此,如果其析构函数~complex()并不做特别的操作,使用其默认的析构函数即可。
2.2 Class with pointer member(s):string类
上述string类,其数据成员只有一根指针,用于指向申请的堆内存,堆内存使用完后需要析构,所以其析构函数就显得很重要了,需要重新定义:
同时,为避免浅拷贝而引起的潜在问题,拷贝构造与拷贝赋值操作符都需要重新定义:
C++不像Java,没有内存回收机制,堆内存需要手动释放始终不是一个最佳的选择。利用类的析构函数自动调用这一特性,似乎可以做一些事情。对,就是封装一个需要指向堆内存的指针,由析构函数来自动完成内存释放这一动作。智能指针的初衷即来自于此,当然,智能指针还可以定义一些更智能化的操作,如share_ptr的对象计数、unique_ptr的所有权转移。
下面这张图很好地说明了智能指针的内部结构和使用方法:
智能指针在语法上有三个很关键的地方,第一个是保存的外部指针,对应于上图的T* px,这个指针将代替传入指针进行相关传入指针的操作;第二个是重载“*”运算符,解引用,返回一个指针所指向的对象;第三个是重载“->”运算符,返回一个指针,对应于上图就是px。
在C++中,泛型编程的最佳实践就是STL库了。C++的类实现了数据结构和操作数据结构的函数的封装,相对于数据与操作的分离,这是一进步,但当两者的结合度不是很高时,或者想让作为算法的函数能够独立于特定的数据结构而有更高的通用性时,这就是泛型编程的思想。STL用类模板封装数据结构为容器,用函数模板封装算法库,通过封装容器类对象的指针的迭代器来联结算法库与容器类,实现算法对容器一定的独立性,从而实现其通用性。
迭代器也是一种智能指针,这里也存在上面提到的智能指针的三个要素,分别对应于下图的红色字体和黄色标注部分:
下图是迭代器重载的“*”和“->”重载符:
创建一个list迭代器对象,list::iterator ite; iterator是一个内部类,是list的内部类,这里的list用于保存Foo对象,也就是list模板定义里的class T,operator*()返回的是一个(*node).data对象,node是__link_type类型,然而__link_type又是__list_node<T>*类型,这里的T是Foo,所以node是__list_node<Foo>*类型,所以(*node).data得到的是Foo类型的一个对象,而&(operator*())最终得到的是该Foo对象的一个地址,即返回Foo* 类型的一个指针。
我们排序时,升序还是降序排列,设计的升序或降序排序算法只有一行代码的不同,如果写成的一个函数既可以做升序也可以做降序排序呢?用一个函数指针做参数即可。
函数的运算符是“()”,而类对\”()\”的重载即可以模仿函数的行为,称为仿函数或函数对象。仿函数是对函数指针的一种加强,仿函数相对于函数指针,既可以操作对象状态,还可以由编译器做一个内联调用,从而具有更佳的性能。
重上图可以看到,每个仿函数都是某个类重载“()”运算符,然后变成了“仿函数”,实质还是一个类的函数,但看起来具有函数的属性。每个仿函数其实在背后都集成了一个奇怪的类,如下图所示,这个类不用程序员手动显式声明。
标准库中的仿函数也同样继承了一个奇怪的类:
这个类的内容如下图所示,只是声明了一些东西,里面没有实际的变量或者函数。
ref
C++面对对象高级编程[侯捷]
https://www.bilibili.com/video/BV1S7411p7g2
-End-
本文作者及来源:Renderbus瑞云渲染农场https://www.renderbus.com
文章为作者独立观点不代本网立场,未经允许不得转载。