C++ Type Casting
一直搞不清楚 static_cast, dynamic_cast, reinterpret_cast, const_cast 的作用以及区别,花个时间学习并记录一下!
隐式类型转换
当一个值拷贝到与其类型相兼容的类型时,隐式类型转换会自动执行,这个在 POD(Plain Old Data) 或基础数据类型中经常看到,例如:
1 | short a = 2000; |
这里,short 类型的对象 a,首先隐式转换成 int 类型,然后直接赋值给了 int 类型的对象 b。这是我们所熟知的标准类型转换。标准类型转换作用于基础数据类型,并且允许数值类型 (short to int, int to float, double to int …) 到 bool 类型或 bool 类型到数值类型的转换,当然还有一些指针类型的转换,这些都是标准类型转换。
从一些较小的 integer 类型转换到 int 类型的时候,或是从 float 类型转换到 double 类型的时候,是保证不会丢失精度的,保证值是不会变的,可以把这种转换称之为“提升”(promoted)。其他数值类型之间的转换并不能保证值不变:
- integer 类型转换到 unsigned integer 类型,最高位的 1 的语义将会发送改变,从原来表示符号变成表示数值。(即:原来的 -1 如今变成了最大的值,-2 变成第二大的值 …)
- bool 类型的 false 相当于数值类型的 0 以及指针类型的 nullptr;然而 bool 类型的 true 相当于数值类型除 0 以外的所有值,如果把 true 转成数值类型的话,那将会是 1。
- 如果转换发生在浮点类型到整数类型上,那么结果将会被截断,如果结果超过了整数类型所能表示的范围,那么这个转换将会导致未定义的行为。
- 如果转换发生在相似类型之间(integer to integer, floating to floating)那么这个转换是有效的,但是具体的值视情况而定(并且这种转换可能是不可移植的 non-portable)
上述的这些隐式转换中,有一些是会损失精度的,那些会损失精度的转换会有编译器给出警告,你可以通过显示转换来消除警告。
对于非基础数据类型而言,数组和函数能够隐式转换为指针,并且指针通常允许以下几种转换:
- 空指针(nullptr)可以转换到指向任意类型的指针
- 指向任意类型的指针能够转换到指向 void 的指针
- Pointer upcast: 指向派生类的指针能够在不改变 const 或 volatile 约束条件的情况下转换成指向其基类的指针。
类的隐式转换
对于类来说,可以通过以下三种成员函数来控制隐式类型转换:
- Single-argument Constructor: 允许从特定类型隐式转换来初始化对象。
- Assignment operator: 允许从特定类型隐式转换给对象赋值。
- type-cast operator: 允许隐式转换到一个特定类型。
举个栗子:
1 | // implicit conversion of classes: |
注意:type-cast operator 使用特殊的语法,它使用 operator 作为关键字,在其后跟转换后的类型以及一对空括号。返回值类型就是转换后的类型,所以就没必要在 operator 前加上返回值类型了。
四种 casting
C++ 是一门强类型语言。许多转换都需要显式的说明,特别是那些具有不同值语义的转换。下面就有个例子:
1 | double x = 10.3; |
对于基础数据类型而言,以上两种同用的显式转换方式已经够用了。然而,如果对类,类指针不加区分地使用上述地方式会导致程序的运行时错误(即使编译是能通过的)。举个栗子:
1 | // class type-casting |
以上代码编译能通过,但是会产生运行时错误。程序声明了一个类 Dummy 的对象 d 和指向 Addition 的指针 padd,然后把 d 的地址经过显式强制类型转换到指向 Addition 的指针再赋值给 padd,随后调用了类 Addition 的方法。
在 C++ 的显示强制类型转换可以在任意两种类型之间进行,这就回来带巨大的隐患,想一想,一种类型被显式地强制类型转换到另一种完全不相关的类型,然后在调用该类型的操作会导致什么情况,毫无疑问会产生运行时错误,轻则 core dump,重则程序不声不响的继续给你运行下去,你完全不知道哪里出问题了,然后用了很长时间来找错,浪费时间浪费精力,哎~~~
不受限制的显式类型转换允许指针的类型转换到指向其他的任意类型,甚至是与其原本毫不相关的类型。
为了控制这些在类型间的转换,C++ 就引入了四种转换操作符:dynamic_cast,reinterpret_cast,static_cast,const_cast。它们具有相同的使用格式,<> 内是要转换的类型,() 内是将要被转换的表达式。
不受限制的显式类型转换 ==> 受限制的类型转换 ==> 提高安全性
1 | dynamic_cast <new_type> (expression) |
dynamic_cast
dynamic_cast 能够作用在指向类对象的指针或引用上(或作用在 void*)。它保证了类型转换后的结果指针(引用)一定指向有效的完整的类对象。
它不仅能把指向类对象的指针 upcast (converting from pointer-to-derived to pointer-to-base) ,还能 downcast (convert from pointer-to-base to pointer-to-derived) 到指向多态类对象(带有虚成员函数的类),但是这个类对象必须是有效且完整的。举个栗子:
1 | // dynamic_cast |
上述代码跑起来后会输出以下结果:
1 | Null pointer on second type-cast. |
上面的代码尝试执行从指向 Base 类型的指针对象 ( pba 和 pbb ) 到指向 Derived类型的指针对象的两次动态强制转换,但只有第一次成功。注意它们各自的初始化
1 | Base * pba = new Derived; |
尽管两者都是指向 Base 类型的指针,但 pba 实际上指向的是 Derived 类型的对象,而 pbb 指向的是 Base 类型的对象。因此,当使用动态强制转换执行它们各自的类型转换时,pba 指向的是 class Derived 的完整对象,而 pbb 指向的是 class Base 的对象,这是 class Derived 的不完整对象。
当 dynamic_cast 转换由于不满足目标类是有效且完整这个条件的时候,它会返回 null 指针来表示转换失败了,如果 dynamic_cast 用来转换引用类型并且失败了,它会抛出一个 bad_cast 的异常。
static_cast
static_cast 也能完成 upcast 和 downcast,和 dynamic_cast 的唯一区别就在于,它不会帮你检查转换后的类型是否是有效且完整的,这个检查得你自己来做,这么做有好处也有坏处。好处在于降低了运行时开销,坏处是需要程序员对自己的代码十分了解。
dynamic_cast 能做的 static_cast 都能做,初此之外,它还能对以下几种情况进行转换:
- 显式调用单参数构造函数(Single-argument constructor)或转换操作符(type-cast operator)。
- 转换到右值引用
- 把 enum class 转换成整数类型或浮点类型
- 把任意类型转换到 void 类型
注意:static_cast 不能转换掉 expression 的 const、volatile、或者 __unaligned 属性。
C++ primer第五章 里写了编译器隐式执行任何类型转换都可由 static_cast 显示完成
reinterpret_cast
reinterpret_cast 能够将任意指针类型转换为任意其他指针类型,即使是不相关的类。它的操作结果是简单的二进制位的拷贝和赋值,什么意思呢?看看下面的例子:
1 | long long int i; |
这里 i 是 int 类型的,p 是指向 char 类型的指针对象,这是两个完全不相关的类型,i = reinterpret_cast<int>(p)
操作就是简单把对象 p 的地址处的二进制值原封不动地存放到对象 i 的地址处。所以你可以打印出 p 和 i 地址处存放的值,可以看到是完全一样的。由于 long long int 类型为 8 个字节,char 类型在 64 位下是 8 个字节,所以转换前后没有数位损失。
const_cast
const_cast 一般用于强制消除对象的常量性。它是唯一能做到这一点的 C++ 风格的强制转型。这个转换能剥离一个对象的 const属性,也就是说允许你对常量进行修改:
1 | // const_cast |
print 函数只接受 non-const 数据,那你就可以用 const_cast 去除对象 c 的 const 属性。
上面的例子保证可以工作,因为函数 print 不会往指向的对象写数据。但是请注意:移除指向对象的常量后,实际向它写入数据会导致未定义的行为。