目录
1、C语言中的类型转换
2、为什么C++需要四种类型转换
3、C++强制类型转换
static_cast
reinterpret_cast
const_cast
dynamic_cast
4、RTTI(了解)
5、常见面试题
在C语言中,如果赋值运算符左右两侧类型不同,或者形参与实参类型不匹配,或者返回值类型与接收返回值类型不一致时,就需要发生类型转化,C语言中总共有两种形式的类型转换:隐式类型转换和显式类型转换。
- 隐式类型转化(截断或提升):编译器在编译阶段自动进行,能转就转,不能转就编译失败
- 显式类型转化(强转):需要用户自己处理
C++对于C语言中的隐式类型转换并不太“满意”,因为其中有很多的坑,比如我们之前模拟实现string类中的insert函数,如果按照如下的方式写就是错的:
class myString { public: void insert(size_t pos, char ch) { //扩容…… //挪动数据 size_t end = _size; while (end >= pos) { _str[end + 1] = _str[end]; --end; } //放入插入数据…… } private: char* _str; size_t _size; size_t _capacity; };
- 当pos等于0时,就会出现问题了,程序会进入死循环,当pos=0时此段程序的终止条件是end<0,但是end的数据类型为unsigned int,所以无论end怎么--都会始终>=0,可能有人会说那把end换成int类型不就行了吗,大错特错,此时就会发生整型提升(隐式类型转换),你end是int类型,但是pos是unsigned int无符号整型,这里会把int提升转换为unsigned int类型,又导致end无论怎么--都会始终>=0,最终程序陷入死循环。所以迫不得已我们当时的解决办法是把end放到_size + 1的位置,从而防止后续出现整型提升等问题。
此外,只有相近类型之间才能发生隐式类型转换,比如int、char、double、unsigned int表示的都是数据的大小,只不过它们表示的范围和精度不同,它们之间可以发生隐式类型转换,而指针类型表示的是地址编号,因此整型和指针类型之间不能发生隐式类型转换,若需要转换只能显示类型转换。如下示例:
int main() { int i = 1; //隐式类型转换 -- 相近类型(意义相近) double d = i; printf("%d, %.2f\n", i, d); int* p = &i; //显示的强制类型转换 -- 不相似类型 int address = (int)p; printf("%x, %d\n", p, address); return 0; }
C风格的转换格式很简单,但是有不少缺点的:
- 隐式类型转化有些情况下可能会出问题:比如数据精度丢失
- 显式类型转换将所有情况混合在一起,代码不够清晰
因此C++提出了自己的类型转化风格,注意因为C++要兼容C语言,所以C++中还可以使用C语言的转化风格。
标准C++为了加强类型转换的可视性,引入了四种命名的强制类型转换操作符:
- static_cast、reinterpret_cast、const_cast、dynamic_cast
下面来分开来讨论。
static_cast用于非多态类型的转换(静态转换),编译器隐式执行的任何类型转换都可用
static_cast,但它不能用于两个不相关的类型进行转换。//static_cast 相近类型之间的转换 int main() { double d = 12.34; int a = static_cast
(d); cout << a << endl;//12 /*int* p = &a; int x = static_cast (p);不是相近类型,不支持转换*/ return 0; }
reinterpret_cast操作符通常为操作数的位模式提供较低层次的重新解释,用于将一种类型转换
为另一种不同的类型。//reinterpret_cast 不相近类型之间的转换 int main() { double d = 12.34; int a = static_cast
(d); cout << a << endl;//12 //int *p = static_cast (&a);这里使用static_cast会报错,应该使用reinterpret_cast int* p = reinterpret_cast (&a); cout << *p << endl;//12 return 0; } reinterpret_cast还有一个非常bug的用法,比如下面的代码中将带参带返回值的函数指针转换成了无参无返回值的函数指针,并且还可以用转换后的函数指针调用此函数。
typedef void (*FUNC)(); int DoSomething(int i) { cout << "DoSomething" << endl; return 0; } int main() { //下面转换函数指针的代码是不可移植的 FUNC f = reinterpret_cast
(DoSomething); f();//DoSomething return 0; } reinterpret_cast可以让编译器以FUNC的定义方式去看待DoSomething函数,所以非常的bug,C++不保证所有的函数指针都被一样的使用,这样使用有时会产生不确定的结果,所以并不建议这样使用。
const_cast也是不同类型之间的转换,最常用的用途就是删除变量的const属性,方便赋值,转换后就可以对const变量的值进行修改,如下:
int main() { const int a = 2; int* p = const_cast
(&a);//取消变量a的const属性 *p = 3; cout << a << endl; //2 cout << *p << endl;//3 return 0; }
- 在一开始我定义了const属性的变量a,随后使用const_cast取消了a的const属性,这样就可以通过此指针来修改变量a的值。
为什么我取消了a的const属性,并后续通过修改*p的方式,为何a还是2呢?原因如下:
- 这里设计到了编译器的优化,编译器默认cosnt修饰的变量是不会被修改的,因此会将cosnt修饰的变量a放到寄存器中,当需要读取const变量时就会直接从寄存器中读取,但是我们实际修改的是内存中a的值,所以最终导致输出的a是未修改前的值2。
如果我非要修改a呢?该如何解决呢?
- 如果不想让编译器将const变量优化到寄存器当中,只需要加一个关键字volatile对const变量进行修饰即可,这个关键字的作用是让编译器强制去内存中读取,这样我们就能看到修改后的结果了。
int main() { //volatile强制每次访问变量a都去内存中去读取,防止编译器的优化 volatile const int a = 2; int* p = const_cast
(&a);//取消变量a的const属性 *p = 3; cout << a << endl; //3 cout << *p << endl;//3 return 0; } 在C语言中,没有const_cast,但是C语言是通过强制类型转换的方式完成上述目的的:
int main() { //volatile强制每次访问变量a都去内存中去读取,防止编译器的优化 volatile const int a = 2; //int* p = const_cast
(&a);//C++取消变量a的const属性 int* p = (int*)&a;//C语言强转 *p = 3; cout << a << endl; //3 cout << *p << endl;//3 return 0; } 总结:
- C++继续兼容C的类型转换,但是期望大家使用上面规范的转换,可读性会提升,出错的概率会降低
- C++中的static_cast对应C语言中的隐式类型转换
- C++中的reinterpret_cast和const_cast对应C语言中的强制类型转换
dynamic_cast用于将一个父类对象的指针/引用转换为子类对象的指针或引用(动态转换),即向下转换,当然也有向上转换,如下的介绍:
- 向上转型:子类对象指针/引用->父类指针/引用(不需要转换,赋值兼容规则,天然支持)
- 向下转型:父类对象指针/引用->子类指针/引用(用dynamic_cast转型是安全的)
注意:
- dynamic_cast只能用于父类含有虚函数的类
- dynamic_cast会先检查是否能转换成功,能成功则转换,不能则返回0
向上转换就是我们先前学过的切割、切片,是语法天然支持的,不需要进行转换,而向下转换是语法不支持的,需要进行强制类型转换,并且只有指针和引用才支持向下转换,对象不支持。为什么要支持向下转换呢?看如下的代码:
class A { public: virtual void f() {} }; class B : public A {}; //pa可能指向父类对象,也可能指向子类对象 void fun(A* pa) { //…… } int main() { A a; B b; fun(&a); fun(&b); return 0; }
上述代码中,我fun函数中的父类指针pa到底是指向父类的对象,还是指向子类的对象呢?针对这两种情况我做出下面的讨论:
- 如果父类的指针(或引用)指向的是一个父类对象,那么将其转换为子类的指针(或引用)是不安全的,因为转换后可能会访问到子类的资源,而这个资源是父类对象所没有的。
- 如果父类的指针(或引用)指向的是一个子类对象,那么将其转换为子类的指针(或引用)则是安全的。
使用C语言的强制类型转换进行向下转型是不安全的,因为此时无论父类的指针(或引用)指向的是父类对象还是子类对象都会进行转换。而使用dynamic_cast进行向下转型则是安全的,如果父类的指针(或引用)指向的是子类对象那么dynamic_cast会转换成功,但如果父类的指针(或引用)指向的是父类对象那么dynamic_cast会转换失败并返回一个空指针。比如:
class A { public: virtual void f() {} }; class B : public A {}; void func(A* pa) { // dynamic_cast会先检查是否能转换成功,能成功则转换,不能则返回 B* pb1 = (B*)pa; //不安全 B* pb2 = dynamic_cast(pa); //安全 cout << "pb1: " << pb1 << endl; cout << "pb2: " << pb2 << endl; } int main() { A a; B b; func(&a); func(&b); return 0; }
总结:
- 强制类型转换关闭或挂起了正常的类型检查,每次使用强制类型转换前,我们应该仔细考虑是否还有其他不同的方法达到同一目的,如果非强制类型转换不可,则应限制强制转换值的作用域,以减少发生错误的机会。强烈建议:避免使用强制类型转换,尽可能使用dynamic_cast。
RTTI:Run-time Type identification的简称,即:运行时类型识别。C++通过以下方式来支持RTTI:
- typeid运算符:在运行时识别出一个对象的类型
- dynamic_cast运算符:在运行时识别出一个父类的指针(或引用)指向父类对象还是子类对象
- decltype:在运行时推演出一个表达式或函数返回值的类型
1、C++中的4中类型转化分别是:_________、_________、_________、_________
- 答:为static_cast、reinterpret_cast、const_cast、dynamic_cast
2、说说4中类型转化的应用场景。
①:static_cast
- 没有运行时类型检查来保证转换的安全性
- 进行向上转换(把派生类的指针或引用转换成基类表示)是安全的
- 进行向下转换(把基类的指针或引用转为派生类表示),由于没有动态类型检查,所以是不安全的
使用:
- 用于基本数据类型之间的转换,如把int转换为char
- 把任何类型的表达式转换为void类型
②:reinterpret_cast
- 可以将整型转换为指针,也可以把指针转换为数组,可以在指针和引用力进行肆无忌惮的转换,平台移植性比较差
③:const_cast
- 常量指针转换为非常量指针,并且仍然指向原来的对象,常量引用被转换为非常量引用,并且仍然指向原来的对象。去掉类型的const或volatile属性。
④:dynamic_cast
- 在进行向下转换时,dynamic_cast具有类型检查(信息在虚函数中)的功能,比static_cast更安全。
- 转换后必须是类的指针、引用或者void*,基类要有虚函数,可以交叉转换。
- dynamic本身只能用于存在虚函数的父子关系的强制类型转换;对于指针,转换失败则返回nullptr,对于引用,转换失败会抛出异常。