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语言指针与函数
C语言指针函数就是函数中用到了指针的函数,主要是有以下两种方式
- 以指针为参数的函数
- 以指针为返回值的函数
指针做函数参数
学习函数的时候,讲了函数的参数都是值拷贝,在函数里面改变形参的值,实参并不会发生改变。如下图:
每个函数都有一个独立的栈区,在函数传参的过程中,是把实参的值拷贝给形参,修改形参的值并不能作用到实参。如果想要通过形参改变实参的值,就需要传入实参的地址,可以通过寻址方式作用到实参上,如下图:
想要修改实参的值,需要传入实参的地址,故想要修改该指针变量的指向需要传入指针变量的地址,也就是二级指针。多级指针中也是依次类推,数据结构中常有二级指针传参。
示例程序| 传参的方式动态申请一维数组
传参的方式修改一级指针的值,需要传入二级指针,通过寻址的方式修改一级指针,如下测试代码:
运行结果如下:
示例程序| 封装函数操作数组
通常在封装函数操作数字类(int ,float,double,…)数组一定要传入数组长度,操作字符串类通常不需要,因为字符串存在字符串结束标记。例如封装遍历数组函数和字符串比较函数,代码如下:
运行结果如下:
当然比较函数你也可以返回0,-1,1,只需要在字符串比较函数中分类讨论下即可。
指针做函数返回值
指针当做函数返回值和普通函数一样,只是返回值类型不同而已,既然返回是一个指针,*指针等效变量,故*函数调用也可以等效变量。把指针当做函数返回值注意项:
- 不要返回临时变量的地址
- 可以返回动态申请的空间的地址
- 可以返回静态变量和全局变量的地址
当函数返回临时变量的地址时,地址中存储的数据随着函数调用完会被回收掉,导致获取垃圾值。如下测试代码:
运行结果如下:
在vs开发工具中会友善给予提醒,希望看到这类提醒当做错误处理,及时改善,友善提醒如下:
示例程序| 返回值的方式动态申请一维数组
可以返回动态申请的空间的地址,堆区内存需要调用free函数手动释放,如下测试代码:
运行结果如下:
示例程序| 用字符串初始化堆区内存并返回首地址
其实和数字类的操作没什么太大区别,唯一要注意的是字符串申请统计长度用strlen,申请是可见长度加1,拷贝赋值用strcpy完成,如下测试代码:
运行结果如下:
什么是函数指针
如果在程序中定义了一个函数,那么在运行时系统就会为这个函数代码分配一段存储空间,这段存储空间的首地址称为这个函数的地址。获取函数地址有以下两种方式:
- 函数名
- &函数名
既然是地址我们就可以定义一个指针变量来存放,这个指针变量就叫作函数指针变量,简称函数指针。函数指针的唯一作用就是调用函数,函数指针没有++和 –运算
如何创建函数指针
函数返回值类型 (*指针变量名) (函数参数列表);
简单来说一句话,用(*变量名) 替换函数名,剩下的照抄即可,形参名可写可不写就是函数指针变量。如下函数的函数指针创建:
如何通过函数指针调用函数
函数指针可以通过不同的初始化方式,调用除了函数名不同,其他类型相同的所有函数。调用方式有以下两种:
- 直接函数指针名替换函数名去调用函数
- (*函数指针)替换函数名的方式去调用函数
推荐使用第一种方式,代码看起来比较简单。如下测试代码:
运行结果如下:
回调函数
回调函数就是以函数指针作为某个函数的参数,函数指针比较重要的应用就是回调函数,在Windows SDK,多线程,事件处理中大量用到回调函数。函数指针变量可以作为某个函数的参数来使用的,回调函数就是一个通过函数指针调用的函数。简单讲:回调函数是由别人的函数执行时调用你实现的函数。通俗的讲:你到一个商店买东西,没有货,留给店员电话,有货了,打电话给你,然后你去取货。在这个例子里,你的电话号码就叫回调函数,你把电话留给店员就叫登记回调函数,店里后来有货了叫做触发了回调关联的事件,店员给你打电话叫做调用回调函数,你到店里去取货叫做响应回调事件。如下测试代码:
通常salesperson是第三方封装好的,我们只需要实现salesperson函数指针,通过salesperson去调用自己的函数,通常别人设计的回调函数都会绑定事件,目前初步接触了解下。运行结果如下:
万能指针充当函数指针使用前必须要强制类型转换,函数指针的类型就是去掉变量名即可 ,如下测试代码:
运行结果如下:
右左法则
首先找到标识符,然后往右看,再往左看,每当遇到圆括号时,就应该调转阅读方向,一旦解析完圆括号里面的所有东西,就跳出圆括号,重复这个过程直到整个声明解析完毕。
示例1| int (*func)(int *p)
首先找到那个标识符,就是func,它的外面有一对圆括号,而且左边是一个*号,这说明func是一个指针,然后跳出这个圆括号,先看右边,也是一个圆括号,这说明(*func)是一个函数,而func是一个指向这类函数的指针,就是一个函数指针,这类函数具有int*类型的形参,返回值类型是 int。
示例2| int (*func)(int *p, int (*f)(int*))
func被一对括号包含,且左边有一个*号,说明func是一个指针,跳出括号,右边也有个括号,那么func是一个指向函数的指针,这类函数具有int *和int (*)(int*)这样的形参,返回值为int类型。再来看一看func的形参int (*f)(int*),类似前面的解释,f也是一个函数指针,指向的函数具有int*类型的形参,返回值为int。
示例3| int (*func[5])(int *p)
func右边是一个[]运算符,说明func是一个具有5个元素的数组,func的左边有一个*,说明func的元素是指针,要注意这里的*不是修饰 func的,而是修饰func[5]的,原因是[]运算符优先级比*高,func先跟[]结合,因此*修饰的是func[5]。跳出这个括号,看右边,也是一对圆括号,说明func数组的元素是函数类型的指针,它所指向的函数具有int*类型的形参,返回值类型为int。
示例4| int (*(*func)[5])(int *p)
func被一个圆括号包含,左边又有一个*,那么func是一个指针,跳出括号,右边是一个[]运算符号,说明func是一个指向数组的指针,现在往左看,左边有一个*号,说明这个数组的元素是指针,再跳出括号,右边又有一个括号,说明这个数组的元素是指向函数的指针。总结一下就是:func是一个指向数组的指针,这个数组的元素是函数指针,这些指针指向具有int*形参,返回值为int类型的函数。
示例5| int (*(*func)(int *p))[5]
func是一个函数指针,这类函数具有int*类型的形参,返回值是指向数组的指针,所指向的数组的元素是具有5个int元素的数组。
示例6| int (*(*(*func)(int *))[5])(int *)
func是一个函数指针,这类函数的返回值是一个指向数组的指针,所指向数组的元素也是函数指针,指向的函数具有int*形参,返回值为int。
实际当中,需要声明一个复杂指针时,如果把整个声明写成上面所示的形式,对程序可读性是一大损害。应该用typedef来对声明逐层分解,增强可读性,如果对typedef不懂的,后续讲解。
如果阁下正好在学习C/C++,看文章比较无聊,不妨关注下关注下小编的视频教程,通俗易懂,深入浅出,一个视频只讲一个知识点。视频不深奥,不需要钻研,在公交、在地铁、在厕所都可以观看,随时随地涨姿势。
本文作者及来源:Renderbus瑞云渲染农场https://www.renderbus.com
文章为作者独立观点不代本网立场,未经允许不得转载。