# RTTI RTTI是运行阶段类型识别(Running Type Identification)的简称。这是新添加的特性之一。RTTI旨在程序在运行阶段确定对象的类型提供一个标准方式。很多类库已经为其类对象提供了实现这种功能的方式,但是由于C++内部并不支持,因此有多种不兼容的机制。 ## RTTI的用途 跟踪对象的类型,得到对象的类型。 ## RTTI工作原理 C++有3个支持RTTI的元素。 - 如果可能的话,dynamic_cast运算符将使用一个指向基类的指针来生成一个指向派生类的指针,否则该运算符返回0(空指针)。 - `typeid` 运算符返回一个指出对象的类型的值。 - `type_info` 结构存储了有关特定类型的信息。 只能将RTTI用于包含虚函数的类层次结构,原因在于只有对于这种层次结构,才应该将派生对象的地址赋给基类指针。 ==注意:RTTI只适用于包含虚函数的类。== ### 1. dynamic_cast运算符 dynamic_cast运算符是最常见的RTTI组件,它不能回答指针指向的是哪类对象,但能够回答是否可以安全地将对象的地址赋给特定类型的指针。 ```C++ class Grand { // has virtual methods }; class Superb: public Grand {...}; class Magnificent: public Super {...}; //假设有这样的指针 Grand * pg = new Grand; Grand * ps = new Superb; Grand * pm = new Magnificent; // Superb * pm = dynamic_cast(pg); //指针pg的类型是否可以被安全地转换为Superb *, 如果可以,运算符将返回对象的地址,否则返回一个空指针。 //通常,如果指向的对象(*pt)的类型为Type或者是从Type直接或者间接派生而来的类型,则下面的表达式将指针pt转换为Type类型的指针: dynamic_cast(pt); //否则,结果为0,即空指针。 ``` # 强制类型转换 ## 旧式强制类型转换 早期显式进行强制转换包含两种形式: ```C++ type(expr); //函数形式的强制类型转换 (type) expr; //C语言风格的强制类型转换 ``` ## dynamic_cast dynamic_cast支持运行时的类型识别,结果是一个左值。 ## const_cast 函数描述 ```C++ const_cast (expression) ``` 主要是用来去掉const属性,当然也可以加上const属性,去掉用的多,加上用的少,一般的类型必须要与(expression)的类型一致才语法正确。从底层看是:去掉const属性,是将变量从const区存储移动到stack区存储;加上const属性,是将变量从stack区或者heap区存储移动到const区存储。 去掉const属性:`const_cast (&num)` ,常用,因为不能将一个const变量直接赋给非const变量,所以必须要有转换这一步。 加上const属性:`const int * k = const_cast(j)` ,一般很少用,因为可以将一个非const变量直接赋给一个const变量。 使用场景 ```C++ //1.常量指针被转化成非常量指针,转换后指针指向原来的变量(即转换后的指针地址不变)。 class A{ public: A(){ m_iNum = 0; } int m_iNum; }; void foo(){ //指针指向类 const A *pca1 = new A; A *pa2 = const_cast(pca1); //常量对象转换为非常量对象 pa2->m_iNum = 200; //fine //转换后指针指向原来的对象 cout<< pca1->m_iNum <m_iNum<(&ica); *ia = 200; cout<< *ia <(a1); //常量引用转为非常量引用   a2.m_iNum = 200; //fine   cout<< a0.m_iNum << a1.m_iNum << a2.m_iNum << endl; //1 1 200 } ``` ## static_cast 任何明确定义的类型转换,只要不包含底层cont,都可以使用static_cast。 ```C++ int j; double slope = static_cast(j)/i; ``` 当需要把一个较大的数据类型赋值给较小的数据类型时,`static_cast` 非常有用。这里的强制类型转换告诉程序的读者和编译器:我们知道且不在乎潜在的精度损失。一般来说,如果编译器发现一个较大的数据类型试图赋值给较小的类型,就会给出警告信息。但是当我们执行了显式的类型转换后,警告信息就会被关闭。 有时`static_cast` 对于编译器无法自动执行的类型转换也非常有用。 ## reinterpret_cast `reinterpret_cast` 通常为运算对象的位模式提供较低层次上的重新解释。 ```C++ //例子 int *ip; char *pc = reinterpret_cast(ip); //pc所指的真实对象是一个int,而非字符,如果把pc当成字符指针去使用就可能在运行时发生错误。 string str(pc); //编译可能通过,但是运行发生错误 ``` 使用`reinterpret_cast` 是非常危险的,用pc初始化str的例子证明这个可能带来难以理解的错误,并且编译器也没有发出警告或者错误提示。`reinterpret_cast` 本质上依赖于机器,要想要安全的使用`reinterpret_cast` ,必须对涉及的类型和编译器实现转换的过程非常了解。 --- # 数据类型转换与四种常见类型转换运算符 C++ 主要提供了两种类型转换机制,分别是**隐式类型转换‌**和‌**显式类型转换‌**。 ## 隐式类型转换‌ 隐式转换由编译器自动完成,无需程序员手动干预。常见于算术运算、赋值操作、函数参数传递等场景。 1. 优点 - ‌代码简洁高效‌:无需编写额外的转换代码,使表达式更紧凑,提高了开发效率。 - ‌类型安全(相对)‌:编译器遵循严格的提升规则(如整型提升、浮点提升),通常能保证数据在兼容类型间的安全流动。 - ‌无运行时开销‌:所有检查均在编译期完成不引入任何性能损耗。 - ‌支持多态基础‌:派生类指针/引用到基类指针/引用的向上转型是隐式的,这是实现多态的前提。 1. 缺点 - ‌精度丢失风险‌:当从高精度类型转为低精度类型时(如 `double` 转 `int`,或 `long long` 转 `int`),数据会被截断或舍入,且编译器可能仅发出警告而非错误,导致难以察觉的 Bug。 - ‌隐蔽性强,难以调试‌:由于转换是自动发生的,程序员可能未意识到类型发生了变化。例如,整数除法 `int a = 5 / 2;` 结果为 2 而非 2.5,若后续隐式转为 `double`,错误已被固化。 - ‌语义歧义‌:在涉及用户自定义类型时,单参数构造函数或类型转换运算符可能导致意外的隐式构造或转换,破坏代码逻辑(如 `MyClass obj = 10;` 可能意外调用构造函数)。 ## 显式类型转换 显式转换由人工主动发起,明确指示编译器进行类型转换。主要包括 ‌**C 风格转换‌** 和 ‌**C++ 命名转换运算符‌** (`static_cast`, `dynamic_cast`, `const_cast`, `reinterpret_cast`)。 1. 优点 - ‌意图明确,可读性高‌:特别是 C++ 的四种命名转换,清晰地表达了转换的目的(是数值转换、多态向下转型、去常量还是底层重解释)。 - ‌易于检索和维护‌:在大型项目中,可以通过搜索 `static_cast` 等关键字快速定位所有强制转换点,便于代码审查和重构。 - ‌更强的类型安全检查‌: ① `static_cast` 在编译期检查类型兼容性,阻止不相关的指针转换。 ② `dynamic_cast` 在运行期检查多态类型的合法性,失败返回 `nullptr` 或抛出异常,避免非法内存访问。 - ‌控制精度高‌:程序员可以精确控制何时发生转换,避免隐式转换带来的意外副作用。配合 `explicit` 关键字,可以禁止不必要的隐式构造。 1. 缺点 - ‌代码冗长‌:相比隐式转换,显式转换语法较长,可能使代码显得繁琐。 - ‌潜在的性能开销‌: ① `dynamic_cast` 依赖 RTTI(运行时类型信息),涉及运行时查询,比静态转换慢。 ② 某些转换(如浮点转整型)本身涉及 CPU 指令开销。 - ‌滥用风险(尤其是 C 风格和 `reinterpret_cast`)‌: ① C 风格转换 `(type)value` 混合了所有转换语义,编译器检查宽松,极易掩盖错误。 ② `reinterpret_cast` 直接操作二进制位,若使用不当(如解引用错误对齐的指针),会导致未定义行为(Undefined Behavior)。 - ‌仍需程序员负责正确性‌:除了 `dynamic_cast` 外,大多数显式转换(如 `static_cast` 向下转型、`const_cast` 修改常量)如果逻辑错误,编译器不会报错,运行时仍可能崩溃或产生错误结果。 ### C 风格转换 语法:`(type)expression` 或 `type(expression)` ```cpp double d = 3.14; int i = (int)d; // C 风格 int j = int(d); // 函数风格 ``` 在以前的 C 语言代码中经常会见到此风格类型的转换代码,编写不复杂,但是难以在代码中搜索定位,而且编译器检查宽松,可能执行危险的转换(如去除 `const` 或 `reinterpret`)而不发出警告;无法明确表达转换意图。 ### C++ 命名转换运算符 为了解决以前老式风格带来的不便利和不安全,C++ 提供了四种更精细、更安全的转换运算符(即上文中提到的 `static_cast`, `dynamic_cast`, `const_cast`, `reinterpret_cast`)。 语法均为 `xxx_cast(expression)``` `new_type` 是要转换成的新类型,`expression` 是被转换的数据或表达式。 例如,老式的 C 风格的 `double` 转 `int` 的写法为: ```cpp double d = 3.14; int n = (int)d; ``` C++ 新风格的写法为: ```cpp double d = 3.14; int n = static_cast(d); ``` #### static_cast(静态转换) 用于编译时已知的、良性的类型转换。它是 C 风格转换中最常用部分的替代品。主要适用于以下场景: 1. 基本数据类型之间的转换(如 `double` 转 `int`,`int` 转 `double`)。 2. 非多态类的继承层级中的指针/引用转换(向上转型安全,向下转型不安全但允许,无运行时检查,需人工保障)。 3. `void*与其他具体类型指针之间的互转。` 4. 调用显式构造函数或类型转换函数。 ```cpp class Complex { public: Complex(double real = 0.0, double imag = 0.0) : m_real(real), m_imag(imag) { } public: // 类型转换函数 operator double() const { return m_real; } private: double m_real; double m_imag; }; int main() { int m = 100; long n = static_cast(m); // 宽转换,没有信息丢失 char ch = static_cast(m); //窄转换,可能会丢失信息 int *p1 = static_cast( malloc(10 * sizeof(int)) ); //将void指针转换为具体类型指针 void *p2 = static_cast(p1); //将具体类型指针,转换为void指针 Complex c(12.5, 23.8); double real= static_cast(c); //调用类型转换函数 // 下面的用法是错误的 float *p3 = static_cast(p1); //不能在两个具体类型的指针之间进行转换 p3 = static_cast(0X2DF9); //不能将整数转换为指针类型 return 0; } ``` **总结**: - `static_cast` 是“静态转换”的意思,也就是在编译期间转换,转换失败的话会抛出一个编译错误,其要求转换的两个类型之间必须存在‌**继承关系‌**、‌**隐式转换关系‌(如基本数据类型间的提升/截断)\**或‌\**用户定义的转换函数‌**,不能用于无关类型之间的转换,因为这类转换缺乏编译期的类型关联验证,极易导致未定义行为、数据损坏或内存访问错误。 - 两个具体类型指针之间的转换,例如 `int*` 转 `double*`、`Student*` 转 `int*` 等。不同类型的数据存储格式不一样,长度也不一样,用 `A` 类型的指针指向 `B` 类型的数据后,会按照 `A` 类型的方式来处理数据,引发未定义行为甚至导致崩溃。 - `int` 和指针之间的转换,将一个具体的地址赋值给指针变量是非常危险的,因为该地址上的内存可能没有分配,也可能没有读写权限,恰好是可用内存反而是小概率事件。 - 非多态类的继承层级中的指针/引用转换,向上转型安全,因为子类 `Is-A` 父类,这是继承决定了的;而向下转型时,父类对象并不一定包含子类的特有成员。如果父类指针实际指向的是一个纯父类对象(或其他兄弟子类对象),强制转换为子类指针会导致‌未定义行为‌(如内存访问错误、数据损坏); - `void*` 与其他指针类型之间的互转,也要确保在互转的过程中,目标类型与原始数据类型完全一致,否则使用时也会引发未定义行为(道理同第一点一样)。单纯的 `void` 是不完整类型,编译器不允许使用 `void a = static_cast(m);` 这种转换,但是 `static_cast(m);` 是在函数中或者函数返回的场景下显式抑制“未使用变量”警告‌的标准写法,将变量 `m` 转换为 `void` 类型,从而告诉编译器“我有意不使用这个变量”,以消除编译警告。 ```cpp void test() { int a; // 编译时会发生警告:“a”: 未引用的局部变量 static_cast(a); // 显式抑制“未使用变量”警告 } ``` - `static_cast` 不能用于修改(即:去除或添加)表达式的 `const` 修饰和 `volatile` 修饰属性,但这句话只针对指针和引用类型(下一节 `const_cast` 会讲到)属于**修改对象属性**的范畴。但对于值类型来说,至少编译器是允许这样操作的,不外乎就是把变量的值复制了一份给新的变量(相当于用其值初始化了一个新变量),两个变量互不影响,这属于**创建新对象**的范畴。 ```cpp const int n = 100; int p = static_cast(n); const int m = static_cast(p); ``` #### const_cast(常量转换) `const_cast` 专门用于修改变量的 `const` 或 `volatile` 限定符。适用于将 `const/volatile` 指针/引用转换为非 `const/volatile` 指针/引用,以便修改原本被声明为 `const/volatile` 的对象(‌注意‌:如果原始对象确实是 const 定义的,修改它会导致未定义行为)。其只能‌改变 cv-限定符(`const/volatile`),不能改变数据类型。 ```cpp const int a = 10; // int* p = &a; // 错误:不能将 const int* 赋值给 int* int* p = const_cast(&a); // *p = 20; // 危险!如果 a 是真正 const 的,这是未定义行为 ``` 一起来看个意思的事情,我们将以上代码最后一段(即 `*p = 20;`)注释打开并添加输出值的语句 `std::cout << a << *p << a << std::endl`,运行后我们可以看到输出为 `10 20 10`。看到这个结果我们就会产生疑问了:指针 `p` 经过 `const_cast` 转换后指向 `a`,然后赋值了新值 `20`,为什么最后打印出不是 `10 20 20` 呢? 这就是‌未定义行为(Undefined Behavior, UB)的表现。代码 `*p = 20;` 试图修改一个原本定义为 `const` 的对象 `a`,这在 C++ 标准中属于‌未定义行为‌。因此,程序的任何输出结果(包括 `10 20 10`、`20 20 20` 或程序崩溃)都是“合法”的,因为标准不保证任何特定行为。虽然结果是未定义的,但在大多数常见编译器(如 GCC、Clang、MSVC)的默认优化级别下,出现 `10 20 10` 的原因如下: 1. 编译器优化:常量折叠(Constant Folding): 由于 `a` 被声明为 `const int a = 10;`,编译器在编译阶段就知道 `a` 的值永远是 `10`。当编译器遇到 `std::cout << a` 时,它‌不会去内存中读取 `a` 的值‌,而是直接将指令替换为立即数 ‌`10‌`。这就是为什么第一个和第三个 `a` 都输出 `‌10‌`,即使内存中的值可能已经被非法修改了。 2. 指针解引用:实际内存访问: `p`是一个指向`a`所在内存地址的普通`int*`指针。当执行`*p = 20;`时,CPU 会直接往`a`的内存地址写入`‌20`‌。当执行`std::cout << *p`时,程序会通过指针`p`‌去内存中读取‌该地址的值。此时内存中确实存着`‌20‌`,所以输出 ‌`20‌`。 **总结**: `const_cast`的修改场景,一般都用于去除cv-限定符,因为去除属于权限扩张。而添加cv-限定符属于权限收缩,C++ 允许非 `const/volatile` 指针/引用隐式转换为 `const/volatile` 指针/引用(如 `int* → const int*`)。这是语言内置的安全特性,无需任何强制转换,使用 `const_cast`,不仅代码冗余,还会向阅读者传递错误信号(让人误以为这里存在特殊的类型风险或需要去除常量性,不建议但能用)。每次使用 `const_cast` 时,务必确认“原始对象是否可变”,否则宁可重构接口也不应强行转换。 #### dynamic_cast(动态转换) `dynamic_cast` 用于多态类型(含有虚函数的类)之间的安全转换,主要在运行时进行类型检查(RTTI),它既允许向上转换(Upcasting),也允许向下转换(Downcasting)和 交叉转换(Cross-casting)。向上转型是无条件的,不会进行任何检测,所以都能成功;向下转换和交叉转换的前提必须是确保安全的,要借助 RTTI 进行检测。 `new_type` 和 `expression` 必须同时是指针类型或者引用类型。换句话说,`dynamic_cast` 只能转换指针类型和引用类型,其它类型(`int`、`double`、数组、类、结构体等)都不行。对于指针,如果转换失败将返回 `nullptr`;对于引用,如果转换失败将抛出 `std::bad_cast` 异常。 **向下转换示例**: ```cpp class Base { public: virtual ~Base() {} }; class Derived : public Base { }; int main() { Base* b = new Derived(); Derived* d = dynamic_cast(b); if (d) { std::cout << "成功" << std::endl; } else { std::cout << "失败" << std::endl; } return 0; } ``` **交叉转换示例**: ```cpp class BaseA { public: virtual ~BaseA() {} // 必须有多态性(虚函数) void funcA() { std::cout << "Func A" << std::endl; } }; class BaseB { public: virtual ~BaseB() {} // 必须有多态性(虚函数) void funcB() { std::cout << "Func B" << std::endl; } }; class Derived : public BaseA, public BaseB { public: void funcD() { std::cout << "Func D" << std::endl; } }; int main() { // 向上转型:Derived* -> BaseA* (安全,隐式或 static_cast) BaseA* a_ptr = new Derived(); // 3. 交叉转换:BaseA* -> BaseB* // 编译器无法在编译期确定 a_ptr 是否指向包含 BaseB 的对象 // 必须使用 dynamic_cast 在运行时通过 RTTI 查找路径 BaseB* b_ptr = dynamic_cast(a_ptr); if (b_ptr) { std::cout << "交叉转换成功" << std::endl; b_ptr->funcB(); } else { std::cout << "交叉转换失败" << std::endl; } return 0; } ``` **总结**: - `dynamic_cast` 与 `static_cast` 是相对的,`dynamic_cast` 是“动态转换”的意思。`dynamic_cast` 向下转换和交叉转换会在程序运行期间借助 RTTI 遍历‌继承链,检查基类指针实际指向的对象是否为目标派生类类型,这就要求基类必须包含虚函数;`static_cast` 在编译期间完成类型转换,编译器在编译阶段已知晓继承关系,直接计算内存偏移量(或地址不变),能够更加及时地发现错误。 - `dynamic_cast` 向上转换‌不依赖 RTTI(运行时类型信息)‌来进行安全性检查,因为向上转换在 C++ 中是‌静态安全‌的。虽然语法上允许,但是向上转换应优先使用隐式转换或 `static_cast`更高效、更清晰的做法。使用 `dynamic_cast` 进行向上转换会误导读者以为需要进行运行时检查,且可能产生不必要的微小性能开销(取决于编译器实现)。 - 不管是向上转换还是向下转换还是交叉转换,都需要确保是在类继承机制之间进行转换,而且指针/引用对象是正确的。基于多态的表现形式,一定是父类指针指向子类对象(如代码示例),进行向下转换,转换的两端的实际内容都是子类,才能成功。如果是一个纯粹的父类指针要进行向下转换,必定会失败返回 `nullptr`。向上转换虽然是静态安全的,但你不能强制使用 `reinterpret_cast`将一个毫不相关的类型(如:int)重新解释为子类,要求它向上转换,这样会产生未定义行为。 #### reinterpret_cast(重新解释转换) `reinterpret` 是“重新解释”的意思,顾名思义,`reinterpret_cast` 这种转换仅仅是对二进制位的重新解释,不会借助已有的转换规则对数据进行调整,非常简单粗暴,所以风险很高。`reinterpret_cast` 可以认为是 `static_cast` 的一种补充,一些 `static_cast` 不能完成的转换,就可以用 `reinterpret_cast` 来完成,例如两个具体类型指针之间的转换、`int` 和指针之间的转换。 ```cpp class A { public: A(int a = 0, int b = 0) : m_a(a), m_b(b) { } private: int m_a; int m_b; }; int main() { //将 char* 转换为 float* char str[] = "hello world"; float* p1 = reinterpret_cast(str); std::cout << *p1 << std::endl; //将 int 转换为 int* int* p = reinterpret_cast(100); std::cout << *p1 << std::endl; //将 A* 转换为 int* p = reinterpret_cast(new A(25, 96)); std::cout << *p << std::endl; return 0; } ``` **总结**: - 可以想象,用一个 `float` 指针来操作一个 `char` 数组是一件多么荒诞和危险的事情,这样的转换方式不到万不得已的时候不要使用。 - 将 `A*` 转换为 `int*`,使用指针直接访问 `private` 成员刺穿了一个类的封装性,更好的办法是让类提供 `get/set` 函数,间接地访问成员变量。 ### 使用建议 1. 能够不要使用类型转换尽量不要用类型转换,尽可能的只用原本的子类类型。 2. 需要使用类型转换的时候,可以尽量避免使用 `reinterpret_cast` 因为他过于自由,安全性太差。 3. 能够不去强行把 `const_cast` 转换的情况就尽量不要去破坏常量的属性。 4. 能用 `static_cast` 的地方,不要去使用 `dynamic_cast`。 ### 速查表 ![图片](https://mmbiz.qpic.cn/mmbiz_png/QjJs3FFciaXDlFGcdSJmAWIb2gNmCpk8tPaUBxwLibc7fG7eV29RFXtDEWhvlTp688ib5XFjexuNyOB7VeQd7z8HRk7AhM7HseRX4ibCNvnbQ7Y/640?wx_fmt=png&from=appmsg&tp=wxpic&wxfrom=5&wx_lazy=1#imgIndex=0)