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

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

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


七、多态的原理(重点)

7.1 虚函数表

场景引入:计算sizeof(Base)大小

class Base
{
    public:
    virtual void Func1()
    {
        cout << "Func1()" << endl;
    }
    private:
    int _b = 1;
};
结果:sizeof(Base) == 8

具体解析:

  • 通过调式窗口,我们发现除了_b成员,还多了一个 _vfptr放在对象的前面。该指针称为虚函数表指针(v代表virtual,f代表function)。
  • 一个含有虚函数的类都至少都有一个虚函数表指针,因为虚函数地址要被放到虚函数表中的虚函数表中,虚函数表也简称虚表。
  • 在调试窗口中_vfptr位置跟平台有关系,有些平台可能会放置到对象的最后面。

7.2 派生类继承基类成员

class Base
{
    public:
    virtual void Func1()
    {
        cout << "Base::Func1()" << endl;
    }
    virtual void Func2()
    {
        cout << "Base::Func2()" << endl;
    }
    //非虚函数
    void Func3()
    {
        cout << "Base::Func3()" << endl;
    }
    private:
    int _b = 1;
};
class Derive : public Base
{
    public:
    //虚函数func1的重写
    virtual void Func1()
    {
        cout << "Derive::Func1()" << endl;
    }
    private:
    int _d = 2;
};
int main()
{
    Base b;
    Derive d;
    return 0;
}

通过观察和调式:

  • 派生类对象d也存在虚表指针,d对象由两部分构成:基类继承下来的成员和虚表指针,另外一部分是自己的成员。
  • 基类b对象和派生类d对象虚表是不一样的,这里Func1完成了重写,所以d的虚表中存的是重写的Derive :: Func1
  • 所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。(重写是语法的叫法,覆盖是原理层的叫法)
  • 只有虚函数才会被放到虚表里面。Func2继承下来后是虚函数,而Func3也继承下来了,但是不是虚函数,所以不会放进虚表
  • 派生类没有自己的虚表指针,直接继承基类的虚表指针,如果无法继承,那么派生类自己建立虚表
  • 虚函数表本质是一个虚函数指针的指针数组,一般情况这个数组最后后面放一个nullptr

总结派生类的虚表生成:

  • 先将基类中的虚表内容拷贝一份到派生类虚表中
  • 如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖表中基类的虚函数
  • 派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后

7.3 多态的原理

class Person
{
    public:
    virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person
{
    public:
    virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func(Person& p)
{
    p.BuyTicket();
}
int main()
{
    Person Mike;
    Func(Mike);
    Student Johnson;
    Func(Johnson);
    return 0;
}

7.3.1 基类指针或引用进行调用虚函数理由

对于多态来说,Func函数传Person调用的Person::BuyTicket,传Student调用的是Student::BuyTicket。

具体说明:

  • 构成多态的条件其中一条:通过基类的指针或引用进行调用虚函数
  • 根据图中信息可得,基类对象被基类类型指针指向,通过基类中虚表指针找到虚表中的虚函数;派生类对象被基类类型指针指向,指向派生类中基类切片那部分。
  • 指向派生类基类切片那部分,导致编译器无法区分基类是指向基类本身,还是指向派生类中切片中包含基类部分。但是编译器不会主动去区分它所指向的是一个实际的基类对象还是派生类对象中的基类部分。
  • 编译器通过基类类型来限制访问的范围,而虚函数的动态绑定通过虚表指针和虚表来确保正确的函数调用。
  • 体现了切片的作用及其为什么需要通过基类的指针或者引用调用
  • 相同类型的类,共享同一块虚表。运行时去指向对象虚函数表中找BuyTicket的地址。

7.3.2 不满足多态情况

如果出现不满足多态的情况,编译链接根据调用对象类型,确定调用函数及其函数地址

小结:

  • 多态调用:运行时,到指向对象的虚表中找虚函数调用,做到指向父类调用父类的虚函数,指向子类调用子类的虚函数
  • 普通调用:编译时,调用对象是哪个类型,就调用他的函数
  • 虚表:虚函数表,存的虚函数,目标实现多态
  • 虚基表:存的当前位置记录虚基类部分的偏移量,解决菱形继承导致的数据冗余和二义性

7.3.3 反汇编中情况

void Func(Person* p)
{
    ...
        p->BuyTicket();
    // p中存的是mike对象的指针,将p移动到eax中
    001940DE mov eax,dword ptr [p]
        // [eax]就是取eax值指向的内容,这里相当于把mike对象头4个字节(虚表指针)移动到了edx
        001940E1 mov edx,dword ptr [eax]
        // [edx]就是取edx值指向的内容,这里相当于把虚表中的头4字节存的虚函数指针移动到了eax
        00B823EE mov eax,dword ptr [edx]
        // call eax中存虚函数的指针。这里可以看出满足多态的调用,不是在编译时确定的,是运行起来以后到对象的中取找的。
        001940EA call eax
        00头1940EC cmp esi,esp
}
int main()
{
    ...
        // 首先BuyTicket虽然是虚函数,但是mike是对象,不满足多态的条件,所以这里是普通函数的调用转换成地址时,是在编译时已经从符号表确认了函数的地址,直接call 地址
        mike.BuyTicket();
    00195182 lea ecx,[mike]
        00195185 call Person::BuyTicket (01914F6h)
        ...
}

八、虚函数与虚表存储内存区域

问题:

  • 虚函数存储在哪的?
  • 虚表存储在哪的?

错误答案:虚函数存在虚表,虚表存在对象中,这里答案是错误的。

接下来我们可以通过打印地址来观察,这样是一种小技巧

int main()
{
    int i = 0;
    static int j = 1;
    int* p1 = new int;
    const char* p2 = "xxxxxxxx";
    printf("栈:%p\n", &i);
    printf("静态区:%p\n", &j);
    printf("堆:%p\n", p1);
    printf("常量区:%p\n", p2);
    Person p;
    Student s;
    Person* p3 = &p;
    Student* p4 = &s;
    printf("Person虚表地址:%p\n", *(int*)p3);
    printf("Student虚表地址:%p\n", *(int*)p4);
    return 0;
}

从打印结构来看,关于上面两个问题,我们可以得到答案

答案:

  • 虚函数存储在代码段,同普通函数一样。
  • 虚表存储在常量区,虚表存储是虚函数指针,而不是虚函数
  • 虚表属于类,不归属函数局部中,因此不应该存储在栈上


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

相关文章
|
1月前
|
编译器 C# 开发者
C# 9.0 新特性解析
C# 9.0 是微软在2020年11月随.NET 5.0发布的重大更新,带来了一系列新特性和改进,如记录类型、初始化器增强、顶级语句、模式匹配增强、目标类型的新表达式、属性模式和空值处理操作符等,旨在提升开发效率和代码可读性。本文将详细介绍这些新特性,并提供代码示例和常见问题解答。
45 7
C# 9.0 新特性解析
|
1月前
|
自然语言处理 编译器 Linux
|
24天前
|
编译器 PHP 开发者
PHP 8新特性解析与实战应用####
随着PHP 8的发布,这一经典编程语言迎来了诸多令人瞩目的新特性和性能优化。本文将深入探讨PHP 8中的几个关键新功能,包括命名参数、JIT编译器、新的字符串处理函数以及错误处理改进等。通过实际代码示例,展示如何在现有项目中有效利用这些新特性来提升代码的可读性、维护性和执行效率。无论你是PHP新手还是经验丰富的开发者,本文都将为你提供实用的技术洞察和最佳实践指导。 ####
28 1
|
25天前
|
数据安全/隐私保护 iOS开发 开发者
iOS 14隐私保护新特性深度解析####
随着数字时代的到来,隐私保护已成为全球用户最为关注的问题之一。苹果在最新的iOS 14系统中引入了一系列创新功能,旨在增强用户的隐私和数据安全。本文将深入探讨iOS 14中的几大隐私保护新特性,包括App跟踪透明度、剪贴板访问通知和智能防追踪功能,分析这些功能如何提升用户隐私保护,并评估它们对开发者和用户体验的影响。 ####
|
28天前
|
设计模式 安全 数据库连接
【C++11】包装器:深入解析与实现技巧
本文深入探讨了C++中包装器的定义、实现方式及其应用。包装器通过封装底层细节,提供更简洁、易用的接口,常用于资源管理、接口封装和类型安全。文章详细介绍了使用RAII、智能指针、模板等技术实现包装器的方法,并通过多个案例分析展示了其在实际开发中的应用。最后,讨论了性能优化策略,帮助开发者编写高效、可靠的C++代码。
35 2
|
28天前
|
安全 编译器 C++
【C++11】新特性
`C++11`是2011年发布的`C++`重要版本,引入了约140个新特性和600个缺陷修复。其中,列表初始化(List Initialization)提供了一种更统一、更灵活和更安全的初始化方式,支持内置类型和满足特定条件的自定义类型。此外,`C++11`还引入了`auto`关键字用于自动类型推导,简化了复杂类型的声明,提高了代码的可读性和可维护性。`decltype`则用于根据表达式推导类型,增强了编译时类型检查的能力,特别适用于模板和泛型编程。
22 2
|
6天前
|
安全 编译器 C++
C++ `noexcept` 关键字的深入解析
`noexcept` 关键字在 C++ 中用于指示函数不会抛出异常,有助于编译器优化和提高程序的可靠性。它可以减少代码大小、提高执行效率,并增强程序的稳定性和可预测性。`noexcept` 还可以影响函数重载和模板特化的决策。使用时需谨慎,确保函数确实不会抛出异常,否则可能导致程序崩溃。通过合理使用 `noexcept`,开发者可以编写出更高效、更可靠的 C++ 代码。
13 0
|
6天前
|
存储 程序员 C++
深入解析C++中的函数指针与`typedef`的妙用
本文深入解析了C++中的函数指针及其与`typedef`的结合使用。通过图示和代码示例,详细介绍了函数指针的基本概念、声明和使用方法,并展示了如何利用`typedef`简化复杂的函数指针声明,提升代码的可读性和可维护性。
29 0
|
1月前
|
PHP 开发者
PHP 7新特性深度解析
【10月更文挑战第40天】随着PHP 7的发布,这个广泛使用的语言带来了许多令人兴奋的新特性和性能改进。本文将深入探讨PHP 7的主要变化,包括类型声明、错误处理机制、性能优化等方面,帮助开发者更好地理解和应用这些新特性。
34 4
|
1月前
|
C# 开发者
C# 10.0 新特性解析
C# 10.0 在性能、可读性和开发效率方面进行了多项增强。本文介绍了文件范围的命名空间、记录结构体、只读结构体、局部函数的递归优化、改进的模式匹配和 lambda 表达式等新特性,并通过代码示例帮助理解这些特性。
36 2

推荐镜像

更多