• 忘掉天地
  • 仿佛也想不起自己
bingliaolongBingliaolong  2024-06-21 23:59 Aet 隐藏边栏 |   抢沙发  8 
文章评分 4 次,平均分 5.0

多态

概述

  1. 多态性(Polymorphism)是面向对象编程的一个核心概念,它允许同一个接口调用在不同对象上执行不同的操作
  2. C++中,多态性主要通过继承和虚函数实现
  3. 多态性使代码更具灵活性和可扩展性
    1. 因为你可以编写更通用的代码,依赖于抽象基类而不是具体的派生类

多态的类型

  1. 编译时多态(静态多态)
    1. 通过函数重载和运算符重载实现
  2. 运行时多态(动态多态)
    1. 通过继承和虚函数实现

编译时多态

  1. 编译时多态性在编译阶段就已经确定
    1. 主要通过函数重载和运算符重载来实现
  2. 函数重载
    1. 同一作用域内可以有多个同名函数,只要它们的参数列表不同

  1. 运算符重载
    1. 可以为类自定义运算符行为

运行时多态

  1. 运行时多态性在程序运行时通过虚函数表(vtable)实现
    1. 基类定义虚函数,派生类可以重写这些虚函数
    2. 通过基类指针或引用调用虚函数时,根据实际对象类型调用相应的派生类实现
  2. 总结来说:
    1. 运行时多态是通过虚函数和动态绑定来实现的

虚函数

  1. 虚函数允许在基类中定义接口,由派生类提供具体实现

虚函数表

  1. 虚函数表(vtable)是编译器维护的一种数据结构,用于实现运行时多态
  2. 每个包含虚函数的类都有一个虚函数表,其中存储了虚函数的地址
    1. 对象的虚函数指针(vptr)指向这个表
    2. 通过基类指针调用虚函数时,程序会通过 vptr 查找实际函数地址并调用
  3. C++中,每个包含虚函数的类在实例化对象时,编译器会在对象的构造函数中插入代码来初始化虚函数指针(vptr
    1. 虚函数指针指向虚函数表(vtable),虚函数表中包含了该类的虚函数的地址
    2. 当通过基类指针或引用调用虚函数时,程序会通过vptr找到对应的vtable,并从中获取实际要调用的函数地址,从而实现动态多态
  4. 总结来说:
    1. 编译器会在编译期为每一个有虚函数的类确定一个虚函数表
    2. 类里面的虚函数的地址会按声明顺序存到这个虚函数表里
    3. 然后在运行时,在对象构造的时候,会动态得将vptr指向这个类对象动态类型对应的虚函数表的地址

虚函数与虚函数表示例

  1. 示例代码

  1. Base类的虚函数表:

  1. Derived类的虚函数表:

  1. 在构造函数中初始化vptr

  1. 虚函数调用:
    1. 通过对象 b 的vptr找到虚函数表(vtable
    2. 在虚函数表中查找 show 函数的地址
    3. 调用虚函数表中 show 函数的地址,实际调用 Derived::show()

虚函数表原理

  1. 内存布局和虚函数表(vtable
    1. 每个包含虚函数的类都有一个虚函数表(vtable),这个表中存储了该类的所有虚函数的地址
    2. 虚函数表是在编译期确定的
    3. 每个对象都有一个隐藏的指针,称为虚函数指针(vptr),指向这个虚函数表
    4. 这个指针vptr的初始化是在执行对象的构造函数时,编译器插入了代码进行初始化的,让它执行本类的虚函数表
    5. 而类的所有对象共用一张虚函数表
  2. 当你使用派生类对象初始化一个基类指针时,会发生隐式向上转型(implicit upcasting
    1. 这意味着派生类对象被视为基类对象,并且基类指针只能访问基类中定义的成员
  3. 对象内存布局:
    1. 对于一个包含虚函数的类,其对象内存布局中通常包含以下部分:
    2. 虚函数指针(vptr):指向该对象所属类的虚函数表(vtable
    3. 类的其他成员变量ij

  1. 生隐式向上转型
    1. 即使一个派生类对象被隐式向上转型为基类指针,指针本身仍然指向的是派生类对象
    2. 在派生类对象的内存布局中,虚函数指针(vptr)仍然指向派生类的虚函数表(vtable
    3. 因此,通过基类指针调用虚函数时,程序会使用对象的虚函数指针,找到派生类的虚函数表,从而调用派生类的虚函数实现

虚函数指针

  1. 虚函数指针(vptr)的位置:
    1. 虚函数指针(vptr)通常是对象内存布局中的第一个成员,指向该对象所属类的虚函数表(vtable
  2. 虚函数表(vtable)的位置:
    1. 虚函数表(vtable)是一个类级别的数据结构,不是对象的一部分
    2. 每个包含虚函数的类有一个虚函数表,存储在全局或静态存储区

type_info

  1. type_infoC++ 标准库中的一个类,用于在运行时描述类型信息
  2. 它是 RTTIRun-Time Type Information,运行时类型信息)的一部分,主要用于在运行时进行类型识别和检查
  3. type_info 对象通常与 typeid 运算符一起使用,typeid 运算符用于获取对象或类型的 type_info
  4. type_info 对象通常由编译器生成,并在程序的全局静态存储区中维护
    1. 每个类型对应一个唯一的 type_info 对象
    2. type_info 对象是与类关联的,而不是与类的实例关联的
      也就是说,每个类型的实例化对象的type_info数据是一样的
    3. 编译器确保每种类型在程序中只存在一个 type_info 对象
  5. 使用场景
    1. 动态类型识别:在多态场景中,通过基类指针或引用获取实际对象的类型
    2. 异常处理:在捕获异常时,通过 typeid 获取异常对象的类型信息,进行特定类型的异常处理
    3. 类型比较和排序:在泛型编程中,可以使用 type_info 进行类型比较和排序,确保模板参数类型符合预期
  6. type_info是否在虚函数的调用过程中起到作用?
    1. 在通过基类指针调用虚函数的过程中,type_info 本身并不起直接作用
    2. 虚函数调用依赖于虚函数表(vtable)和虚函数指针(vptr),而不是 type_info

纯虚函数和抽象类

  1. 抽象类是不能实例化的类,通常用于定义接口
  2. 包含一个或多个纯虚函数的类就是抽象类

动态绑定

  1. 总结就是:
    1. 通过基类的指针或引用去调用和派生类同名的函数时,根据基类指针或引用所指对象的动态类型来确定vptr指向哪个虚函数表,然后根据偏移量在表中拿到了目标函数地址

静态类型

  1. 变量在声明时的类型(编译时已知)
    1. 编译阶段
    2. Base* ptr = new Derived;Base*
    3. 静态绑定(如非虚函数调用、默认参数选择)
  2. 若基类和派生类的虚函数有不同默认参数,调用时由静态类型决定参数值。例如:
    1. 这就是所谓的静态类型决定默认参数:

动态类型

  1. 对象实际指向的类型(运行时确定)
    1. 运行阶段
    2. ptr 实际指向 Derived 对象
    3. 动态绑定(虚函数调用、dynamic_cast
  2. 虚函数调用根据动态类型选择派生类实现,例如:
    1. 这就是所谓的动态类型决定函数实现:

向上转型

  1. 向上转型(upcasting)是将派生类的指针或引用隐式地转换为基类的指针或引用的过程

    1. 这在C++中是安全的,因为派生类对象包含了基类的所有成员和行为
    2. 不会发送截断现象
    3. 因此,基类指针或引用可以访问基类的成员函数和数据成员,但不能访问派生类特有的成员
  2. 原理:

    1. 指针/引用仅“看待”对象为基类部分,但实际对象仍是完整的派生类对象
  3. 示例:

  1. 关于内存布局
    1. 即使发生向上转型,派生类对象的内存布局和虚函数表(vtable)保持不变
    2. 基类指针或引用指向的是派生类对象,因此虚函数指针(vptr)仍然指向派生类的虚函数表,从而实现虚函数的动态绑定
  2. 内存布局实例:

向下转型

  1. 将基类指针/引用转回派生类指针/引用

    1. 如果需要访问派生类特有的数据成员或成员函数,需要进行向下转型
  2. static_cast

    1. 必须确保基类指针/引用实际指向的是目标派生类对象
    2. 若对象类型不匹配,导致未定义行为

  1. dynamic_cast(推荐方式)
    1. 基类必须有虚函数(启用 RTTI
    2. 若转换失败,返回 nullptr(指针)或抛出异常(引用)

截断现象

  1. 仅发生在值拷贝时
    1. 当派生类对象被赋值给基类对象(而非指针/引用)时,派生类独有的部分会被丢弃

其他问题

  1. 编译时多态,本质是编译器在生成汇编指令时生成了多份吗

    1. 编译时多态(静态多态)在本质上是编译器在编译阶段根据函数重载或运算符重载生成了不同的函数或运算符的多个版本
    2. 在编译时,编译器能够确定调用的是哪个具体的函数或运算符版本
      因此在生成汇编指令时,会生成针对每个重载版本的不同代码
    3. 这意味着,函数的不同版本在编译期间被静态地解析,并且每个版本都有自己的汇编代码实现
  2. 在抽象类里面,可以加非虚函数实现吗

    1. 在抽象类中可以添加非虚函数的实现,目的是在保持抽象类接口规范的同时,提供可复用的基础功能
  3. 在抽象类里面,可以定义数据成员吗

    1. 抽象类通过数据成员为所有派生类提供统一的属性定义,避免重复代码

类型转换

dynamic_cast

  1. 用途:
    1. 用于多态类型的安全向下转型
  2. 特点:
    1. 在运行时进行类型检查,确保转换的安全性
    2. 如果转换失败,返回 nullptr(对于指针)或抛出 std::bad_cast 异常(对于引用)
  3. 所谓运行期进行类型检查:
    1. 编译器会在生成汇编代码时插入相应的类型检查相关的汇编指令,以保证在运行时对类型进行验证
    2. 这些验证通常通过虚表(vtable)和运行时类型信息(RTTI)来实现
  4. 使用条件:
    1. 要求基类有至少一个虚函数,以支持 RTTI(运行时类型信息)

static_cast

  1. 用途:
    1. 用于已知类型关系的转换,包括向上转型和向下转型
  2. 已知类型关系包括:
    1. 基类和派生类之间的转换:基类指针或引用转换为派生类指针或引用,反之亦然
    2. 基本类型之间的转换:如整数到浮点数的转换,反之亦然
    3. 指针类型之间的转换:如 void* 到具体类型指针的转换
  3. 特点:
    1. 在编译时进行类型检查(比如检查两个类是不是继承关系),但不进行运行时检查
    2. 如果类型不匹配,可能导致未定义行为
  4. 使用条件:
    1. 要求在编译时能够确定类型转换的合理性

reinterpret_cast

  1. 用途:
    1. 用于进行位级别的类型转换,通常在底层编程中使用
  2. 特点:
    1. 不进行任何类型检查,直接将对象的位模式重新解释为另一类型。如果用于不相关的类型,可能导致未定义行为
  3. 所谓位级别的类型转换:
    1. 指直接将一个对象的内存表示(即位模式)解释为另一种类型,而不对数据本身进行任何转换或解释
    2. 这种转换仅仅改变了编译器对该内存块的解释方式,而不改变内存中的实际位模式
    3. 具体解释在tip 6
  4. 使用条件:
    1. 通常用于非常低级别和特定场景下的转换,不推荐在常规代码中使用

  1. 应用场景
  2. 示例
    1. 会按照转换的目标类型去从地址去读取目标类型的相应字节数
    2. 并不会改变内存中原有的二进制的数据
    3. 但是会按照转换的目标类型去解释这些二进制位

const_cast

  1. 用途:
    1. 是一个特殊的类型转换运算符
    2. 用于移除或添加 constvolatile 限定符
    3. 它的主要作用是改变编译器对对象的类型视图,使得可以通过非 const 的方式访问对象
  2. 特点:
    1. 仅用于修改对象的常量性,不改变对象的类型
  3. 原理:
    1. const_cast 并不是通过修改常量的存放位置来实现其功能的
    2. 实际上,const_cast 只是改变了编译器对对象的类型视图,使得可以通过非 const 的方式访问对象
    3. 然而,使用 const_cast 可能是一件危险的事情,见tip 5
  4. 使用条件:
    1. 需要改变对象的常量性时使用

  1. 使用const_cast其实是一件危险的事情,如下例:
    1. 编译器可能会对常量进行优化,将其直接嵌入指令中
    2. 因为 .rodata 段通常是只读的,尝试修改该段中的数据可能会导致程序崩溃或引发其他严重错误

  1. 建议:
    1. 仅用于非常量对象:确保使用 const_cast 仅用于原本就不是常量的对象
    2. 避免修改全局或静态常量:全局或静态常量通常存储在只读数据段,修改它们是危险的
    3. 文档和注释:在代码中使用 const_cast 时,添加适当的注释,解释为何需要移除 const 限定符以及这样做的安全性

关于数据段和BSS段

概述

  1. 数据段(Data Segment):
    1. 存放已初始化的全局和静态变量,以及只读数据段中的常量
  2. BSS 段(Block Started by Symbol):
    1. 存放未初始化的全局和静态变量,这些变量在程序启动时被初始化为零

常量位置

  1. 全局常量:通常存放在只读数据段(.rodata
  2. 局部常量:存放在栈上
  3. 静态常量:通常存放在只读数据段(.rodata

示例

编译器对对象的类型视图

概述

  1. 编译器对对象的类型视图指的是编译器在编译阶段如何理解和处理变量的类型信息
  2. 类型视图包括变量的类型、限定符(如 constvolatile)、以及该类型的相关操作和限制
  3. 通过类型视图,编译器能够进行类型检查、类型转换和优化代码生成等工作

类型视图的基本概念

  1. 类型(Type):
    1. 变量的基本属性,包括内置类型(如 intfloat)和用户定义类型(如 structclass
  2. 限定符(Qualifiers):
    1. 修饰类型的属性,包括 const(常量性)和 volatile(易变性)
  3. 类型检查:
    1. 编译器在编译阶段检查变量和表达式的类型是否匹配
  4. 类型转换:
    1. 编译器在类型视图的基础上进行合法的类型转换,包括隐式转换和显式转换

本文为原创文章,版权归所有,欢迎分享本文,转载请保留出处!

bingliaolong
Bingliaolong 关注:0    粉丝:0 最后编辑于:2025-03-18
Everything will be better.

发表评论

表情 格式 链接 私密 签到
扫一扫二维码分享