【C++】面向对象编程的三大特性:深入解析多态机制(三)

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介: 【C++】面向对象编程的三大特性:深入解析多态机制

【C++】面向对象编程的三大特性:深入解析多态机制(二)https://developer.aliyun.com/article/1617395


九、动态绑定与静态绑定

  • 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
  • 动态绑定又称为后期绑定(晚绑定),在程序运行期间,根据具体拿到的类型确定程序的具体行,调用具体的函数,也成为动态多态。

十、单继承与多继承的虚函数表

10.1 单继承的虚函数表

class Base
{
    public :
    virtual void func1() { cout<<"Base::func1" <<endl;}
    virtual void func2() {cout<<"Base::func2" <<endl;}
    private :
    int a;
};
class Derive :public Base
{
    public :
    virtual void func1() {cout<<"Derive::func1" <<endl;}
    virtual void func3() {cout<<"Derive::func3" <<endl;}
    virtual void func4() {cout<<"Derive::func4" <<endl;}
    private :
    int b;
};

调试窗口进行观察(不够准确)

子类继承了父类虚表,得到了Func2虚函数及其Func1完成了虚函数的重写;问题在于监视窗口观察不到Func3和Func4,这里是编译器的监视窗口故意隐藏。

内存窗口进行观察

如果通过内存窗口来观察的话,虽然我们可以大致确定就是Func3和Func4虚函数的地址,但是如何证明呢?这里就需要使用到了打印虚表中函数了

10.2 打印虚表中函数

通过调式窗口来看,虚表指针是存储在头4字节上的,虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr。

如果是函数指针数组话,类型是难以书写,可以使用typedef对于类型重定义typedef void(*VFPTR) (); (这里数组指针和函数指针重定义写法是比较特殊的)

如果需要取头4个字节,能不能直接强转为int类型就行。这里强转是没有用的,只有相同类型才能进行强制类型转化,那么怎么办?

10.2.1 指针高级用法

打印虚表中虚函数地址实现步骤

步骤:

  • 先取b的地址,强制成一个int*的指针,指针可以随便转,指针本质是地址编号是整型,虽然不能直接转化为int类型,但是可以通过int *类型的指针间接的转化,是一种指针高级用法。
  • 再解引用取值,就得到了b对象头4个字节的值,这个值就是指向虚表的指针
  • 再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组
  • 虚表指针传递给printVTTable进行打印虚表
  • 需要声明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最后面没有放nullptr,导致越界,这是编译器的问题,我们只需要清理解决方案,在次编译就行了
//得到数据,重新定义个函数指针数组
VFPTR* vTableb = (VFPTR*)(*(int*)&b);
PrintVTable(vTableb);
VFPTR* vTabled = (VFPTR*)(*(int*)&d);
PrintVTable(vTabled);

打印虚表中虚函数地址函数逻辑:

void PrintVTable(VFPTR vTable[])
{
    // 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数
    cout << " 虚表地址>" << vTable << endl;
    for (int i = 0; vTable[i] != nullptr; ++i)
    {
        printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
        VFPTR f = vTable[i];
        f();
    }
    cout << endl;
}

10.3 多继承中虚函数表

class Base1 
{
    public:
    virtual void func1() {cout << "Base1::func1" << endl;}
    virtual void func2() {cout << "Base1::func2" << endl;}
    private:
    int b1;
};
class Base2 
{
    public:
    virtual void func1() {cout << "Base2::func1" << endl;}
    virtual void func2() {cout << "Base2::func2" << endl;}
    private:
    int b2;
};
class Derive : public Base1, public Base2
{
    public:
    virtual void func1() {cout << "Derive::func1" << endl;}
    virtual void func3() {cout << "Derive::func3" << endl;}
    private:
    int d1;
};
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
    cout << " 虚表地址>" << vTable << endl;
    for (int i = 0; vTable[i] != nullptr; ++i)
    {
        printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
        VFPTR f = vTable[i];
        f();
    }
    cout << endl;
}
int main()
{
    Derive d;
    VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
    PrintVTable(vTableb1);
    VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d+sizeof(Base1)));
    PrintVTable(vTableb2);
    return 0;
}

从上面的可以观察出来,多继承体制中派生类是继承了两张虚表,同时继承下来的虚函数是不同的,至于为什么不放在一张虚表,可以想一下切片,如果只有一个切片,如何实现多态的指向谁调用谁的逻辑呢?

10.3.1 打印多继承中第二张虚表中虚函数的地址

第一种办法

VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d+sizeof(Base1))
                           PrintVTable(vTableb2);

第一种办法使用指针运算法则进行移动指针指向位置,但是只适应不考虑内存对齐等因素情况下。由于内存对齐等因素,可能会导致会导致指向错误。更加推荐下面通过取地址直接访问的办法

第二种方法:

十一、菱形继承、菱形虚拟继承

实践种我们不建议设计出菱形继承、菱形虚拟继承,一方面太复杂容易出现问题,另一方面这样的模型,访问基类成员有一定性能损耗。所以继承、菱形虚拟继承继承虚表情况,我们不就不需要看了,一般我们也不需要研究清楚,实践中也很少用,如果需要了解通过下面两篇链接文章。

C++ 虚函数表解析 | 酷 壳 - CoolShell

C++ 对象的内存布局 | 酷 壳 - CoolShell

11.1 菱形虚拟继承(简单了解)

菱形虚拟继承,每个类都有一个虚函数,除了虚表指针也有我们的虚基表指针。这里虚基表有存储两个偏移量一个是距离虚表的偏移量和距离共享虚基类A的偏移量。

这里由于虚基类A是共享的,B C类的虚函数不能放进去,所以只能单独建立虚表。没有继承父类的虚表,这里是不能利用父类的虚表,不能放放我自己的虚函数,A是共享,派生类单独建立虚表

十二、相关面试题

  1. inline函数可以是虚函数吗?
  • 答:可以,不过编译器就忽略inline属性,这个函数就不再是inline,因为虚函数要放到虚表中去
  1. 静态成员可以是虚函数吗?
  • 答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
  1. 构造函数可以是虚函数吗?
  • 答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。
  1. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?
  • 答:可以,并且最好把基类的析构函数定义成虚函数。
  1. 对象访问普通函数快还是虚函数更快?
  • 答:首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。
  1. 虚函数表是在什么阶段生成的,存在哪的?
  • 答:虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的

以上就是本篇文章的所有内容,在此感谢大家的观看!这里是店小二呀C++笔记,希望对你在学习C++语言旅途中有所帮助!

相关文章
|
28天前
|
存储 Java
深入探讨了Java集合框架中的HashSet和TreeSet,解析了两者在元素存储上的无序与有序特性。
【10月更文挑战第16天】本文深入探讨了Java集合框架中的HashSet和TreeSet,解析了两者在元素存储上的无序与有序特性。HashSet基于哈希表实现,添加元素时根据哈希值分布,遍历时顺序不可预测;而TreeSet利用红黑树结构,按自然顺序或自定义顺序存储元素,确保遍历时有序输出。文章还提供了示例代码,帮助读者更好地理解这两种集合类型的使用场景和内部机制。
38 3
|
30天前
|
存储 算法 Java
解析HashSet的工作原理,揭示Set如何利用哈希算法和equals()方法确保元素唯一性,并通过示例代码展示了其“无重复”特性的具体应用
在Java中,Set接口以其独特的“无重复”特性脱颖而出。本文通过解析HashSet的工作原理,揭示Set如何利用哈希算法和equals()方法确保元素唯一性,并通过示例代码展示了其“无重复”特性的具体应用。
41 3
|
1月前
|
缓存 JavaScript 前端开发
Vue3与Vue2生命周期对比:新特性解析与差异探讨
Vue3与Vue2生命周期对比:新特性解析与差异探讨
88 2
|
11天前
|
编译器 C# 开发者
C# 9.0 新特性解析
C# 9.0 是微软在2020年11月随.NET 5.0发布的重大更新,带来了一系列新特性和改进,如记录类型、初始化器增强、顶级语句、模式匹配增强、目标类型的新表达式、属性模式和空值处理操作符等,旨在提升开发效率和代码可读性。本文将详细介绍这些新特性,并提供代码示例和常见问题解答。
28 7
C# 9.0 新特性解析
|
1月前
|
编译器 程序员 定位技术
C++ 20新特性之Concepts
在C++ 20之前,我们在编写泛型代码时,模板参数的约束往往通过复杂的SFINAE(Substitution Failure Is Not An Error)策略或繁琐的Traits类来实现。这不仅难以阅读,也非常容易出错,导致很多程序员在提及泛型编程时,总是心有余悸、脊背发凉。 在没有引入Concepts之前,我们只能依靠经验和技巧来解读编译器给出的错误信息,很容易陷入“类型迷路”。这就好比在没有GPS导航的年代,我们依靠复杂的地图和模糊的方向指示去一个陌生的地点,很容易迷路。而Concepts的引入,就像是给C++的模板系统安装了一个GPS导航仪
104 59
|
16天前
|
自然语言处理 编译器 Linux
|
10天前
|
C# 开发者
C# 10.0 新特性解析
C# 10.0 在性能、可读性和开发效率方面进行了多项增强。本文介绍了文件范围的命名空间、记录结构体、只读结构体、局部函数的递归优化、改进的模式匹配和 lambda 表达式等新特性,并通过代码示例帮助理解这些特性。
22 2
|
12天前
|
PHP 开发者
PHP 7新特性深度解析及其最佳实践
【10月更文挑战第31天】本文将深入探讨PHP 7带来的革新,从性能提升到语法改进,再到错误处理机制的变革。我们将通过实际代码示例,展示如何高效利用这些新特性来编写更加健壮和高效的PHP应用。无论你是PHP新手还是资深开发者,这篇文章都将为你打开一扇窗,让你看到PHP 7的强大之处。
|
13天前
|
安全 编译器 PHP
PHP 8新特性解析与实践应用####
————探索PHP 8的创新功能及其在现代Web开发中的实际应用
|
16天前
|
Kubernetes Cloud Native 调度
云原生批量任务编排引擎Argo Workflows发布3.6,一文解析关键新特性
Argo Workflows是CNCF毕业项目,最受欢迎的云原生工作流引擎,专为Kubernetes上编排批量任务而设计,本文主要对最新发布的Argo Workflows 3.6版本的关键新特性做一个深入的解析。

推荐镜像

更多