目录
1. C的类型转换
1. 显式类型转换
2. 隐式类型转换
3. 总结
2. C++强制类型转换
2.1. static_cast
2.2. reinterpret_cast
2.3. const_cast
2.4. dynamic_cast
3. 为什么需要强制类型转换
4. RTTI
在C 语言中,如果 赋值运算符左右两侧类型不同,或者形参与实参类型不匹配,或者返回值类型与 接收返回值类型不一致时,就需要发生类型转化 , C 语言中总共有两种形式的类型转换: 隐式类型 转换和显式类型转换。
显式类型转换(Explicit Type Casting): 通过强制类型转换运算符来完成,将要转换的值放在括号内,并在括号前加上目标类型。
什么情况下支持显式类型转换呢? 意义不相近的类型,转换后仍旧有意义。
void Test1(void)
{
// 显式类型转换
int* ptr = nullptr;
//int i = ptr; // 默认情况下,指针不支持隐式类型转换为整形
int i = (int)ptr; // 但是可以支持强转
}
隐式类型转换(Implicit Type Casting): 也称为自动类型转换,由编译器自动完成。在某些情况下,程序会自动将一种类型的值转换为另一种类型的值,以便符合表达式的类型要求。
什么情况下支持隐式类型转换:意义相近的类型支持隐式类型转换
void Test2(void)
{
double d = 1.1;
int i = d; //意义相近的类型,支持隐式类型转换
}
1. 隐式类型转化有些情况下可能会出问题:比如数据精度丢失2. 显式类型转换将所有情况混合在一起,代码不够清晰,不太规范。因此在C++中,提出了自己的类型转换,但由于C++要兼容C,因此以前的转换方式C++依旧支持,兼容C隐式类型转换和强制类型转换,但是C++期望的是不要再用C的那一套类型转换,期望你用规范的C++显示强制类型转换
在C++标准中,为了规范类型转换,标准提出了四种命名的强制类型转换操作符
静态转换(static_cast):
static_cast<目标类型>(源对象)
静态转换可用于各种合理的类型转换,例如基本类型之间的转换、隐式转换、窄化转换等。它在编译时进行类型检查,不能用于执行不安全的类型转换,如将一个指针类型转换为非相关的指针类型
void Test3()
{
// 静态类型转换 static_cast
// 支持合理的转换
double d = 1.1;
int i = static_cast(d);
// 不支持合理的转换,例如将指针转换为整形
int* ptr = nullptr;
int i = static_cast(ptr); // 编译报错
}
重新解释转换(reinterpret_cast):
reinterpret_cast<目标类型>(源指针或引用)
重新解释转换是一种较低级别的类型转换,它可以将一个指针或引用转换为任意其他类型,并可以改变对象的解释方式。它提供了对类型的强制解释,但具有一定的危险性,因为对于不兼容的类型进行转换可能导致未定义行为。
void Test4(void)
{
// 重新解释类型转换 reinterpret_cast
// 可以将一个指针转化为一个整形
int* ptr = nullptr;
int i = reinterpret_cast(ptr);
std::cout << ptr << std::endl;
// 也可以将一个整形转化为指针
int j = 5;
int* ptr1 = reinterpret_cast(i);
}
常量转换(Const Cast):
const_cast<目标类型>(源指针或引用)
常量转换用于去除指针或引用的常量属性。它主要用于对常量对象进行修改,具有一定的风险,并且滥用可能导致未定义行为。
void Test5(void)
{
const int i = 5;
// int* ----- const int*
int*p = const_cast(&i);
*p = 10;
std::cout << i << std::endl;
std::cout << *p << std::endl;
}
首先,我们要知道,C++中const修饰的变量称之为常变量,是可以被修改的,只不过不支持直接修改,这是因为,C++的const修饰的变量并没有存在常量区,而是存于栈区的,例如我们上面的方式,就可以修改i的值。
既然你说它可以被修改,那么我们应该看到的结果是:两个10?
理想很丰满,现实很残酷。抱歉,这里不是两个10。但我就有疑问了,按道理说,p是i的地址,那么*p应该和i是相等的啊。实际上,它们的确是相等的。我们可以通过监视窗口查看一下。
相信很多人看到这个奇葩的结果,CPU都快干烧了。怎么回事,监视窗口和控制台的结果不一样。
要理解这个奇异的现象,我们要说编译器是对const修饰的变量有特殊处理,有的会把这个const修饰的变量存于寄存器中,因为编译器认为既然你声明为const属性,那么它会认为这个值不会被修改(即只有读),因此它不会将它存储于内存中,而是存于寄存器中,为了提高读取它的效率。而我们知道,std::cout<< 是一个重载的函数,当它去取i的值的时候,它并没有去内存去取,而是在寄存器中取i的值。因此,我们在控制台窗口看到的i值是5。而*p是会先在内存中找到p的地址,通过解引用,得到*p的值,也就是10。
而我们的监视窗口,它本质是另一个进程,它会实时去内存得到目标值。因此我们通过监视窗口可以看到,它们两个值都是10。
而在vs中的处理是怎样的呢?我们通过反汇编代码了解:
我们可以看到,当调用std::cout << i 的时候,此时需要将参数压栈,但是我们看到,它并没有从寄存器中取值,而是直接压的一个数字5。
那有没有什么方式,去获得i修改以后的值,让控制台的结果和监视窗口的结果一致。
在以前我们学习过一个关键字 volatile ,它告诉编译器,不要进行优化,你就从内存中给我去取值。
void Test5(void)
{
volatile const int i = 5;
int*p = const_cast(&i);
*p = 10;
std::cout << i << std::endl;
std::cout << *p << std::endl;
}
顺便,在这里提醒一下,上面都是C++规范的操作,有时候,它也可以这样:
void Test5(void)
{
volatile const int i = 5;
//int*p = const_cast(&i); // C++规范做法
int* p = (int*)&i; // C++兼容C的做法,当然结果一致
*p = 10;
std::cout << i << std::endl;
std::cout << *p << std::endl;
}
最后,总结一下const_cast,const_cast之所以单独一类,是因为它想告诉我们,它是一种很危险的操作,例如我们上面,将const int* ---> int*,会去除掉const属性,不太了解底层的人,很容易蒙圈。
动态转换(Dynamic Cast):
dynamic_cast<目标类型>(源指针或引用)
动态转换主要用于多态类型间的转换,即在继承关系中的基类和派生类之间的转换。它在运行时进行类型检查,能够判断对象是否是所请求的目标类型,如果是,返回转换后的指针或引用;如果不是,则返回空指针或引发std::bad_cast
异常。dynamic_cast是C++独有的类型转换。
dynamic_cast用于将一个父类对象的指针/引用转换为子类对象的指针或引用(动态转换)向上转型:子类对象指针/引用->父类指针/引用(不需要转换,赋值兼容规则也就是切片)向下转型:父类对象指针/引用->子类指针/引用(用dynamic_cast转型是安全的)注意:1. dynamic_cast只能用于父类含有虚函数的类2. dynamic_cast会先检查是否能转换成功,能成功则转换,不能则返回0
class base
{
public:
int _a = 5;
};
class derive : public base
{
public:
int _b = 10;
};
void Test6(void)
{
derive d_target;
// C++ 支持派生类向上"转型"为基类
base b_target = d_target; // 派生类对象切片为基类对象
// 如果这是类型转换,这里都编不过,因此这些都不是类型转换
base* p_target = &d_target; // 派生类指针切片为基类指针
base& r_target = d_target; // 派生类引用切片为基类引用
// 例如:
double d = 1.1;
//C++兼容C的不规范的转换
int& i = d; // 这里编译报错,为什么?
// C++规范的转换
int& i = static_cast(d); // 但同样会报错
// 因为这里是类型转换,而类型转换都会生成一个临时变量,临时变量是右值
// 因此我们这里需要加const,引用右值
const int& i = static_cast(d);
}
首先我们要知道一个问题。类型转换都会生成一个临时变量,派生类对象(指针,引用)可以赋值给基类对象(指针,引用),但它不是类型转换,而是一种特殊支持(切片行为),而父类对象是无论什么情况下都无法转换为子类对象的。哪怕你此时进行强转:
void Test6(void)
{
base b_target;
derive d_target;
derive new_target = reinterpret_cast(b_target); //编译报错
//derive new_target = (derive)(b_target);
}
因为派生类对象不仅有父类的成员,还有子类本身自己的成员。而父类对象只有父类自身的成员,如果编译器支持了强转为子类,那么此时这个父类对象不是可以访问子类成员了吗?这本质是一种非法访问。因此编译器不支持父类对象转化为子类对象。但是,编译器却支持父类的指针或者引用转化为子类的指针或者引用。因为父类的指针既有可能指向一个子类对象,也有可能指向一个父类对象。
void func(base* ptr)
{
// 这里是支持的,可以将父类指针强转为子类指针
derive* dp = reinterpret_cast(ptr);
std::cout << dp->_a << " : " << dp->_b << std::endl;
dp->_a = 5;
dp->_b = 10;
}
void Test7(void)
{
base b;
derive d;
func(&b);
func(&d);
}
调用Test7(),进程crash,为什么?原因是第一次调用时候,这个基类指针指向的对象是一个基类的对象,而你去修改了派生类成员_b,造成了越界,非法访问,导致进程崩溃。因此这种情况下进行强转(因为不管你这个ptr是指向父类对象还是子类对象,都会转换成功)是不安全的。可能会导致进程挂掉,因此我们提出了第二种解决方案,通过dynamic_cast进行动态转换。
可以看到,dynamic_cast必须要求基类是一个多态类型,即基类必须要有虚函数,子类可以不写,继承父类的虚函数。
class base
{
public:
virtual void func(void){}
public:
int _a = 5;
};
class derive : public base
{
public:
int _b = 10;
};
我们之所以说,用dynamic_cast将基类指针或者引用转换为派生类指针或者引用是安全的,是因为dynamic_cast转换后,如果这个父类的指针指向子类,那么可以转换成子类指针或者引用,转换表达式返回正确的地址。
如果这个父类的指针指向父类,那么不能转换成子类指针或者引用,转换表达式返回nullptr
也就是,我们可以通过返回值判定转换是否合理,如果合理,就转换成功。如果不合理,转换失败,返回空。
如果这个父类的引用指向父类对象,那么dynamic_cast会抛异常;如果这个父类的引用指向子类对象,那么可以转换成功。
void func(base* ptr)
{
derive* dp = dynamic_cast(ptr);
// 如果ptr指向子类对象,转换成功
if (dp != nullptr)
{
std::cout << dp->_a << " : " << dp->_b << std::endl;
dp->_a = 10;
dp->_b = 20;
}
// 如果ptr指向父类对象,转换失败,并返回空
else
{
std::cout << dp << std::endl;
}
}
void func(base& ptr)
{
// 如果ptr指向子类对象,转换成功
// 如果ptr指向父类对象,抛异常,具体为:std::bad_cast异常
derive dp = dynamic_cast(ptr);
std::cout << dp._a << " : " << dp._b << std::endl;
dp._a = 10;
dp._b = 20;
}
dynamic_cast其真正的作用是区分:基类的指针或者引用指向的对象到底是基类对象还是派生类对象。
扩展问题:
namespace tmp
{
class base1
{
public:
virtual void func(void){}
public:
int _a1 = 5;
};
class base2
{
public:
virtual void func(void){}
public:
int _a2 = 10;
};
class derive : public base1, public base2
{
public:
int _b = 20;
};
}
void Test8(void)
{
tmp::derive d_target;
tmp::base1* bp1 = &d_target;
tmp::base2* bp2 = &d_target;
std::cout << bp1 << std::endl;
std::cout << bp2 << std::endl;
std::cout << "------------------" << std::endl;
// C++兼容C语言的类型强转
tmp::derive* dp1 = (tmp::derive*)bp1;
tmp::derive* dp2 = (tmp::derive*)bp2;
std::cout << dp1 << std::endl;
std::cout << dp2 << std::endl;
std::cout << "------------------" << std::endl;
// C++规范的类型强转
tmp::derive* dp3 = reinterpret_cast(bp1);
tmp::derive* dp4 = reinterpret_cast(bp2);
std::cout << dp3 << std::endl;
std::cout << dp4 << std::endl;
std::cout << "------------------" << std::endl;
// C++的动态转换
// 要求基类必须是多态类型
tmp::derive* dp5 = dynamic_cast(bp1);
tmp::derive* dp6 = dynamic_cast(bp2);
std::cout << dp5 << std::endl;
std::cout << dp6 << std::endl;
std::cout << "------------------" << std::endl;
}
结果:
0113FB70
0113FB78
------------------
0113FB70
0113FB70
------------------
0113FB70
0113FB78
------------------
0113FB70
0113FB70
------------------
注意强制类型转换关闭或挂起了正常的类型检查,每次使用强制类型转换前,程序员应该仔细考虑是 否还有其他不同的方法达到同一目的,如果非强制类型转换不可,则应限制强制转换值的作用域,以减少发生错误的机会。强烈建议:避免使用强制类型转换。
类型转换是在编程过程中将一个变量或表达式的类型转换为另一种类型的操作。强制类型转换(也称为显式类型转换)是一种强制将一个类型转换为另一种类型的手段。以下是一些需要使用强制类型转换的常见情况:
1. 执行窄化转换:当我们需要将一个较大的数据类型转换为较小的数据类型时,强制类型转换可以帮助我们执行窄化转换。但需要注意的是,窄化转换可能会导致数据的精度丢失,因此在执行窄化转换时需要谨慎处理。
2. 处理兼容但不兼容的指针类型:在涉及指针类型的操作中,强制类型转换可以用于将一个指针类型转换为另一个指针类型,即使它们是兼容的但不兼容的类型。然而,在进行指针类型转换时,需要确保类型之间的关系和内存布局兼容,并且避免执行不安全的指针转换。
3. 推迟类型检查:有时,在某些特定场景下,我们可能需要在编译时推迟类型检查。通过强制类型转换,可以在编译时将一个表达式的类型转换为另一种类型,以适应特定的需求。但是,为了代码的清晰性和可维护性,应尽量避免过多地使用强制类型转换,特别是在没有必要的情况下。
需要注意的是,虽然强制类型转换提供了一种改变数据类型的手段,但滥用类型转换可能导致代码变得难以理解、维护和调试。因此,应根据需要谨慎使用强制类型转换,并确保清楚地理解转换的含义、可能导致的副作用和风险。在大多数情况下,应优先考虑使用更安全和合理的类型转换方式,如静态转换或动态转换。
.
RTTI (Run-Time Type Identification) 是 C++ 中提供的一种机制,即:运行时类型识别,用于在运行时确定对象的确切类型。它使我们能够在运行时检查对象的类型,并根据需要进行类型转换或执行其他与类型相关的操作。
C++通过以下方式来支持RTTI:1. typeid运算符可以获取一个对象类型的字符串2. dynamic_cast运算符可以判断父类的指针或者引用是指向父类对象还是子类对象,但要求父类必须是一个多态类型
补充:
我们以前学习的decltype,它可以获取一个对象的类型,并可以通过其结果重新定义一个新的对象,不过decltype类型推导是在编译时进行的,例如:
int i = 10;
//推导一个对象的类型,其结果可以定义一个新的对象
decltype(i) j = 0;