文章目录
- 一、算术转换
- 整形提升
- 算术转换
- 二、隐式类型转换
- 三、显式类型转换
- C语言旧式的类型转换
- 1. C语言隐式类型转换
- 2. C语言显式的类型强转
- C++引入的四种显式类型转换
- 1. static_cast
- 基本数据类型之间的转换
- 类指针之间的转换:
- 枚举类型之间的转换
- C 语言风格的强制类型转换和 `static_cast` 之间的差异
- 2. const_cast
- 3. reinterpret_cast
- 用reinterpret_cast强转测试平台大小端
- 4. dynamic_cast
- 应该避免使用强制类型转换
- 四、RTTI - 运行时类型识别
- `typeid` 运算符:
- `dynamic_cast`运算符
- `decltype`关键字
一、算术转换
算术转换(arithmetic conversion)的含义是把一种算术类型转换成另外一种算术类型。算术转换的规则定义了一套类型转换的层次,其中运算符的运算对象将转换成最宽的类型。例如,如果一个运算对象的类型是 long double
,那么不论另外一个运算对象的类型是什么都会转换成 long double
。 还有一种更普遍的情况,当表达式中既有浮点类型也有整数类型时,整数值将转换成相应的浮点类型。
可以参考这篇文章:【C语言】深入理解C语言中的整型提升和算术转换
整形提升
整型提升是将较小的整数类型转换为较大的整数类型的过程。通常发生在以下情况:
- 当操作数的类型比int类型更小(例如:char, short)时,会自动提升为int类型。
- 当操作数的类型是无符号的(例如:unsigned char, unsigned short),并且可以在不丢失任何信息的情况下转换为int类型,则提升为int类型;否则,提升为unsigned int类型。
整型提升的主要目的是确保数据在计算过程中能够正确处理,避免因较小类型溢出而导致的错误。
算术转换
二、隐式类型转换
在下面这些情况下,编译器会自动地转换运算对象的类型:
- 在大多数表达式中,比int类型小的整型值首先提升为较大的整数类型。
- 在条件中,非布尔值转换成布尔类型。
- 初始化过程中,初始值转换成变量的类型;在赋值语句中,右侧运算对象转换成左侧运算对象的类型。
- 如果算术运算或关系运算的运算对象有多种类型,需要转换成同一种类型。
- 函数调用时也会发生类型转换。
三、显式类型转换
C语言旧式的类型转换
对于C/C++,类型转换发生在下面三种情况下:
- 赋值运算符左右两侧类型不同
- 形参与实参类型不匹配
- 返回值类型与接收返回值类型不一致时
1. C语言隐式类型转换
#include <stdio.h>
int main()
{
int i = 1;
// 隐式类型转换
double d = i;
printf("%d, %.2f\n", i, d);
}
输出结果:
1, 1.00
2. C语言显式的类型强转
#include <stdio.h>
int main()
{
int* p = &i;
// 显式的强制类型转换
int address = (int)p;
printf("%p, %d\n", p, address);
return 0;
}
输出结果:
00000046857EF584, -2055277180
C语言类型强转的缺陷:
- 转换的可视性比较差
- 所有的转换形式都是以一种相同形式书写,难以跟踪错误的转换
因此C++提出了自己的类型转化风格,注意因为C++要兼容C语言,所以C++中还可以使用C语言的转化风格。
C++引入的四种显式类型转换
标准C++为了加强类型转换的可视性,引入了四种命名的强制类型转换操作符:
static_cast
、reinterpret_cast
、const_cast
、dynamic_cast
转换的形式:
castname<type>(expression);
其中:
- type是转换的目标类型而expression是要转换的值,如果type是引用类型,则结果是左值。
- castname是
static_cast
、reinterpret_cast
、const_cast
、dynamic_cast
四种。
1. static_cast
static_cast用于非多态类型的转换(静态转换),编译器隐式执行的任何类型转换都可用
static_cast,它适用于基本数据类型之间的转换或者具有继承关系的类之间的转换,但它不能用于两个不相关的类型进行转换。
基本数据类型之间的转换
#include <iostream>
int main()
{
double doubleValue = 3.14;
int intValue = static_cast<int>(doubleValue);
std::cout << "Double value: " << doubleValue << std::endl;
std::cout << "Converted to int: " << intValue << std::endl;
return 0;
}
在这个例子中,static_cast
被用于将double
类型转换为int
类型。这种转换会截断小数部分,只保留整数部分。
类指针之间的转换:
#include <iostream>
class Base {
public:
virtual void print() {
std::cout << "Base class" << std::endl;
}
};
class Derived : public Base {
public:
void print() override {
std::cout << "Derived class" << std::endl;
}
};
int main() {
Derived derivedObj;
Base* basePtr = &derivedObj;
// 使用 static_cast 进行基类指针到派生类指针的转换
Derived* derivedPtr = static_cast<Derived*>(basePtr);
derivedPtr->print(); // 输出 "Derived class"
return 0;
}
在这个例子中,static_cast
被用于将基类指针转换为派生类指针。这种转换只在确保基类指针指向的实际对象是派生类对象的情况下才是安全的。
枚举类型之间的转换
#include <iostream>
enum class Color { RED, GREEN, BLUE };
int main() {
int blueValue = static_cast<int>(Color::BLUE);
std::cout << "Integer value for BLUE: " << blueValue << std::endl;
return 0;
}
在这个例子中,static_cast
被用于将枚举类型Color
的成员转换为整数类型。这可以用于在需要整数值的情况下使用枚举。
C 语言风格的强制类型转换和 static_cast
之间的差异
int main()
{
int a = 123;
int* int_ptr = &a;
char* char_ptr1 = int_ptr; // 编译错误
char* char_ptr2 = (char*)int_ptr; // 没有编译错误,语法允许
char* char_ptr3 = static_cast<char*> (int_ptr); // 编译错误:类型转换无效
}
-
char* char_ptr1 = int_ptr;
使用的是 C 语言风格的强制类型转换,它将int*
类型的指针直接赋值给char*
类型的指针,这在语法上是允许的,但它存在潜在的问题。这样的强制类型转换可能会导致未定义行为,因为在将整数指针转换为字符指针时,可能会造成指针间隔不一致,访问越界等问题。 -
char* char_ptr2 = (char*)int_ptr;
中使用的是 C 语言风格的强制类型转换,这是合法的,但仍然存在潜在的问题。这种转换通常被视为一种“原始”转换,可能会导致指针的类型不匹配,从而引发运行时错误。 -
char* char_ptr3 = static_cast<char*>(int_ptr);
使用了static_cast
进行类型转换,这种转换是相对更为安全的,但在这个特定的情况下它会产生编译错误。static_cast
对于不同类型的指针之间的转换会进行更为严格的检查,确保类型之间有合理的关系,而在这个例子中,由于int*
和char*
之间的关系并非直接的继承关系,因此编译器会报错。
2. const_cast
const_cast最常用的用途就是删除变量的const属性,方便赋值。
- 如果指针或引用指向或绑定的对象本身不是一个常量,使用强制类型转换获得写权限是合法的行为。
- 然而如果该对象是一个常量,再使用 const_cast 执行写操作,虽然不会编译报错,但是会产生未定义的后果。
#include <iostream>
int main()
{
int nonConstValue = 42;
const int* constPtr = &nonConstValue;
// 使用 const_cast 删除 const 属性
int* nonConstPtr = const_cast<int*>(constPtr);
// 修改变量的值
*nonConstPtr = 55;
// 输出修改后的值
std::cout << "Modified value: " << nonConstValue << std::endl;
return 0;
}
在修改之前,你需要确保指针所指的对象或引用被绑定的对象本身是可修改的!
使用const_cast的场景一般不能使用static_cast:
int* nonConstPtr1 = const_cast<int*>(constPtr);
//int* nonConstPtr2 = static_cast<int*>(constPtr); //编译错误:static_cast 无法丢掉常量或其他类型限定符
在使用 const_cast
进行类型转换时,尖括号中的类型必须符合以下要求:
- 转换方向匹配: 转换的方向应当是合法的。如果原始对象是
const
类型,那么const_cast
的尖括号中的类型应当是非const
类型。如果原始对象不是const
类型,那么const_cast
的尖括号中的类型应当是const
类型。
例如,如果有一个const int*
类型的指针,你可以使用const_cast<int*>(constIntPtr)
来将其转换为int*
类型。
实际上不管怎么修改,都是符合语法的,不会报错:
int Value = 42;
// 四种指针
int* int_Ptr = &Value;
const int* const_int_Ptr = &Value;
int const* int_const_Ptr = &Value;
const int* const const_int_const_Ptr = &Value;
// 使用 const_cast 修改 const 属性
// 1. int* -> int*
int* Ptr1 = const_cast<int*>(int_Ptr);
// 2. int* -> const int*
const int* Ptr2 = const_cast<const int*>(int_Ptr);
// 3. const int* -> int*
int* Ptr3 = const_cast<int*>(const_int_Ptr);
// 4. const int* -> const int*
const int* Ptr4 = const_cast<const int*>(const_int_Ptr);
// 5. int const* -> int*
int* Ptr5 = const_cast<int*>(int_const_Ptr);
// 6. int const* -> const int*
const int* Ptr6 = const_cast<const int*>(int_const_Ptr);
// 7. const int* const -> int*
int* Ptr7 = const_cast<int*>(const_int_const_Ptr);
// 8. const int* const -> const int*
const int* Ptr8 = const_cast<const int*>(const_int_const_Ptr);
// 9. int* const -> int*
int* Ptr9 = const_cast<int*>(int_const_Ptr);
// 10. int* const -> const int*
const int* Ptr10 = const_cast<const int*>(int_const_Ptr);
// 11. const int* const -> int* const
int* const Ptr11 = const_cast<int* const>(const_int_const_Ptr);
// 12. const int* const -> const int* const
const int* const Ptr12 = const_cast<const int* const>(const_int_const_Ptr);
// 13. const int* const -> int const*
int const* Ptr13 = const_cast<int const*>(const_int_const_Ptr);
// 14. const int* const -> const int const*
const int const* Ptr14 = const_cast<const int const*>(const_int_const_Ptr);
// 15. int const* const -> int*
int* Ptr15 = const_cast<int*>(int_const_Ptr);
// 16. int const* const -> const int*
const int* Ptr16 = const_cast<const int*>(int_const_Ptr);
- 底层类型匹配: 尖括号中的类型必须是原始类型的底层类型。底层类型是指指针或引用所指向的实际类型。
例如,如果有一个const int*
类型的指针,使用const_cast<double*>(constIntPtr)
来将其转换为double*
类型是不合法的,因为int
和double
不是相同的底层类型:
const int value = 42;
const int* ptr = &value;
int* ptr2 = const_cast<double*>(ptr); // 编译错误
3. reinterpret_cast
reinterpret—cast 通常为运算对象的位模式提供较低层次上的重新解释。举个例子,假设有如下的转换:
int a = 123;
int* ip = &a;
char* pc1 = reinterpret_cast<char*>(ip);
char* pc2 = static_cast<char*>(ip); //这里使用static_cast会报错:类型转化无效
char* pc3 = const_cast<char*>(ip); //const_cast不能修改基础类型
我们必须牢记 pc 所指的真实对象是一个 int 而非字符,如果把 pc 当成普通的字符指针使用就可能在运行时发生错误。例如:
cout << pc << endl;
可能导致异常的运行时行为。
用reinterpret_cast强转测试平台大小端
大端存储模式:是指把数据的低位保存在内存的高地址中,把数据的高位,保存在内存的低地址中;
小端存储模式:是指把数据的低位保存在内存的低地址中,把数据的高位,保存在内存的高地址中。#include \<iostream> int main() { int intValue = 0x12345678; // 使用 reinterpret_cast 将 int* 转化为 char* char* charPtr = reinterpret_cast<char*>(&intValue); // 输出每个字节的十六进制表示 std::cout << "Memory content: "; for (int i = 0; i < sizeof(int); ++i) { std::cout << std::hex << static_cast\<int>(charPtr[i]) << " "; } std::cout << std::endl; // 判断平台是大端还是小端 if (charPtr[0] == 0x78) { std::cout << "该平台是小端" << std::endl; } else if (charPtr[sizeof(int) - 1] == 0x78) { std::cout << "该平台是大端" << std::endl; } else { std::cout << "大小端未知" << std::endl; } return 0; }
4. dynamic_cast
dynamic_cast用于将一个父类对象的指针/引用转换为子类对象的指针或引用(动态转换)。
使用形式如下所示:
dynamic_cast<type*>(e)
dynamic_cast<type&>(e)
dynamic_cast<type&&>(e)
其中:
- e必须是一个类类型,并且通常情况下该类型应该含有虚函数。
- 在第一种形式中,e 必须是一个有效的指针;
- 在第二种形式中,e 必须是一个左值;
- 在第三种形式中,e 不能是左值。
在上面的所有形式中, e 的类型必须符合以下三个条件中的任意一个:
- e 的类型是目标 type 的公有派生类
- e 的类型是目标 type 的公有基类
- e 的类型就是目标 type 的类型
如果符合,且基类指针/引用指向的实际对象是派生类对象时,类型转换可以成功。否则,转换失败:
- 如果一条 dynamic_cast语句的转换目标是指针类型并且失败了,则结果为 0 。
- 如果转换目标是引用类型并且失败了,则 dynamic_cast 运算符将抛出一个 bad_cast 异常。
验证一下:
#include <iostream>
#include <typeinfo>
class A
{
public:
virtual void f() {}
};
class B : public A
{
virtual void f() override {}
};
int main()
{
A a;
B b;
// 子类对象指针/引用 -> 父类指针/引用
// dynamic_cast<type*>(e)
A* ptrA = dynamic_cast<A*>(&b);
if (ptrA)
{
std::cout << "从B*到A*的动态转换成功。" << std::endl;
}
else
{
std::cout << "从B*到A*的动态转换失败。" << std::endl;
}
// dynamic_cast<type&>(e)
try
{
A& refA = dynamic_cast<A&>(b);
std::cout << "从B&到A&的动态转换成功。" << std::endl;
}
catch (const std::bad_cast& e)
{
std::cout << "从B&到A&的动态转换失败。" << std::endl;
}
// dynamic_cast<type&&>(e) - 注意:这种用法不常见,通常情况下我们不会将右值引用作为 dynamic_cast 的目标
try
{
A&& rrefA = dynamic_cast<A&&>(std::move(b));
std::cout << "从B&&到A&&的动态转换成功。" << std::endl;
}
catch (const std::bad_cast& e)
{
std::cout << "从B&&到A&&的动态转换失败。" << std::endl;
}
// 父类对象指针/引用 -> 子类指针/引用
// dynamic_cast<type*>(e)
B* ptrB = dynamic_cast<B*>(&a);
if (ptrB)
{
std::cout << "从A*到B*的动态转换成功。" << std::endl;
}
else
{
std::cout << "从A*到B*的动态转换失败。" << std::endl;
}
// dynamic_cast<type&>(e)
try
{
B& refB = dynamic_cast<B&>(a);
std::cout << "从A&到B&的动态转换成功。" << std::endl;
}
catch (const std::bad_cast& e)
{
std::cout << "从A&到B&的动态转换失败。" << std::endl;
}
// dynamic_cast<type&&>(e)
try
{
B&& rrefB = dynamic_cast<B&&>(std::move(a));
std::cout << "从A&&到B&&的动态转换成功。" << std::endl;
}
catch (const std::bad_cast& e)
{
std::cout << "从A&&到B&&的动态转换失败。" << std::endl;
}
return 0;
}
后面三个失败的原因是括号中的指针或引用的实际指向的对象不是派生类对象,语法只允许一个动态类型是派生类指针的对象的静态类型从基类指针转化为派生类指针!
注意:
- dynamic_cast只能用于父类含有虚函数的类
- dynamic_cast会先检查是否能转换成功,能成功则转换,不能则返回0
向上转型:子类对象指针/引用 -> 父类指针/引用(不需要转换,因为有“赋值兼容规则”)
向下转型:父类对象指针/引用 -> 子类指针/引用(直接赋值不可以,但是用dynamic_cast转是安全的,因为有异常会抛异常或者返回空指针)
应该避免使用强制类型转换
强制类型转换关闭或挂起了正常的类型检查,每次使用强制类型转换前,程序员应该仔细考虑是
否还有其他不同的方法达到同一目的,如果非强制类型转换不可,则应限制强制转换值的作用
域,以减少发生错误的机会。
强烈建议:避免使用强制类型转换
四、RTTI - 运行时类型识别
RTTI:Run-time Type identification 的简称,即:运行时类型识别。
C++通过以下方式来支持RTTI:
- typeid运算符
- dynamic_cast运算符
- decltype
typeid
运算符:
typeid
运算符是C++中的运算符,用于获取表达式的类型信息。它返回一个std::type_info
对象,该对象包含有关表达式类型的信息。主要用于在运行时获取对象的实际类型,特别是在处理多态类型时。
type_info 类必须定义在 <typeinfo> 头文件中,并且至少提供下表所列的操作:
type_info的操作 | 描述 |
---|---|
t1 == t2 | 如果 type_info 对象 t1 和 t2 表示同一种类型,返回 true,否则返回false |
t1 != t2 | 如果 type_info 对象 t1 和 t2 表示不同的类型,返回 true,否则返回false |
t.name() | 返回一个 C 风格字符串,表示类型名字的可打印形式。 |
t1.before(t2) | 返回一个 bool 值,表示t1是否位于t2之前。 |
除此之外,因为 type_info 类一般是作为一个基类出现,所以它还应该提供一个公有的虚析构函数。当编译器希望提供额外的类型信息时,通常在 type_info 的派生类中完成。type_info 类没有默认构造函数,而且它的拷贝和移动构造函数以及赋值运算符都被定义成删除的。因此,我们无法定义或拷贝 type_info类型的对象,也不能为 type_info 类型的对象赋值。创建 type_info 对象的唯一途径是使用 typeid 运算符。
示例代码:
#include <iostream>
#include <typeinfo>
class Base
{
public:
virtual ~Base() {}
};
class Derived : public Base {};
int main()
{
Base* basePtr = new Derived();
// 使用 typeid 获取对象的类型信息
const std::type_info& typeInfo = typeid(*basePtr);
// 比较类型信息
if (typeInfo == typeid(Derived))
{
std::cout << "basePtr 指向 Derived 类型的对象。" << std::endl;
} else if (typeInfo == typeid(Base))
{
std::cout << "basePtr 指向 Base 类型的对象。" << std::endl;
}
delete basePtr;
return 0;
}
由运行结果可见,typeid
运算符获取到的类型名是运行时的动态类型:
dynamic_cast
运算符
上文已经介绍
decltype
关键字
decltype
是C++11引入的关键字,用于获取表达式的类型而不进行实际的计算。它在编译时返回表达式的类型,并常用于模板元编程和泛型编程中,以提取变量或表达式的类型。
示例代码:
// decltype的一些使用使用场景
template<class T1, class T2>
void F(T1 t1, T2 t2)
{
decltype(t1 * t2) ret;
cout << typeid(ret).name() << endl;
}
int main()
{
const int x = 1;
double y = 2.2;
decltype(x * y) ret; // ret的类型是double
decltype(&x) p; // p的类型是int*
cout << typeid(ret).name() << endl;
cout << typeid(p).name() << endl;
F(1, 'a');
return 0;
}