1.6 类型转换
简介
在C++语言中,如果两种类型有关联,那么当程序需要其中一种类型的运算对象时,可以用另一种关联类型的对象或值代替。
// C++不会直接将两个不同类型的值相加, 会先将3的int类型隐式转换为double类型, 再执行两个double类型相加
// 初始化i的过程中由于i的类型无法改变, 因此加法运算得到的double类型的结果会被转换为int类型的值用于初始化i
int i = 3.14 + 3;
隐式转换
1. 隐式转换发生的场景
在下面这些场景中,编译器会自动地转换运算对象的类型:
- 比int类型小的整型值首先提升为比较大的整数类型
- 在条件中,非布尔值转换为布尔类型(指针或者算术类型的值为0,则自动转换为false)
- 初始化过程中,初始值转换成变量的类型;在赋值语句中,右侧运算对象转换成左侧运算对象的类型
- 如果算数运算或关系运算的运算对象有多种类型,需要转换成同一种类型
- 函数调用时也会发生类型转换
- 大多数用到数组的表达式会将数组自动转换为指向数组首元素的指针(数组作为decltype关键字参数、取地址符&、sizeof和typeid等运算符的运算对象时,上述自动转换不会发生)
- 常量整数值0或者字面值
nullptr
能转换成任意指针类型,指向任意非常量的指针能转换为void*
,指向任意对象的指针能转换为const void*
- 转换成常量:允许指向非常量类型的指针/引用转换为指向相应的常量类型的指针/引用
2. 算数转换
算数转换的含义是指把一种算数类型转换为另一种算数类型,有如下一些规则:
- 运算符的运算对象将转换成最宽的类型:例如一个对象是long double类型,那么无论另一个对象是什么类型都会转化为long double类型;表达式中既有浮点类型又有整数类型时,整数值将转换为对应的浮点类型
- 如果运算对象是无符号对象和有符号对象,且无符号类型不小于带符号类型,那么带符号的类型会转化为无符号类型:例如两个类型分别是unsigned int和int,那么int类型的运算对象会转换为unsigned int,此时如果int运算对象为负值,那么存在后面提到的副作用
无符号数与有符号数
1. 无符号数超过表示范围
当我们赋给无符号类型一个超过它表示范围的值时,结果是初始值对无符号类型表示数值总数取模后的余数。比如8比特大小的unsigned char
可以表示0~255,如果我们将-1赋给它将会得到255。
2. 有符号数超过表示范围
当我们赋给带符号类型一个超出它表示范围的值时,结果是未定义的,程序可能会继续工作、可能崩溃,也可能产生垃圾数据。
3. 不要混用有符号数和无符号数
当一个算数表达式中既有无符号类型又有带符号类型时,带符号数会自动转化为无符号数,结果可能是出乎意料的:
// 切勿混用带符号类型和无符号类型
unsigned u = 10;
int i = -42;
std::cout << i + i << std::endl; // -84
std::cout << i + u << std::endl; // 如果int占32位则输出4294967264
尤其在我们通过标准库类型的size()
成员函数返回一个无符号类型时:
// 注意下面这段程序几乎每次都会非预期地输出error, 尽管s.size()返回一个正数, 不可能小于-1
// 但是混用带符号数和无符号数会将n转换成一个比较大的无符号数
#include <iostream>
int main() {
std::string s = "tomocat";
int n = -1;
if (s.size() < n) {
std::cout << "error" << std::endl;
}
}
类的转换
类类型可以定义由编译器自动执行的转换,不过编译器每次只能执行一种类类型的转换。例如:
string s = "tomocat"; // 字符串字面量转换成string类型
while (cin >> s); // IO库定义了从istream到布尔值转换的规则, 如果读入成功则为true
显式转换
1. static_cast
任何具有明确定义的类型转换,只要不包含底层const就可以使用static_cast
。例如将int运算对象强制转换为double类型就可以使表达式执行浮点数除法:
int i, j;
double slope = static_cast<double>(j) / i;
static_cast
也常用于编译器无法自动执行的类型转换,例如我们可以使用static_cast
找回存在于void*
的指针:
double d;
void *p = &d;
double *dp = static_cast<double*>(p);
2. const_cast
const_cast
只能用于改变运算对象的底层const,用于将常量对象改成非常量对象。一旦我们去掉了某个对象的const性质,编译器就不会再阻止我们对该对象进行写操作了。如果对象本身不是一个常量,使用强制类型转换获得写权限是合法的行为,如果对象是一个常量,再使用const_cast
执行写操作就会产生未定义的后果。
只有const_cast
能改变表达式的常量属性,使用其他形式的强制类型转换改变表达式的常量属性都将引发编译错误。同样地,也不能用const_cast
改变表达式的类型:
const char *cp;
char *q = static_cast<char*>(cp); // 错误: static_cast不能去掉const性质
static_cast<string>(cp); // 正确: 字符串字面量转换为string类型
const_cast<string>(cp); // 错误: const_cast只能改变常量属性
Tips:
const_cast
最常用于重载函数的情景。
// 常量引用的函数版本
const string &shorterString(const string &s1, const string &s2) {
return s1.size() <= s2.size() ? s1 : s2;
}
// 非常量引用的函数版本复用常量引用的函数版本
string &shorterString(string &s1, string &s2) {
const string &r = shorterString(const_cast<const string&>(s1),
const_cast<const string&>(s2));
return const_cast<string &>(r);
}
3. reinterpret_cast
reinterpret_cast
通常为运算对象的位模式提供较低层次上的重新解释,例如:
int *ip;
char *cp = reinterpret_cast<char*>(ip);
我们必须牢记pc所指的真实对象是一个int而非char,如果把pc当成普通字符指针使用就可能在运行时发生错误。
4. dynamic_cast
dynamic_cast
主要用来执行“安全向下转型”(safe downcasting),也就是用来决定某对象是否归属继承体系中的某个类型。它是唯一无法由旧式语法执行的动作,也是唯一可能耗费重大运行成本的转型动作。
5. 旧式的强制类型转换
在早期版本的C++语言中,显式地进行强制类型转换包含两种形式:
type(expr); // 函数形式的强制类型转换
(type)expr; // C语言风格的强制类型转换
根据所涉及的类型不同,旧式的强制类型转换分别具有与const_cast
、static_cast
和reinterpret_cast
相似的行为,例如转换后不合法,则旧式强制类型转换与reinterpret_cast
具有相似的功能:
int *ip;
char *cp = (char*)ip; // 等价于使用reinterpret_cast