在C语言中,如果赋值运算符左右两侧类型不同,或者形参与实参类型不匹配,或者返回值类型与接收返回值类型不一致时,就需要发生类型转化,C语言中总共有两种形式的类型转换:隐式类型转换和显式类型转换。
例如:
void Test()
{
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);
}
缺陷:转换的可视性比较差,所有的转换形式都是以一种相同形式书写,难以跟踪错误的转换。
标准 C++ 为了加强类型转换的可视性,引入了四种命名的强制类型转换操作符:
static_cast、reinterpret_cast、const_cast、dynamic_cast
static_cast 用于非多态类型的转换(静态转换),编译器隐式执行的任何类型转换都可用 static_cast,但它不能用于两个不相关的类型进行转换。它对应的是C语言的隐式类型转换。
例如,相近的类型可以用 static_cast,即意义相似的类型,如下:
int main()
{
double d = 12.34;
int a = static_cast(d);
cout << a << endl;
return 0;
}
但是有一定关联,意义不相似的类型不可以用 static_cast,例如:
int* ptr = static_cast(a);
以上语句会发生报错;那么以上语句应该用什么呢?我们往下看。
reinterpret_cast 操作符通常为操作数的位模式提供较低层次的重新解释,用于将一种类型转换为另一种不同的类型。
像上面这种意义不相似的类型我们应该使用 reinterpret_cast,例如:
int main()
{
int a = 12;
int* ptr = reinterpret_cast(a);
return 0;
}
const_cast 最常用的用途就是删除变量的 const 属性,方便赋值。
例如:
int main()
{
const int a = 2;
int* p = const_cast(&a);
*p = 3;
return 0;
}
但是这里有一个奇怪的现象,我们将 a 的值和 *p 的值打印出来,并且将它们的地址打印出来观察:
我们会发现,a 和 p 的地址是一样的,但是当我们修改 *p 的时候,a 的值为什么不变呢?
其实这里和编译器的优化有关系,const 修饰的变量,编译器通常会对它进行优化,它通常会认为 const 修饰的变量不会被修改,所以编译器不会每次都去内存去取数据,它会将数据放在寄存器,甚至用一个常量去替代,类似于宏一样,当我们需要打印数据时,就直接用初始数据替代我们的 const 变量;所以当我们内存中的数据被修改了,但是编译器没有去内存中去取数据,所以 a 的值没有受影响。
如果我们想让编译器每次都去内存中去取数据呢?我们可以使用关键字 volatile,我们在 const 变量前加上这个关键字,就是告诉编译器不需要对该 const 变量进行优化,每次都去内存中取数据,如下:
int main()
{
volatile const int a = 2;
int* p = const_cast(&a);
*p = 3;
cout << a << endl;
cout << *p << endl;
cout << &a << endl;
cout << p << endl;
return 0;
}
我们可以看到 a 和 *p 的值就一样了。但是我们又发现了另外一个问题,为什么 &a 的值是 1 呢?这是因为 cout 对 &a 识别的时候匹配错了,我们只需要将 &a 强转成如下即可:
如果以上的转换我们使用C语言的强制类型转换可以吗?我们可以尝试一下:
int main()
{
const int a = 2;
int* p = (int*)&a;
*p = 3;
cout << a << endl;
cout << *p << endl;
cout << &a << endl;
cout << p << endl;
return 0;
}
如上图,也是可以完成转换的。那么C++为什么要使用这几种类型转换的方式呢?其实C++是为了增强程序的可读性,为了将它们区分开来,例如意义相类似的就用 static_cast;意义不相似的就用 reinterpret_cast;const_cast 就说明这个类型转换不安全。
dynamic_cast 用于将一个父类对象的指针/引用转换为子类对象的指针或引用(动态转换),这个是C语言不具备的。
怎么理解向上转换呢?假设有一个父类 A,一个子类 B,以下场景是不需要转换的,因为符合赋值兼容规则:
int main()
{
B objb;
A obja = objb;
A& ra = objb;
return 0;
}
其中 A& ra = objb;
ra 引用的是objb 中父类的部分,即发生了切割,ra 就是 objb 中父类的部分的别名。
向下转换的规则:父类对象不能转换成子类对象,但是父类指针和引用可以转换成子类指针和引用。
如果我们直接使用强制类型进行向下转换,是不安全的,例如以下场景:
有两个类,分别是父类和子类:
class A
{
public:
virtual void f(){}
int _a = 1;
};
class B : public A
{
public:
int _b = 2;
};
当我们分别定义两个类的对象,传给 func 函数:
void func(A* pa)
{
B* ptr = (B*)pa;
ptr->_a++;
ptr->_b++;
}
int main()
{
A a;
B b;
func(&a);
func(&b);
return 0;
}
如果是 func(&b);
那么在 func 函数内就是将父类的对象重新转换为子类,是没有问题的,因为在传入前它本身就是子类的对象。
但是如果是 func(&a);
就会存在越界问题,因为在传入时是父类的对象,在 func 函数内部将该父类对象强制转换成子类对象,那么它本身是父类对象,现在强转为子类对象后,它就可以访问不属于自己的空间 _b,也就是越界访问了,所以是存在问题的。
所以说向下转换直接进行转换是不安全的!
所以C++提供了一种安全的类型转换方式:dynamic_cast
,我们可以使用 dynamic_cast 对上面的代码进行修改:
void func(A* pa)
{
B* ptr = dynamic_cast(pa);
if (ptr)
{
cout << "转换成功" << endl;
ptr->_a++;
ptr->_b++;
}
else
{
cout << "转换失败" << endl;
}
}
int main()
{
A a;
B b;
func(&a);
func(&b);
return 0;
}
其中,dynamic_cast 会自动帮我们识别它之前是父类的对象还是子类的对象,从而帮我们实现转换,如果它之前是父类,现在转换为子类,那么就是不可以的,会转换失败,转换失败会返回空;如果它之前是子类,变成父类后又转换为子类,是可以的,就帮我们进行转换。dynamic_cast 还需要一个前提,就是父类必须要有虚函数。
对上面的代码进行测试,当传入父类的对象,转换失败:
当传入子类的对象,转换成功:
总结: