第7章 站在对象模型的尖端3: RTTI

embedded/2025/3/17 7:24:22/

运行时类型识别(RTTI)允许程序员查询对象的实际类型,以及将基类的指针或引用转换为派生类的指针或引用。

#include <typeinfo>  // 用于 typeid 和 dynamic_cast
//基类
class type {
public:virtual ~type() {}  // 虚析构函数,确保派生类能够正确销毁virtual void call() = 0;  // 纯虚函数,使 type 成为抽象类
};
// 单一函数类型
class fct : public type {
public:void call() override {std::cout << "fct::call()" << std::endl;}
};
// 可以被重载的函数类型
class gen : public type {
public:void call() override {std::cout << "gen::call() - non-const" << std::endl;}void call() const override {std::cout << "gen::call() - const" << std::endl;}
};
int main() {// 创建 type* 指针,并让它指向 fct 对象type* typePtr1 = new fct();// 创建 type* 指针,并让它指向 gen 对象type* typePtr2 = new gen();// 尝试将 type* 转换为 fct*fct* fctPtr1 = dynamic_cast<fct*>(typePtr1);if (fctPtr1) {fctPtr1->call();  // 安全地调用 fct::call()} else {std::cout << "typePtr1 is not a fct" << std::endl;}// 尝试将 type* 转换为 fct*,但实际上是 genfct* fctPtr2 = dynamic_cast<fct*>(typePtr2);if (fctPtr2) {fctPtr2->call();  // 如果转换成功,调用 fct::call()} else {std::cout << "typePtr2 is not a fct, it's actually a gen" << std::endl;}// 清理资源delete typePtr1;delete typePtr2;return 0;
}

这里的type是所有类型的基类,fct代表单一函数类型,而gen代表可以被重载的函数类型。

当尝试将 type* 指针转换为 fct* 时,如果该指针实际上指向的是 gen 对象,那么使用 static_cast 将导致未定义行为,因为 gen 和 fct 的内存布局可能不同。使用 dynamic_cast 则会在运行时检查实际的对象类型,如果发现类型不匹配,它会返回 nullptr,从而避免了潜在的错误。

dynamic_cast 在运行时检查类型转换的有效性。它使用了 RTTI(运行时类型信息)来确定对象的真实类型。如果转换不安全,dynamic_cast 会返回 nullptr(对于指针)或抛出异常(对于引用),这使得程序可以在转换失败时采取适当的措施。dynamic_cast 只能用于含有至少一个虚函数的类之间(即多态类型)。这是因为 RTTI 只能应用于这些类。

为什么要将基类指针转换为派生类指针?

将基类指针转换为派生类指针通常是为了访问派生类特有的成员,这些成员在基类中是不可见或不存在的,基类指针只能访问基类中定义的公共或保护成员。这种转换通常发生在多态场景下,当有一个指向基类的指针,但实际上该指针指向的是派生类的对象。通过向下转型(downcast),可以利用派生类特有的功能。

在多态设计中,基类指针经常被用来指向派生类对象,以便于编写通用的代码。但在某些情况下,你可能需要知道具体的派生类类型,以便执行特定的操作。

有时候,直接使用派生类指针可以提高性能,尤其是在频繁调用派生类特有的方法时,直接访问可以避免虚函数调用带来的额外开销。

#include <iostream>
class Shape {
public:virtual ~Shape(){}virtual void draw() const = 0;  // 纯虚函数,使得 Shape 成为抽象类
};
class Circle : public Shape {
private:double radius;
public:Circle(double r) : radius(r) {}void setRadius(double r) {radius = r;}void draw() const override {std::cout << "Drawing a circle with radius: " << radius << std::endl;}
};
class Rectangle : public Shape {
private:double width, height;
public:Rectangle(double w, double h) : width(w), height(h) {}void draw() const override {std::cout<<"Drawing a rectangle with width: "<< width<<" and height: "<< height << std::endl;}
};
int main() {Shape* shape1 = new Circle(5.0);Shape* shape2 = new Rectangle(8.0, 9.0);// 通过基类指针调用 draw 方法shape1->draw();  // 输出圆的信息shape2->draw();  // 输出矩形的信息// 如果需要设置圆的半径,就需要向下转型Circle* circle = dynamic_cast<Circle*>(shape1);if (circle) {circle->setRadius(½);  // 设置新的半径circle->draw();  // 再次绘制,显示更新后的半径} else {std::cout << "shape1 is not a Circle" << std::endl;}// 清理资源delete shape1;delete shape2;return 0;
}

非法/合法的向下转换
 向下转换是合法的,当且仅当基类指针或引用实际上指向的是派生类对象。当基类指针或引用实际指向的是派生类对象时,向下转换是合法的。例如,如果有一个 Base* 指针,但它实际上指向的是 Derived 对象,那么将其转换为 Derived* 是合法的。

在C++中,当一个派生类对象被创建时,它首先包含了所有基类的数据成员。这意味着派生类对象的内存布局开始于基类部分,然后是派生类特有的部分。因此,一个指向基类部分的指针实际上也是指向整个派生类对象的起始位置。

当创建一个 Derived 对象时,内存布局如下:

+-----------------+

| Base::baseData | <- Base* 指向这里

+-----------------+

| Derived::data |

+-----------------+

  • 一个 Base* 指针实际上可以用来访问整个 Derived 对象,只是通过这个指针只能访问 Base 部分的成员。如果我们将 Base* 向下转换为 Derived*,就可以访问 Derived 特有的成员。

  • 如果基类指针或引用实际指向的是基类对象本身,而不是派生类对象,那么将其转换为派生类指针或引用是非法的。这会导致未定义行为。

  • 使用 static_cast 或 C 风格的强制类型转换:static_cast 和 C 风格的强制类型转换(如(Derived*)basePtr)不会进行运行时类型检查。如果转换不合法,它们不会提供任何保护,可能会导致未定义行为。

  • 使用 dynamic_cast:在不确定基类指针或引用实际指向哪种派生类对象时,使用 dynamic_cast 进行向下转换。它会在运行时进行类型检查,确保转换的合法性。

dynamic_cast 的工作原理                   

当使用 dynamic_cast 将基类指针或引用转换为派生类指针或引用时,编译器会在运行时检查实际的对象类型是否匹配。如果转换是不可能的(尝试将基类指针转换为一个无关的派生类),dynamic_cast 会返回 nullptr 对于指针,或者抛出 std::bad_cast 异常对于引用。

为了实现 dynamic_cast,C++ 使用了 RTTI 机制。RTTI 允许程序在运行时获取对象的类型信息。每个包含虚函数的类(即多态类)都有一个类型信息对象 (type_info),并且这个对象的指针通常被存储在虚函数表(vtable)的第一个槽位。

dynamic_cast 的成本

  1. 空间成本:每个具有虚函数的类实例都会有一个额外的指针(通常是 vptr),它指向虚函数表。虚函数表的第一个槽位存放的是指向 type_info 对象的指针。

  2. 时间成本:在执行 dynamic_cast 时,需要访问 vptr 来找到 type_info,然后与目标类型的 type_info 进行比较。这是一个运行时操作,比 static_cast 更加昂贵。

dynamic_cast 与引用

引用与指针的一个关键区别在于引用必须总是引用一个有效的对象,不能像指针那样可以为空(nullptr)。因此,当 dynamic_cast 应用于引用时,它不能返回 nullptr 来表示转换失败。相反,如果转换不安全,dynamic_cast 会抛出一个 std::bad_cast 异常。

Base& baseRef = getSomeBaseReference();  // 假设这个函数返回一个 Base& 引用
try {Derived& derivedRef = dynamic_cast<Derived&>(baseRef);// 转换成功,可以安全地使用 derivedRef
} catch (const std::bad_cast& e) {// 转换失败,baseRef 不是 Derived 对象的引用
}

可以重新实现 simplify_conv_op 函数,使其接受一个引用作为参数,并使用 dynamic_cast 来尝试向下转换。如果转换成功,函数将继续执行;如果转换失败,将捕获 std::bad_cast 异常并处理错误情况。

TypeID运算符

typeid 运算符用于获取表达式的类型信息,它可以用于在运行时检查变量或表达式的类型。

typeid 运算符返回一个 const std::type_info 类型的引用,该引用提供了有关类型的详细信息。

std::type_info 是 C++ 标准库中定义的一个类,它提供了关于类型的名称和其他信息。

typeid 运算符的基本用法

  • typeid(expression):返回表达式 expression 的类型信息。

  • typeid(type):返回类型 type 的类型信息。

std::type_info 类提供了以下成员函数:

virtual ~type_info();:析构函数。
bool operator==(const type_info& rhs) const;:判断两个 type_info 对象是否相等。
bool operator!=(const type_info& rhs) const;:判断两个 type_info 对象是否不相等。
bool before(const type_info& rhs) const;:比较两个 type_info 对象,用于排序。
const char* name() const;:返回类型的名称。

typeid 与多态类和非多态类

  • typeid 可以用于多态类(包含虚函数的类),在这种情况下,type_info 对象是在运行时确定的

  • typeid 也可以用于非多态类(不包含虚函数的类)和内置类型,这种情况下,type_info 对象是在编译时确定的

#include <iostream>
#include <typeinfo>
class Base {
public:virtual ~Base() {}
};
// 派生类
class Derived : public Base {
public:void doSomething() {std::cout << "Doing something in Derived" << std::endl;}
};
// 模板类
template <typename T>
class Generic {public:T value;Generic(T val) : value(val) {}
};
// 用于简化类型转换的函数
void simplify_conv_op(const Base& rt) {if (typeid(rt) == typeid(Derived)) {// 向下转换为 Derived 类型Derived& rf = static_cast<Derived&>(rt);rf.doSomething();} else if (typeid(rt) == typeid(Generic<int>)) {// 向下转换为 Generic<int> 类型Generic<int>& rg = static_cast<Generic<int>&>(rt);std::cout << "Generic<int> value: " << rg.value << std::endl;} else {// 其他类型的处理std::cout << "Unknown type" << std::endl;}
}

                     
原文链接:https://blog.csdn.net/m0_52043808/article/details/143589604


http://www.ppmy.cn/embedded/173282.html

相关文章

Haskell语言的二进制与编码

Haskell语言的二进制与编码 引言 Haskell是一种纯函数式编程语言&#xff0c;以其优雅的语法和强大的表达能力而闻名。在当今编程界&#xff0c;Haskell不仅是一种学术界的研究工具&#xff0c;也逐渐被应用于实际的工业项目中。二进制与编码是计算机科学中的重要概念&#x…

Wireshark:在 显示过滤器中“加入条件”过滤后,出现其他类型的数据包,为什么?

一、 在Wireshark中使用“tcp协议”过滤后&#xff0c;仍出现TLSv1.2协议的数据包&#xff0c;原因如下&#xff1a; 1. ‌协议层次关系‌ ‌TCP是传输层协议‌&#xff0c;而‌TLS属于应用层协议‌&#xff0c;后者直接运行于TCP之上‌28。因此&#xff0c;所有TLS流量&…

java八股文之消息中间件

<在Java中使用消息中间件时&#xff0c;通常会选择一些流行的开源解决方案&#xff0c;如Apache Kafka、RabbitMQ、ActiveMQ等。这些消息中间件提供了高效、可靠的消息传递机制&#xff0c;广泛应用于企业级应用中。下面我将介绍如何在Java中使用Apache Kafka进行消息传递的…

58.Harmonyos NEXT 图片预览组件架构设计与实现原理

温馨提示&#xff1a;本篇博客的详细代码已发布到 git : https://gitcode.com/nutpi/HarmonyosNext 可以下载运行哦&#xff01; Harmonyos NEXT 图片预览组件架构设计与实现原理 文章目录 Harmonyos NEXT 图片预览组件架构设计与实现原理效果预览一、组件架构概述1. 核心组件层…

2.12[A]distribute sys

在分布式训练中&#xff0c;特别是使用3D并行&#xff08;数据并行、流水线并行和模型并行&#xff09;时&#xff0c;不同阶段的GPU可能因为通信或数据依赖而出现空闲时间&#xff0c;这些空闲时间就是所谓的“气泡”。这些气泡会降低整体的训练效率&#xff0c;导致GPU资源的…

<rust><tauri><GUI>基于tauri和rust,编写一个二维码生成器

前言 本文是基于rust和tauri&#xff0c;由于tauri是前、后端结合的GUI框架&#xff0c;既可以直接生成包含前端代码的文件&#xff0c;也可以在已有的前端项目上集成tauri框架&#xff0c;将前端页面化为桌面GUI。 环境配置 系统&#xff1a;windows 10平台&#xff1a;vis…

【机器学习-基础知识】统计和贝叶斯推断

1. 概率论基本概念回顾 1. 概率分布 定义: 概率分布(Probability Distribution)指的是随机变量所有可能取值及其对应概率的集合。它描述了一个随机变量可能取的所有值以及每个值被取到的概率。 对于离散型随机变量,使用概率质量函数来描述。对于连续型随机变量,使用概率…

HOT100系列——(普通数组+矩阵)

普通数组 1.合并区间 56. 合并区间https://leetcode.cn/problems/merge-intervals/ 先针对左区间进行排序&#xff0c;这样可以对右边进行考虑&#xff0c;如果intervals 中一个新的区间的左端点小于原ans的右端点&#xff0c;那么就能合并&#xff0c;右端点合并成原p【1】和…