C++面向对象总结——虚指针与虚函数表

最近在逛B站的时候发现有候捷老师的课程,如获至宝。因此,跟随他的讲解又复习了一遍关于C++的内容,收获也非常的大,对于某些模糊的概念及遗忘的内容又有了更深的认识。
以下内容是关于虚函数表、虚函数指针,而C++中的动态绑定实现和这两个内容是分不开的。
一,虚函数表、虚指针
当一个类在实现的时候,如果存在一个或以上的虚函数时,那么这个类便会包含一张虚函数表。而当一个子类继承并重写了基类的虚函数时,它也会有自己的一张虚函数表。
当我们在设计类的时候,如果把某个函数设置成虚函数时,也就表明我们希望子类在继承的时候能够有自己的实现方式;如果我们明确这个类不会被继承,那么就不应该有虚函数的出现。
下面是某个基类A的实现:
class A {
public:
virtual void vfunc1();
virtual void vfunc2();
void func1();
void func2();
private:
int m_data1, m_data1;
};
从下图中可以看到该类在内存中的存放形式,对于虚函数的调用是通过查虚函数表来进行的,每个虚函数在虚函数表中都存放着自己的一个地址,而如何在虚函数表中进行查找,则是通过虚指针来调用,在内存结构中它一般都会放在类最开始的地方,而对于普通函数则不需要通过查表操作。这张虚函数表是什么时候被创建的呢?它是在编译的时候产生,否则这个类的结构信息中也不会插入虚指针的地址信息。

以下例子包含了继承关系:
class A {
public:
virtual void vfunc1();
virtual void vfunc2();
void func1();
void func2();
private:
int m_data1, m_data1;
};
class B : public A {
public:
virtual void vfunc1();
void func2();
private:
int m_data3;
};
class C : public B {
public:
virtual void vfunc1();
void func2();
private:
int m_data1, m_data4;
};
以上三个类在内存中的排布关系如下图所示:

- 对于非虚函数,三个类中虽然都有一个叫 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 重名了,但其实他们并不存在联系,从存放的位置便可知晓。
二,关于动态绑定
首先来说一说静态绑定:静态绑定是指在程序编译过程中,把函数(方法或者过程)调用与响应调用所需的代码结合的过程(如何理解呢?)
来看一段代码:
#include <iostream>
using namespace std; class Shape {
protected:
int width, height;
public:
Shape(int a,int b):width(a),height(b){}
int area()
{
cout << "Parent class area :" << endl;
return 0;
}
};
//将Rectangle类继承Shape类
class Rectangle : public Shape {
public:
Rectangle(int a,int b) :Shape(a, b) { }
int area()
{
cout << "Rectangle class area :" <<width*height<< endl;
return 0;
}
}; // 程序的主函数
int main()
{
Shape* shape;//定义shpae类指针
Rectangle rec(10, 7);//派生类对象
// 基类指针指向派生类对象(存储矩形的地址)
shape = &rec;
// 调用矩形的求面积函数 area
shape->area();
return 0;
}

可以看到调用的却是派生类的函数。
在没有加virtual关键字的时候,通过基类指针指向派生类对象时,基类指针只能访问派生类的成员变量,但是不能访问派生类的成员函数。这是因此在系统编译过程中,已经将area()函数和shape类绑定在一起了。
而动态绑定是在加了virtual关键字以后,派生类中的成员函数在重写的时候会自动生成自己的虚函数表(单独的一个地址),并通过虚指针指向该地址。
即:shape指针->vptr->Rectangle::area()
通过以上内容,我们可以知道在使用基类指针调用虚函数的时候,它能够根据所指的类对象的不同来正确调用虚函数。而这些能够正常工作,得益于虚指针和虚函数表的引入,使得在程序运行期间能够动态调用函数。
动态绑定有以下三项条件要符合:
- 使用指针进行调用
- 指针属于up-cast后的
- 调用的是虚函数
静态绑定,他们是类对象直接可调用的,而不需要任何查表操作,因此调用的速度也快于虚函数。
C++面向对象总结——虚指针与虚函数表的更多相关文章
- C++对象内存模型2 (虚函数,虚指针,虚函数表)
从例子入手,考察如下带有虚函数的类的对象内存模型: class A { public: virtual void vfunc1(); virtual void vfunc2(); void func1 ...
- C++对象内存模型2 (虚函数,虚指针,虚函数表)(转)
class A { public: virtual void vfunc1(); virtual void vfunc2(); void func1(); void func2(); virtual ...
- C++虚函数和虚函数表
前导 在上面的博文中描述了基类中存在虚函数时,基类和派生类中虚函数表的结构. 在派生类也定义了虚函数时,函数表又是怎样的结构呢? 先看下面的示例代码: #include <iostream> ...
- 深入理解类成员函数的调用规则(理解成员函数的内存为什么不会反映在sizeof运算符上、类的静态绑定与动态绑定、虚函数表)
本文转载自:http://blog.51cto.com/9291927/2148695 总结: 一.成员函数的内存为什么不会反映在sizeof运算符上? 成员函数可以被看作是类 ...
- C++ 关于类与对象在虚函数表上唯一性问题 浅析
[摘要] 非常多教材上都有介绍到虚指针.虚函数与虚函数表.有的说类对象共享一个虚函数表,有的说,一个类对象拥有一个虚函数表.还有的说,不管用户声明了多少个类对象,可是,这个VTABLE虚函数表仅仅有一 ...
- 从零开始学C++之虚函数与多态(一):虚函数表指针、虚析构函数、object slicing与虚函数
一.多态 多态性是面向对象程序设计的重要特征之一. 多态性是指发出同样的消息被不同类型的对象接收时有可能导致完全不同的行为. 多态的实现: 函数重载 运算符重载 模板 虚函数 (1).静态绑定与动态绑 ...
- 深入剖析C++多态、VPTR指针、虚函数表
在讲多态之前,我们先来说说关于多态的一个基石------类型兼容性原则. 一.背景知识 1.类型兼容性原则 类型兼容规则是指在需要基类对象的任何地方,都可以使用公有派生类的对象来替代.通过公有继承,派 ...
- C++学习 - 虚表,虚函数,虚函数表指针学习笔记
http://blog.csdn.net/alps1992/article/details/45052403 虚函数 虚函数就是用virtual来修饰的函数.虚函数是实现C++多态的基础. 虚表 每个 ...
- C++虚函数表解析(图文并茂,非常清楚)( 任何妄图使用父类指针想调用子类中的未覆盖父类的成员函数的行为都会被编译器视为非法)good
C++中的虚函数的作用主要是实现了多态的机制.关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数.这种技术可以让父类的指针有“多种形态”,这是一种泛型技术 ...
随机推荐
- 揭开Docker的面纱
开新坑了,开始挖坑Docker了,兄弟们.为什么需要Docker呢?Docker是什么?这里开始揭开Docker的面纱. 一.为什么需要Docker 可能每个开发人员都有一种困扰,软件开发完之后部署项 ...
- Linux云计算-04_Linux用户及权限管理
Linux是一个多用户的操作系统,引入用户,可以更加方便管理Linux服务器,系统默认需要以一个用户的身份登录,而且在系统上启动进程也需要以一个用户身份器运行,用户可以限制某些进程对特定资源的权限控制 ...
- keycloak~自定义redirect_uri的方法
在使用keycloak集成springboot的过程中,对于需要授权访问的接口,它会跳到keycloak里进行登录,之前有个redirect_uri,登录成功后会跳回本客户端,而这个地址默认没有修改的 ...
- 『动善时』JMeter基础 — 54、JMeter聚合报告详解
目录 1.聚合报告介绍 2.聚合报告界面详解 3.聚合报告中信息点说明 (1)百分位数的说明 (2)吞吐量说明 提示:聚合报告组件的使用和察看结果树组件的使用方式相同.本篇文章主要是详细的介绍一下聚合 ...
- SpringBoot中如何监听两个不同源的RabbitMQ消息队列
spring-boot如何配置监听两个不同的RabbitMQ 由于前段时间在公司开发过程中碰到了一个问题,需要同时监听两个不同的rabbitMq,但是之前没有同时监听两个RabbitMq的情况,因此在 ...
- linux学习之路第六天(文件目录类第二部分)
文件目录类 1.cat指令 作用:查看文件内容,是以只读的方式打开. 基本语法 cat [选项] 要查看的文件 常用选项 -n; 使用细节: cat只能浏览文件,而不能修改文件,通常会和more一起使 ...
- IT面试最全逻辑题,收藏后成功率提高10%
这是小学二年级的数学题: 猫妈妈钓到一些鱼,平均分给了7只小猫,每只小猫分到的鱼和剩下的鱼刚好一样多.猫妈妈最多钓到了多少条鱼? 这个是出来工作后的现场面试题: [1]假设有一个池塘,里面有无穷多的水 ...
- 基于Flink构建全场景实时数仓
目录: 一. 实时计算初期 二. 实时数仓建设 三. Lambda架构的实时数仓 四. Kappa架构的实时数仓 五. 流批结合的实时数仓 实时计算初期 虽然实时计算在最近几年才火起来,但是在早期也有 ...
- 嵌入式Redis服务器在Spring Boot测试中的使用
1.概述 Spring Data Redis提供了一种与Redis实例集成的简单方法. 但是,在某些情况下,使用嵌入式服务器比使用真实服务器创建开发和测试环境更方便. 因此,我们将学习如何设置和使用嵌 ...
- shell脚本编写规范和相关变量类型
shell编程规范与变量 一.shell脚本概述 ① 什么是shell? Linux中有哪些shell? ② shell的作用 ③ 用户的登录shell ④ shell脚本 ...