C/C++的旧式类型转换
在C语言中,类型转换被分为显式和隐式,常见的类型转换如下:
整型之间,int
,long long
,long
,shrot
,char
等互相转换
整型与浮点数之间,int
,long
等与float
,double
之间的转换
- 整型与布尔值,非0为
true
,0为false
- 指针与布尔值,空指针为
false
,非空指针为true
- 整型与指针之间
- 不同指针之间
C++后,增加了类的概念,类型就丰富了起来,于是又多出了以下与类相关的转换:
- 内置类型 → 自定义类型,依赖构造函数
- 自定义类型 → 自定义类型,依赖构造函数
- 自定义类型 → 内置类型,依赖操作符重载
前两个是类中常见的,不讲解了,而C++支持通过操作符重载来进行自定义类型 → 内置类型的转换
比如:
class A { public: operator int() { return _a + _b; } operator double() { return _a / _b; } private: int _a = 3; int _b = 5; };
类A
支持两种类型的转换:A -> int
和A -> double
,这是通过操作符重载实现的,operator type
就可以完成到type
类型的转换。注意的是,类型转换的返回值是固定的,所以operator type
左侧不用写函数返回类型。
那么类A
就可以完成以下转换:
A aa; int x = aa; double y = aa;
以上所有类型转换,都是旧式的,是基础C语言的显式与隐式的规则,衍生出来的各种类型转换。
C语言风格的类型转换很简洁,但是也有缺点:
- 转换的分类过于笼统,只分显式和隐式
- 隐式转换非常坑,很容易出bug
比如以下代码:
size_t pos = 0; int end = 10; while (end >= pos) { cout << end << endl; end--; }
这个代码,乍一看没问题,输出10 - 0
的所有数字,但是其实这是一个死循环代码。
由于pos
的类型是无符号整型,当end
与pos
放在一个表达式中比较,此时end
会发生类型转化,从有符号变成无符号,那么end
就永远>= 0
,陷入死循环了。
这个问题就是隐式类型转换导致的。
C++的现代类型转换
C++提供了四种类型转化,以更加细致安全的方式来区分各种类型转换。
静态类型转换 static_cast
用途:
在类型之间进行安全的转换
格式:
static_cast<type>(expression)
任何能够明确的类型转换都可以使用static_cast(static_cast不能转换掉底层const和volatile),以上说明中,安全的转换不是指编译器帮你确认安全,而是说程序员自己确定了这个转换是安全的之后,再用static_cast。
特点在于:不提供运行时的检查,所以叫静态类型转换
主要在以下几种场合中使用:
- 从
整型
到浮点型
的转换(原先的隐式转换):
int x = 10; double y = static_cast<double>(x);
只要是隐式转换可以做到的,都推荐改用static_cast
。
- 从
派生类
到基类
的转换(上行转换):
class Base { //... }; class Derived : public Base { //... }; int main() { Derived d; Base* b = static_cast<Base*>(&d); return 0; }
- 从
void*
到具体类型指针
的转换(指针之间的转换):
void* p = malloc(sizeof(int)); int* x = static_cast<int*>(p);
重新解释转换 reinterpret_cast
用途:
进行不安全的类型转换
格式:
reinterpret_cast<type>(expression)
这是非常激进的指针类型转换,在编译期完成,可以转换任何类型的指针,所以极不安全。
它仅仅重新解释内存中的二进制表示,而不执行任何实际的类型检查,因此非常危险,不建议使用。
- 将一个指针转换为整型:
int x = 1; int p = reinterpret_cast<int>(&x);
- 在不同的指针类型之间进行转换:
int d; double* p = reinterpret_cast<double*>(&d);
常量类型转换 const_cast
用途:
移除或添加对象的
const
属性和volatile
属性
格式:
const_cast<type>(expression)
- 移除
const
属性:
const int x = 10; int* p = const_cast<int*>(&x); *p = 20; // 修改常量对象的值
但是以上代码暗含一个小问题,我们尝试输出一下x
和p
:
cout << x << endl; cout << *p << endl;
输出结果:
10 20
怎么回事?我们明明通过指针修改了x,为什么x是10,而p是20?
实际上,C++的常量不存储在常量区,而是存储在栈区,而编译器会对const变量优化,把const变量放到寄存器中,后续只要访问变量a,都去寄存器中查找。我们通过指针p修改的变量,其实修改的是栈区中的数据,没有修改寄存器的数据,因此访问a还是从寄存器中读出了10。
为了解决这个问题,可以用volatile
关键字修饰const
变量,此时就不会把常量存在寄存器中了。
volatile const int x = 10; int* p = const_cast<int*>(&x); *p = 20; cout << x << endl; cout << *p << endl;
此时的输出结果为:
20 20
由于volatile
的出现,const
的种类也变多了,比如以下代码:
volatile const int x = 10; const int* p = &x;
这是一段错误的代码,&x
的类型是volatile const int*
,与const int*
不同,此时编译器会报错。而const_cast
不止可以消除const
属性,也可以消除volatile
属性。
- 消除
volatile
属性
volatile const int x = 10; const int* p = const_cast<const int*>(&x);
动态类型转换 dynamic_cast
用途:
在继承层次结构中进行安全的向下转换,检查转换是否成功。
格式:
dynamic_cast<type>(expression)
相比static_cast,dynamic_cast会在运行时检查类型转换是否合法,具有一定的安全性。由于运行时的检查,所以会额外消耗一些性能。
dynamic_cast常用于基类与派生类之间的转换,分为两种情况:
上行转换:从 派生类指针/引用 向 基类指针/引用 的转换,本质上是赋值兼容,一般来说是安全的
下行转换:从 基类指针/引用 向 派生类指针/引用 的转换,安全性不确定
一般来说,上行转换使用static_cast即可,因为这是一个比较安全的行为。而下行转换用dynamic_cast来检测安全性。
有如下特性:
- 使用
dynamic_cast
要求基类中必须有虚函数!- 如果转换成功,
dynamic_cast
返回转换后的指针,如果转换失败,dynamic_cast
返回空指针。
- 向下转换,并检查转换是否成功:
class Base { virtual void foo() {} }; class Derived : public Base { /* ... */ }; int main() { Base* b = new Base(); Derived* d = new Derived(); Base* p1 = static_cast<Base*>(d);//上行转换 Derived* p2 = dynamic_cast<Derived*>(p1); Derived* p3 = dynamic_cast<Derived*>(b); cout << p2 << endl; cout << p3 << endl; return 0; }
以上代码中,完成了两次下行转换,分别是p1 -> p2和d -> p3的转换。而p1是通过b转化而来的,所以p1是指向派生类的基类指针,而d是指向基类的基类指针。
因此p1转回派生类指针,是合理的,而d转为派生类指针,是危险的,于是dynamic_cast会阻止d转为派生类指针。
输出结果:
0000015220F64100 0000000000000000