目录
C语言中的类型转换
为什么C++需要四种类型转换
C++:命名的强制类型转换
static_cast
reinterpret_cast
const_cast
dynamic_cast
C语言中的类型转换
在C语言中,如果赋值运算符左右两侧类型不同,或者形参与实参类型不匹配,或者返回值类型与 接收返回值类型不一致时,就需要发生类型转化,C语言中总共有两种形式的类型转换:隐式类型 转换和显式类型转换。
1. 隐式类型转化:编译器在编译阶段自动进行,能转就转,不能转就编译失败
2. 显式类型转化:需要用户自己处理
int i = 10.2; // 隐式类型转换,会警告
int* p = (int*)i; // C显式强制类型转换
缺陷: 转换的可视性比较差,所有的转换形式都是以一种相同形式书写,难以跟踪错误的转换
为什么C++需要四种类型转换
C风格的转换格式很简单,但是有不少缺点:
1. 隐式类型转化有些情况下可能会出问题:比如数据精度丢失,或其他难以预见的错误。
2. 显式类型转换将所有情况混合在一起,代码不够清晰。
因此C++提出了自己的类型转化方式,注意因为C++要兼容C语言,所以C++中还可以使用C语言的 转化方式。
回顾std::string的模拟实现,编译器隐式类型转换带来的错误:
string& insert(size_t pos, char c)
{
assert(pos <= _size);
if(_size == _capacity)
{
reserve(_capacity == 0?4:2*_capacity);
}
int end = _size;
while(end >= pos)
// while(end >= (int)pos)
{
_str[end+1] = _str[end];
end--;
}
// 比较推荐的写法,主要是 int和size_t比较,会出现比较错误,当int小于0时。
// size_t end = _size+1;
// while(end > pos)
// {
// _str[end] = _str[end-1];
// end--;
// }
_str[pos] = c;
++_size;
return *this;
}
如上,while循环的判断部分,运算符的两个运算数的类型不同,int与size_t,此时会发生隐式类型转换,int -> size_t,若pos == 0,则会发生死循环,故产生了while(end >= (int)pos) // 显式类型转换的写法,或者下方更推荐的写法。
C++:命名的强制类型转换
一个命名的强制类型转换具有如下形式:
cast_name<type>(expression);
type是转换的目标类型,expression是要转换的值。
标准C++为了加强类型转换的可视性,引入了四种命名的强制类型转换操作符:
static_cast、reinterpret_cast、const_cast、dynamic_cast
static_cast
static_cast用于非多态类型的转换(静态转换),编译器隐式执行的任何类型转换都可用 static_cast,但它不能用于两个不相关的类型进行转换
1. 当需要把一个较大算术类型赋值给较小类型或整型与浮点类型相互赋值时,编译器通常会因为潜在的精度丢失告警,而static_cast相当于告诉编译器,我们知道并且不在乎潜在的精度丢失。
这属于是编译器隐式执行的类型转换。
int i2 = 10;
short s1 = static_cast<short>(i2);
float f1 = static_cast<float>(i2);
2. static_cast并非只能处理任何隐式类型转换,某些编译器无法自动执行的类型转换也有用。
如void* -> int* void* -> float*的转换,也就是使用static_cast找回存在于void*中的值。
void* p2 = &i2;
int* p3 = (int*)p2;
int* p4 = static_cast<int*>(p2);
但是注意,其他的指针类型之间的转换,不能使用static_cast。因为int* double* char*等不属于相关的类型,故static_cast主要适用于C编译器可以执行的隐式类型转换。
reinterpret_cast
reinterpret_cast通常为运算对象的位模式提供较低层次上的重新解释,用于将一种类型转换为另一种不同的类型。
reinterpret_cast通常适用于C语言编译器无法隐式类型转换的强制类型转换。
// 1
int* pi = new int(3);
char* pc = (char*)pi; // C style。必须强转
// char* pc2 = static_cast<char*>(pi); // Static_cast from 'int *' to 'char *' is not allowed
char* pc3 = reinterpret_cast<char*>(pi); // must use reinterpret_cast
// 5
int i = 10;
double* pd3 = (double*)i; // C style 强制转换
// double* pd4 = static_cast<double*>(i); // Cannot cast from type 'int' to pointer type 'double *'
double* pd5 = reinterpret_cast<double*>(i);
const_cast
const_cast只能改变运算对象的底层const,最常用的用途就是删除变量的const属性,方便赋值(顶层const指const修饰对象本身,底层const指const修饰指针所指向的对象)
void test_const_cast()
{
const int a = 10;
const int* pa = &a;
int* p = const_cast<int*>(pa);
*p = 20;
cout << a << endl; // 10
cout << *p << endl << endl; // 20
volatile const int i = 10; // volatile 易变的 也就是不允许编译器进行优化(寄存器or宏式替换优化),每次都去内存中取值
int* p2 = (int*)&i;
*p2 = 20;
cout << i << endl;
cout << *p2 << endl;
// 下方ptr指向的"aaa"其实是存储在常量区的,不同于上方
// const char* ptr = "aaa";
// char* p3 = const_cast<char*>(ptr);
// *p3 = 'z'; // 错误,非编译错误,本质因为"aaa"存储在常量区,不能被修改!!!
// cout << ptr << endl;
}
解析:
可以使用const_cast或强转将const int* 赋值给 int*,并使用这个int*修改常变量的值的根本原因是const int i这样的常变量没有存储在内存中的常量区,而是在栈区中,这样,如果得到一个int*指针指向常变量存储的地址,就可以修改它了。
而下方打印出a == 10是因为编译器的优化行为,编译器认为常变量不会改变所以将其存储在寄存器中一份,利用int*修改常变量是修改内存中的常变量,打印时打印出的是寄存器中的,故还是原来的值10。而后方*p从内存中取时,就是修改后的值了。
注意编译器对常变量的另一种优化方式是编译时进行类似于宏式的替换,直接将代码中的常变量换为常变量的初始值。事实上VS的编译器就是这样做的。(可通过反汇编看出)(YDYBJ)(注意,VS下的监视窗口实际上是调试器得出的数据信息,是从内存中获取的,所以在VS监视窗口查看上方代码中a的值就是修改后的值)
当然,const_cast的作用并不是让你去修改常变量的值。通常用于删除变量的const属性,方便赋值(变量本身没有const属性)
dynamic_cast
dynamic_cast用于将基类的指针或引用安全地转换成派生类的指针或引用。
dynamic_cast支持运行时类型识别! RTTI - run-time type identification,C++RTTI功能由两个运算符实现:typeid dynamic_cast (decltype)
回顾继承
向上转型:子类指针/引用 -> 父类指针/引用。这里并不属于类型转换,因为这是语法原生支持的,发生切片/切割。
向下转型:父类指针/引用 -> 子类指针/引用(用dynamic_cast转型是安全的)
(注意,子类对象赋值 或 拷贝构造父类对象,本质还是调用拷贝构造 or operator=,而参数为父类引用,故,本质还是父类引用引用子类对象。而父类对象无论如何都不能赋值给子类对象,同理子类引用无法直接引用父类对象)(说实话,其实我更喜欢称为基类和派生类妈的)
父类指针/引用可以强转为子类指针/引用,但这是危险的行为,因为父类指针/引用的指向不一定,若原本指向子类对象,则转为子类指针是合理的,但若原本指向父类对象,则强转后产生一个指向父类对象的子类指针,此时这个指针可以访问子类数据成员,但是是非法访问,因为对应数据不存在。
dynamic_cast可以检测父类指针/引用的指向,若指向子类对象,则安全,返回对应转换结果,若指向父类对象,则转换失败,指针转换失败则返回nullptr,引用转换失败则抛出一个bad_cast异常
示例代码一:dynamic_cast进行指针/引用转换
class Base
{
public:
virtual void func()
{}
public:
int _base = 10;
};
class Derived: public Base
{
public:
int _derived = 20;
};
void test_dynamic_cast()
{
Base b;
Derived d;
// 向上转型:从子类到父类,从派生类到基类
Base b2 = d; // 调用 Base(const Base& b); 父类引用引用子类对象
Base* pb = &d;
Base& fb = d;
// 向下转型
Base* pb2 = &b;
Derived* pd = (Derived*)pb2; // 这是不安全的
Derived* pd2 = static_cast<Derived*>(pb2); // 这是不安全的
// Clang-Tidy: Do not use static_cast to downcast from a base to a derived class; use dynamic_cast instead
// dynamic_cast的正确使用
if(Derived* pd3 = dynamic_cast<Derived*>(pb2))
{
// 转换成功,使用pd3指向的Derived对象
cout << "dynamic cast successful " << pd3->_derived << endl;
}
else
{
// 转换失败,pd3 == nullptr, 使用pb2指向的Base对象
cout << "dynamic cast failed " << pb2->_base << endl;
}
// 向下转型:引用
Base& rb = b;
Derived& rd = (Derived&)rb; // 这是不安全的
try
{
Derived &rd2 = dynamic_cast<Derived &>(rb); // 转换失败则抛出bad_cast异常
// 使用rd2所引用的Derived对象
cout << rd2._derived << endl;
}
catch(const bad_cast& bc)
{
cout << bc.what() << endl;
}
// 向下转型的错误示例,这些都不可以。
// Derived d2 = b; // error
// Derived d2 = reinterpret_cast<Derived>(b); // error
// Derived& pd = b; // error
}
引用类型转换失败时dynamic_cast抛bad_cast异常,而不是像指针类型转换失败时返回nullptr的原因是因为没有空引用...
示例代码二
void test_dynamic_cast2()
{
A1 a1;
A2 a2;
B b;
A1* p1 = &b;
A2* p2 = &b;
B* pb = &b;
cout << p1 << endl;
cout << p2 << endl; // 不同于p1 pb,切片现象
cout << pb << endl << endl;
B* pb1 = (B*)p1; // 不安全
B* pb2 = static_cast<B*>(p2); // 不安全,同于上方 Clang-Tidy: Do not use static_cast to downcast from a base to a derived class; use dynamic_cast instead
cout << pb1 << endl;
cout << pb2 << endl << endl;
B* pb3 = reinterpret_cast<B*>(p1);
B* pb4 = reinterpret_cast<B*>(p2); // 'reinterpret_cast' to class 'B *' from its base at non-zero offset 'A2 *' behaves differently from 'static_cast'
cout << pb3 << endl;
cout << pb4 << endl << endl;
B* pb5 = dynamic_cast<B*>(p1);
B* pb6 = dynamic_cast<B*>(p2);
cout << pb5 << endl;
cout << pb6 << endl << endl;
}
事实证明,reinterpret_cast就像它的名字一样:重新解释,通常为操作数的位模式提供较低层次的重新解释,而不会处理多继承的指针偏移问题。