在C语言,我们一般是使用 (type_name) expression 这种方式来做强转,当然这在C++中,我们也能这样做,但是不那么好,我们更好使用四个转换操作符来实现显示类型转换
- static_cast
- dynamic_cast
- const_cast
- reinterpret_cast
1. static_cast
用法: static_cast <new_type> (expression)
这与C语言做强转基本对等
主要用于以下场景
1.1基本类型之间的转换
将一个基本类型转换为另一个基本类型,例如将整数转换为浮点数或将字符转换为整数。
cpp
int a = 42;
double b = static_cast<double>(a); // 将整数a转换为双精度浮点数b
1.2指针类型之间的转换
将一个指针类型转换成另一个指针类型,尤其是在类层次结构中从基类指针转换为派生类指针。这种转换不在运行时执行类型检查,可能不安全,我们要自己保证指针确实可以互相转换。
cpp
class Base{};
class Derived:public Base{};
Base* base_ptr = new Derived();
Derived* dervied_ptr = static_cast<Derived*>(base_ptr);
1.3引用类型之间的转换
引用类型类似于指针类型之间的转换,我们也应注意安全性
将一个引用类型转换为另一个引用类型
cpp
class Base {};
class Derived :public Base {};
Derived derived_obj;
Base& base_ref = derived_obj;
Derived& derived_ref = static_cast<Derived&>(base_ref);
static_cast 在编译时执行类型转换,在进行指针或者引用类型转换时,需要自己检查合法性。
如果想要运行时执行类型检查,我们就可以使用下面的 dynamic_cast 进行安全的向下类型转换。
2.dynamic_cast
用法:dynamic_cast <new_type>(expression)
dynamic_cast 在C++中应用于父子类结构中的安全类型转换。
它在运行时执行类型检查,相比static_cast更加安全
2.1向下类型转换
当需要将 基类指针或者引用 转换为 派生类指针或引用时,dynamic_cast可以确保类型兼容性。如果转换失败,将 return NULL(对于指针类型)或抛出异常(对于引用类型)。
cpp
class Base { virtual void demo() {} };
class Derived :public Base { int a; };
Base* base_ptr = new Derived();
Derived* derived_ptr = dynamic_cast<Derived*>(base_ptr);
// 将基类指针base_ptr转换为派生类指针derived_ptr,如果类型兼容,则成功
2.2用于多态类型检查
处理多态对象时,dynamic_cast可以用来确定对象的实际类型
cpp
class Animal{ public: virtual ~Animal(){} };
class Dog : public Animal{ public: void bark(){/*....*/ } };
class Cat : public Animal{ public: void meow(){/*....*/ } };
Animal* animal_ptr = /*....*/;
// 将Animal指针转换为Dog指针
Dog* dog_ptr = dynamic_cast<Dog*>(animal_ptr);
if (dog_ptr) {
dog_ptr->bark();
}
注意,要使用 dynamic_cast 有效,基类至少需要一个虚拟函数,只有在基类存在虚函数的情况下才能将基类转化为子类。
2.3 dynamic_cast 底层原理
1. RTTI (运行时类型信息)
dynamic_cast 依赖于 C++ 的运行时类型信息(RTTI),这是编译器在程序运行时提供的一种机制,用于识别对象的实际类型。RTTI 是通过在每个多态类型的类中生成一个指向类型信息的指针来实现的,这个信息通常被称为"虚表"。
2. 虚函数表(vtable )和虚指针(vptr )
每个包含虚函数的类会有一个虚函数表(vtable),该表包含指向类中虚函数的指针。在对象实例中,编译器会为对象添加一个虚指针(vptr),指向该对象的类的虚表。这样,通过虚指针,程序可以在运行时动态地确定调用哪个函数。
3. 类型识别
当使用 dynamic_cast 进行类型转换时,编译器首先检查对象的虚指针以获取其类型信息。通过 RTTI,dynamic_cast 可以确认目标类型与源类型之间的继承关系。
4. 转换过程
- 向上转换 (如基类指针转换为派生类指针):这通常是安全的,dynamic_cast 将直接返回基类指针。
- 向下转换 (如基类指针转换为派生类指针):dynamic_cast 会检查对象的实际类型是否匹配目标类型。如果匹配,则返回指向派生类的指针;如果不匹配,则返回 nullptr。
5. 效率
dynamic_cast 相比静态转换(如 static_cast)性能较低,因为它需要在运行时进行类型检查和识别。这种动态检查增加了运行时开销,因此建议在必要时使用。
《Effective C++》中,有一个例子
cpp
class Point
{
public:
Point(float xval);
virtual ~Point();
float x() const;
static int PointCount();
protected:
virtual ostream& print(ostream& os) const;
float _x;
static int _point_count;
};
每一个多态对象都有一个指向其vtable的指针,被称为vptr。
RTTI(就是上面图中的 type_info 结构)通常与vtable关联。
dynamic_cast就是利用RTTI来执行运行时类型检查和安全类型转换。
以下是dynamic_cast的工作原理的简化描述:
首先,dynamic_cast通过查询对象的 vptr 来获取其RTTI(这也是为什么 dynamic_cast 要求对象有虚函数)
然后,dynamic_cast比较请求的目标类型与从RTTI获得的实际类型。如果目标类型是实际类型或其基类,则转换成功。
如果目标类型是派生类,dynamic_cast会检查类层次结构,以确定转换是否合法。如果在类层次结构中找到了目标类型,则转换成功;否则,转换失败。
当转换成功时,dynamic_cast返回转换后的指针或引用。
如果转换失败,对于指针类型,dynamic_cast返回空指针;对于引用类型,它会抛出一个std::bad_cast异常。
因为dynamic_cast依赖于运行时类型信息,它的性能可能低于其他类型转换操作(如static_cast),static 是编译器静态转换,编译时期就完成了。
3. const_cast
用法: const_cast<new_type>(expression) new_type必须是一个指针、引用,或者指向对象类成员的指针
3.1 修改const对象
当需要修改const对象的时候,可以使用 const_cast 来删除const属性。
cpp
const int a = 42; // 定义一个常量
int* mutable_ptr = const_cast<int*>(&a); // 去掉 const 属性
*mutable_ptr = 43; // 尝试修改 a 的值
这里我们只是将const属性去除,实际上,修改const变量是错误的
因为 a 是一个常量,试图修改它会导致未定义行为。
- 未定义行为:修改一个 const 对象的值会导致未定义行为,程序可能崩溃、输出错误值,或者看似正常工作。
- 避免风险:如果你需要修改某个值,请确保该值本身不是 const。
如果确实需要在某个上下文中使用 const_cast,确保它是安全的。例如,如果有一个原本是 const 的指针,但确实需要在特定情况下修改数据,我们可以考虑使用非常量的数据结构,或者重构代码以避免这种需要。
3.2 const对象调用非const成员函数
当需要使用const对象调用非const成员函数时,可以使用const_cast删除对象的const属性
cpp
class MyClass {
public:
void non_const_function() {
// 函数实现
}
};
const MyClass my_const_obj; // 定义一个常量对象
MyClass* mutable_obj_ptr = const_cast<MyClass*>(&my_const_obj); // 去掉 const 属性
mutable_obj_ptr->non_const_function(); // 调用非 const 成员函数
调用 non_const_function 会导致未定义行为,因为 my_const_obj 是 const 的,修改其状态是不安全的。建议避免这种做法,确保对象在定义时的常量属性不被破坏,以保持程序的稳定性和可靠性。
4.reinterpret_cast
用法: reinterpret_cast <new_type>(expression)
reinterpret_cast <new_type>(expression) 用于在不同类型之间进行低级别的转换。
首先,我们从字面意思来理解,interpret是"解释,诠释",re就代表重新诠释。
cast我认为可以是"转型"之意(当然,《Effective C++》中也是这样翻译的)
它仅仅是重新解释底层(也就是对指针所指的那片比特位换个类型做解释),不进行任何类型的检查。
因此,reinterpret_cast可能导致未定义的行为,应谨慎使用。
4.1 指针类型之间的转换
某些情况下,我们需要在不同指针类型之间进行转换
C++是兼容C的,因此C语言中的强制类型转换在C++中同样适用,具体使用方法可以参照下面的代码示例:
float valueA = 3.0f;
int valueB = (int) valueA;
可以看到,C语言中强制类型转换的一般格式为:
(类型说明符) 表达式
我们知道变量在内存中是以"...0101..."二进制格式存储的,一个int型变量一般占用32个位(bit),参考下面的代码
cpp
#include <iostream>
using namespace std;
int main(int argc, char** argv)
{
int num = 0x00636261;//用16进制表示32位int,0x61是字符'a'的ASCII码
int * pnum = #
char * pstr = reinterpret_cast<char *>(pnum);
cout<<"pnum指针的值: "<<pnum<<endl;
cout<<"pstr指针的值: "<<static_cast<void *>(pstr)<<endl;//直接输出pstr会输出其指向的字符串,这里的类型转换是为了保证输出pstr的值
cout<<"pnum指向的内容: "<<hex<<*pnum<<endl;
cout<<"pstr指向的内容: "<<pstr<<endl;
return 0;
}
在Ubuntu 14.04 LTS系统下,采用g++ 4.8.4版本编译器编译该源文件并执行,得到的输出结果如下:
第6行定义了一个整型变量num,并初始化为0x00636261(十六进制表示),然后取num的地址用来初始化整型指针变量pnum。接着到了关键的地方,使用reinterpret_cast运算符把pnum从int*转变成char*类型并用于初始化pstr。
将pnum和pstr两个指针的值输出,对比发现,两个指针的值是完全相同的,这是因为**"reinterpret_cast 运算符并不会改变括号中运算对象的值,而是对该对象从位模式上进行重新解释"。**
如何理解位模式上的重新解释呢?通过推敲代码11行和12行的输出内容,就可见一斑。
很显然,按照十六进制输出pnum指向的内容,得到636261;但是输出pstr指向的内容,为什么会得到"abc"呢?
在回答这个问题之前,先套用《深度探索C++对象模型》中的一段话,"一个指向字符串的指针是如何地与一个指向整数的指针或一个指向其他自定义类型对象的指针有所不同呢?从内存需求的观点来说,没有什么不同!它们三个都需要足够的内存(并且是相同大小的内存)来放置一个机器地址。指向不同类型之各指针间的差异,既不在其指针表示法不同,也不在其内容(代表一个地址)不同,而是在其所寻址出来的对象类型不同。也就是说,指针类型会教导编译器如何解释某个特定地址中的内存内容及其大小。 "参考这段话和下面的内存示意图,答案就可见一斑了。
使用 reinterpret_cast 运算符把 pnum 从 int* 转变成 char* 类型并用于初始化pstr后,pstr也指向num的内存区域 ,但是由于 pstr 是 char* 类型的,通过pstr读写num内存区域将不再按照整型变量的规则,而是按照char型变量规则。一个char型变量占用一个Byte,对pstr解引用得到的将是一个字符,也就是 'a' 。而在使用输出流输出 pstr 时,将输出 pstr 指向的内存区域的字符,那 pstr 指向的是一个的字符,那为什么输出三个字符呢?这是由于在输出 char* 指针时,**输出流会把它当做输出一个字符串来处理,直至遇到'\0'才表示字符串结束。**对代码稍做改动,就会得到不一样的输出结果,例如将 num 的值改为 0x63006261 ,输出的字符串就变为"ab"。
上面的例子融合了一些巧妙的设计,我们在pstr指向的内存区域中故意地设置了结束符'\0'。假如将num的值改为0x64636261,运行结果会是怎样的呢?
上面是我测试的截图,大家可以思考一下为什么在输出"abcd"之后又输出了6个字符才结束呢(提示:参考上面的内存示意图)?
但是在有些情况下,就不会这么幸运了,迎接我们的很可能是运行崩溃。例如我们直接将num(而不是pnum)转型为char*,再运行程序的截图如下
可以分析出,程序在输出 pstr 时崩溃了,这是为什么呢?pstr指向的内存区域的地址是0x64636261,而这片内存区域很有可能并不在操作系统为当前进程分配的虚拟内存空间中,从而导致段错误。
为什么会出现段错误?
-
类型不匹配:
num
是一个int
类型的变量,通常在32位系统上占用4字节,在64位系统上也通常是4字节。reinterpret_cast<char*>(&num)
将num
的地址(&num
)转换为char*
,这意味着pstr
会指向num
中存储的内存地址的第一个字节(即0x64
,对应字符 'd')。
-
内存访问方式不当:
num
是一个int
,它的内存布局是连续的4个字节。而char*
类型被视为指向单个字节的数据,因此reinterpret_cast<char*>(&num)
认为它指向的是num
地址处的字节数据。pstr
(char*
)可以访问num
中的字节(按字节操作),但是当你尝试将pstr
当作字符串来打印时,程序会继续读取内存,直到遇到一个\0
字符 。如果num
里面没有\0
字符(例如num = 0x64636261
),就会读取超过实际数据的内存区域,导致访问非法内存。
-
段错误原因:
- 转换
num
为char*
后,pstr
指向的是num
中的字节数据(4个字节),但是因为没有适当的字符串结束符\0
(null terminator),程序会继续按字节读取,可能会访问到不属于num
的内存区域。 - 如果这种非法的内存访问发生在程序的保护区(例如没有权限访问的内存),操作系统会引发一个段错误(Segmentation Fault),导致程序崩溃。
- 转换
总而言之,为了避免这种段错误,必须确保:
- 使用正确的指针类型转换和访问方式。
- 如果要将
num
作为字符数组来处理,确保它是以\0
结束的。