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

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 【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++语言旅途中有所帮助!

目录
打赏
0
1
1
0
21
分享
相关文章
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
【C++篇】深度解析类与对象(中)
在上一篇博客中,我们学习了C++类与对象的基础内容。这一次,我们将深入探讨C++类的关键特性,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载、以及取地址运算符的重载。这些内容是理解面向对象编程的关键,也帮助我们更好地掌握C++内存管理的细节和编码的高级技巧。
【C++篇】深度解析类与对象(上)
在C++中,类和对象是面向对象编程的基础组成部分。通过类,程序员可以对现实世界的实体进行模拟和抽象。类的基本概念包括成员变量、成员函数、访问控制等。本篇博客将介绍C++类与对象的基础知识,为后续学习打下良好的基础。
基于红黑树的局域网上网行为控制C++ 算法解析
在当今网络环境中,局域网上网行为控制对企业和学校至关重要。本文探讨了一种基于红黑树数据结构的高效算法,用于管理用户的上网行为,如IP地址、上网时长、访问网站类别和流量使用情况。通过红黑树的自平衡特性,确保了高效的查找、插入和删除操作。文中提供了C++代码示例,展示了如何实现该算法,并强调其在网络管理中的应用价值。
C++ `noexcept` 关键字的深入解析
`noexcept` 关键字在 C++ 中用于指示函数不会抛出异常,有助于编译器优化和提高程序的可靠性。它可以减少代码大小、提高执行效率,并增强程序的稳定性和可预测性。`noexcept` 还可以影响函数重载和模板特化的决策。使用时需谨慎,确保函数确实不会抛出异常,否则可能导致程序崩溃。通过合理使用 `noexcept`,开发者可以编写出更高效、更可靠的 C++ 代码。
99 1
深入解析C++中的函数指针与`typedef`的妙用
本文深入解析了C++中的函数指针及其与`typedef`的结合使用。通过图示和代码示例,详细介绍了函数指针的基本概念、声明和使用方法,并展示了如何利用`typedef`简化复杂的函数指针声明,提升代码的可读性和可维护性。
134 1
C# 9.0 新特性解析
C# 9.0 是微软在2020年11月随.NET 5.0发布的重大更新,带来了一系列新特性和改进,如记录类型、初始化器增强、顶级语句、模式匹配增强、目标类型的新表达式、属性模式和空值处理操作符等,旨在提升开发效率和代码可读性。本文将详细介绍这些新特性,并提供代码示例和常见问题解答。
98 7
C# 9.0 新特性解析
PHP 8新特性解析与实战应用####
随着PHP 8的发布,这一经典编程语言迎来了诸多令人瞩目的新特性和性能优化。本文将深入探讨PHP 8中的几个关键新功能,包括命名参数、JIT编译器、新的字符串处理函数以及错误处理改进等。通过实际代码示例,展示如何在现有项目中有效利用这些新特性来提升代码的可读性、维护性和执行效率。无论你是PHP新手还是经验丰富的开发者,本文都将为你提供实用的技术洞察和最佳实践指导。 ####
73 1
iOS 14隐私保护新特性深度解析####
随着数字时代的到来,隐私保护已成为全球用户最为关注的问题之一。苹果在最新的iOS 14系统中引入了一系列创新功能,旨在增强用户的隐私和数据安全。本文将深入探讨iOS 14中的几大隐私保护新特性,包括App跟踪透明度、剪贴板访问通知和智能防追踪功能,分析这些功能如何提升用户隐私保护,并评估它们对开发者和用户体验的影响。 ####
【C++11】包装器:深入解析与实现技巧
本文深入探讨了C++中包装器的定义、实现方式及其应用。包装器通过封装底层细节,提供更简洁、易用的接口,常用于资源管理、接口封装和类型安全。文章详细介绍了使用RAII、智能指针、模板等技术实现包装器的方法,并通过多个案例分析展示了其在实际开发中的应用。最后,讨论了性能优化策略,帮助开发者编写高效、可靠的C++代码。
84 2

热门文章

最新文章

推荐镜像

更多