一、C语言的类型转换
在C语言中,如下场景会发生类型转换:
- 赋值运算符左右两侧类型不相同。
- 形参与实参类型不匹配。
- 返回值类型与接收返回值类型不一致。
C语言中一共有两种形式的类型转换:
- 隐式类型转化:编译器在编译阶段自动进行,能转就转,不能转就编译失败。
- 显式类型转化:需要用户自己手动进行类型转换。
隐式类型转换适用于相似类型之间的转换,比如 char、int、double 这类整形家族之间的互转;而强制类型转换适用于不相关类型的转换,比如 int 和 int*。
代码语言:javascript复制void test1()
{
int i = 1;
// 隐式类型转换
double d = i;
printf("%d, %.2fn", i, d);
int* p = &i;
// 显示的强制类型转换
int address = (int)p;
printf("%x, %dn", p, address);
}
二、C 的类型转换
C风格的转换格式很简单,但是存在一些缺点:
- 隐式类型转化在有些情况下可能会出问题:比如数据精度丢失。
- 所有的显示类型转换形式都是以一种相同形式书写,代码不够清晰,发生错误时也难以辨别跟踪。
基于C风格类型转换存在的一些缺点,C 提出了自己的类型转化风格,具体来说引入了四种命名的强制类型转换操作符,它们加强了类型转换的可视性:static_cast、reinterpret_cast、const_cast、dynamic_cast
。
注意:由于 C 要兼容C语言,所以 C 中仍然可以使用C语言的转化风格。
三、C 强制类型转换
1、static_cast
static_cast
适用于隐式类型转换的场景,即适用于相似类型之间的转换;如果我们使用 tatic_cast
进行不相关类型之间的转换,编译器会直接报错。
void test2()
{
int a = 12;
//正确写法
double b = static_cast<double>(a);
cout << b << endl;
//错误写法
int* pa = static_cast<int*>(a);
cout << pa << endl;
}
2、reinterpret_cast
reinterpret_cast
适用于不相关类型之间的转换,即重新解释一种类型的含义。
void test2()
{
int a = 12;
//正确写法
double b = static_cast<double>(a);
cout << b << endl;
//这里使用static_cast会报错,应该使用reinterpret_cast
int* pa = reinterpret_cast<int*>(a);
cout << pa << endl;
}
3、const_cast
const_cast
适用于 const 类型和非 const 类型之间的转化,即删除变量的 cons t属性,从而方便赋值。
void test3()
{
const int a = 2;
int* p = const_cast<int*>(&a);
*p = 3;
cout << a << endl;
}
拓展:volatile 关键字的用途
相信有的同学看到上面的输出结果会有疑惑:这里我们将 a 变量的地址通过 const_cast
转换之后赋值给指针变量 p,然后通过 p 将变量 a 的值修改为3;通过监视窗口我们也观察到内存中变量 a 的值确实变成了3,但是为什么终端打印出来的值是2呢?
这其实是因为变量 a 在定义时被 const 修饰,而编译器认为 a 的值不会被修改,所以编译器会将 a 的值放入一个寄存器中,以后每次使用 a 都直接从该寄存器中读取,而不再从内存中读取;这就导致了我们虽然通过指针变量 p 修改了内存中 a 的值,但寄存器中保存的仍然是 a 修改之前的值,所以打印出来的是 2。
要解决这个问题也很简单,我们在定义常变量 a 时使用 volatile
关键字进行修饰即可;volatile
关键字的作用是保持内存可见性,即每次都从内存中读取变量的值。
这个例子其实也可以反映出为什么 C 要设计出 const_cast
强制类型转换操作符来用于 const 类型和非 const 类型之前的转换 – 它从侧面提醒了程序员使用 const_cast
时要注意使用当前普通变量对程序其他位置常变量值的修改。
4、dynamic_cast
前面在学习继承时,我们提到过由于子类中包含父类,所以 子类对象/子类对象的指针/子类对象的引用 赋值给 父类对象/父类对象的指针/父类对象的引用 的过程是天然的,中间没有类型转换,也不会产生临时对象,我们称这个过程为切片/向上转型。
而向下转型则是指将 父类对象/父类对象的指针/父类对象的引用 赋值给 子类对象/子类对象的指针/子类对象的引用,由于父类中并没有子类,所以向上转型是不安全的,很有可能发生越界访问。如下:
代码语言:javascript复制class A
{
public:
virtual void f() {}
int _a = 1;
};
class B : public A
{
public:
void f() {}
int _b = 2;
};
void fun(A* pa)
{
//当pa指向的是B类时,这里就是B类型转为B类型,不会发生越界
//当当pa指向的是A类时,这里就是A类型转为B类型,可能会发生越界
B* pb = (B*)pa;
cout << pb->_b << endl;
}
而dynamic_cast
的作用就是将一个父类对象的指针/引用转换为子类对象的指针或引用 (向下转型)。需要注意的是:
dynamic_cast
只能用于父类含有虚函数的类。dynamic_cast
会检查是否能转换成功,能则进行转换,不能则返回0。
void fun(A* pa)
{
//当pa指向的是B类时,这里就是B类型转为B类型,不会发生越界
//当当pa指向的是A类时,这里就是A类型转为B类型,可能会发生越界
B* pb = dynamic_cast<B*>(pa);
cout << pb->_b << endl;
}
注意:一般情况下我们应该避免使用强制类型转换,因为强制类型转换关闭或挂起了正常的类型检查;所以每次在使用强制类型转换前,程序员应该仔细考虑是否还有其他不同的方法达到同一目的,如果非强制类型转换不可,则应限制强制转换值的作用域,以减少发生错误的机会。
四、RTTI
RTTI 是 Run-time Type identification 的简称,即运行时类型识别。C 有如下方式来支持 RTTI:
- typeid:在运行时识别出一个对象的类型。
- decltype:在运行时推演出一个表达式或函数返回值的类型。
- dynamic_cast**:**在运行时识别出一个父类的指针/引用指向的是父类对象还是子类对象。
注意:C 中的 auto 并不属于 RTTI,auto 是一种变量类型推导机制,它能够根据变量的初始化表达式自动推导出变量的类型,属于编译时识别;而 RTTI 是一种运行时类型识别机制。