0. 类型转换
在C++中,如果两种类型的对象可以通过某种方式进行转换(conversion
),则称这两种对象是有关联的。如果程序需要一种类型的变量进行运算,那么此时就可以用与之相关联的其他类型进行替代。
在上述的转换中,一部分无需程序员主动操作,由编译器以自动的方式进行,称之为隐式转换(implcit conversion
);而剩下的,则需要程序员以某种方式主动将变量转换为另一种类型,则称之为显式转换,又叫做强制类型转换(cast
)。
1. 隐式转换
1.1 算术转换
一种典型的隐式转换是算术转换(arithmetic conversion),其含义是将一种算术类型转换为另一种算术类型。主要发生在两种不同大小(指类型大小)的整型进行算术运算,或者无符号与有符号数进行运算时。
- 整型提升
当两个整型进行运算时,较小的类型会被自动转化为较大的类型进行运算,这称之为整型提升(integral promotion)。
这种提升不光存在于整型中,以下类型在作为一个运算符的运算对象时,都会按序转换为较宽的类型。(bool
、char
、short
、int
、long
、long long
,其中还包括了一些宽字符类型,例如wchar_t
、char16_t
、char32_t
等)
int a = 5;
short b = 3;
long i = a + b;
在上面的例子里,a + b
这一表达式在运算时,b
的值自动被提升为int
与a
完成加法运算,又在赋值运算符的作用下,被提升为long
作为i
的初始值。
- 无符号类型转换
如果在进行整型提升后,两者因存在有无符号上的差异仍无法达成类型匹配,则将进行进一步转换。有无符号类型的互相转换有一个核心原则,即转换后的类型一定能完整存储转换前类型的所有可能值。
- 若无符号类型不小于带符号类型,则带符号转换为无符号的。
- 若带符号类型大于无符号类型,则根据机器的实际情况具体判断。若无符号所有可能值均可以转入有符号类型,则无转有;反之,则有转无。
注意这里的大小指的是类型大小,而非数值大小。例如
long
>short
。因为有些机器中,unsigned short
的所有可能值不一定能在int
中全部装下,此时就需要int
转化为unsigned short
。
1.2 其他隐式类型转换
- 数组名转换
数组名将被隐式转换为其元素类型的指针类型,除了下列情况:
- 数组名用作
decltype
参数 - 数组名用作
&
、sizeof
、typeid
等运算符的运算对象 - 数组名被用于与引用作绑定,例如用
auto &
去推断数组名类型时
- 指针转换
- 常量整数值0或者字面值nullptr能转换成任意指针类型
- 任意指向非常量的指针都能转换为
void *
- 指向任意对象的指针能转换成
const void*
第三种额外补充说明下,为便于理解可拆开看作(type* -> void* -> const void*
),任何非常量指针都可以转换为void*
,而非常量指针又都可以转换成常量指针类型,因此任何类型的对象指针都可以转换成const void*
。
int i = 10;
void *p_i = &i;
const void *cp_i = &i;
需要注意,void*
转换成原来的类型无法隐式转换,必须强转,而const void*
更麻烦,还需先借助const_cast
取消底层const,再强转为原来的类型。强转的内容后面介绍。
- 布尔类型转换
指针或算术类型的值如果为0,则转换为false
;若不为0,则转换为true
。
- 常量类型转换
指向非常量类型的指针或者引用,可以转换成常量类型的指针特性。反之则不成立,因为底层const特性无法轻易去除。
- 类类型转换
只允许一步自动的类类型转换,多步需要显式转换处理。涉及到类型转换运算符operator type()
的重载,在之后的内容中会讲。
2. 显式转换(强制类型转换)
强制类型转换的语法为cast-name<type>(expression)
,因为需要cast_name
标明执行转换的方式,因此也称为命名的强制类型转换。
2.1 static_cast
任何具有明确定义,且不包含底层const属性变化的类型转换,均可通过static_cast
完成。
不包含底层const好理解,那么什么是具有明确定义呢?主要有以下几种情况:
- 任何隐式转换可以用
static_cast
覆写 - 算术类型转换,可以将低精度转换为高精度类型
int a = 5, b = 10;
double c = static_cast<double> (a) / b; // 转换为浮点型运算
- 可以将
void*
转换为任何指针类型
int i = 10;
void *p_i = &i;
int *pp_i = static_cast<int*> (p_i); // 从void*恢复为原先类型
- 可以自动调用自定义类型的类型转换运算符
2.2 const_cast
能且只能修改对象的底层const属性。例如:将const int
转换为int
,或将double
转化为const double
。
需要注意的是,当底层const对象被去掉const性质(cast away the const)后,再对其进行写操作属于未定义行为。
另外,在之后的内容中可以看到,const_cast
在存在函数重载的上下文中经常使用。
int i = 10;
const int* cp_i = &i;
int *p_i = const_cast<int*> (cp_i); // 正确:成功取消了cp_i的底层const特性
int *p_i = const_cast<void*> (cp_i); // clang 16.0.0 error: const_cast from 'const int *' to 'void *' is not allowed
// visual studio: const_cast 只能调节类型限定符;不能更改基础类型
2.3 dynamic_cast
与其他几种不同的是,dynamic_cast
支持程序运行时的类型转换。但由于涉及到运行时类型识别(run-time type identification, RTTI),属于高级特性,留到后面讲。
2.4 reinterpret_cast
“为运算对象在位模式上提供重新解释”,从底层数据层面上进行类型转换。
简单来说,这是最强的一种强制类型转换,对于一个变量,你说是啥就是啥,只要被转换的对象不是自定义类型,那么编译器就不多管,否则会报not allowed
的错误。
这种转换下,如果出了问题,例如转换类型与实际类型不匹配,将由程序员自己承担后果。因此,这种转换完全不推荐使用。
int i = 10, *ip = &i;
char *pc = reinterpret_cast<char *>(ip); // 随意转换,错了不管,且无警告
string str = reinterpret_cast<string> (i);// error: reinterpret_cast from 'int' to 'string' (aka 'basic_string<char>') is not allowed
// 自定义类型不允许reinterpret_cast强转
2.5 旧式强制类型转换
除了命名的强制类型转换,还有一种老的强转写法,语法是type (expr)
或者(type) expr
。
这种转换在允许的情况下执行类似于static_cast
或者const_cast
的行为,当不前两者不满足时,则直接按照reinterpret_cast
的方式进行超级强转。这种旧式的写法,其语义和结果是不明确的,因此不推荐使用。
int i = 10;
double d = (double) i; // static_cast行为
int *p_i = &i;
const int* p_i = (const int*) p_i; // const_cast行为
char *a = (char*) i; // reinterpret_cast行为,运行时会带来严重后果
string str = (string) i; // no matching conversion for C-style cast from 'int' to 'string' (aka 'basic_string<char>')