不同数据类型之间可以相互转换,有些是编译器自动完成的,有些需要程序员手动指定,理解转换规则能避免很多隐蔽的 bug。
2.1
不同数据类型之间可以相互转换,有些是编译器自动完成的(隐式转换),有些需要程序员手动指定(强制转换)。理解转换规则能避免很多隐蔽的 bug——很多"程序结果不对但又找不到错"的情况,根源都在这里。
可以把每种数据类型想象成一个不同大小的"箱子":char 是最小的箱子,只能装 1 字节的东西;int 是中等大小的箱子,能装 4 字节;double 是专门用来装小数的大箱子,能装 8 字节。
当程序需要把一个数据从一种类型的"箱子"挪到另一种类型的"箱子"里时,就发生了类型转换。挪的方向不同,结果也不同:
| 类型 | 大小 | 说明 |
|---|---|---|
| char | 1 字节 | 最小的整数类型,通常用来存字符 |
| short | 2 字节 | 较小范围的整数,竞赛中较少单独使用 |
| int | 4 字节 | 最常用的整数类型,范围约 ±21 亿 |
| long long | 8 字节 | 大范围整数,防止 int 溢出时使用 |
| float | 4 字节 | 单精度小数,精度较低 |
| double | 8 字节 | 双精度小数,竞赛中最常用的小数类型 |
int(中箱子)里的东西倒进 double(大箱子),空间绰绰有余,数据完整无损,这叫升级转换,编译器会自动帮你做。反过来,把 double(大箱子,比如 3.99)硬塞进 int(小箱子),装不下的小数部分会被直接丢弃,只剩下 3,这叫降级转换,往往需要你手动明确指定,因为这一步可能丢失信息。运算时,如果两个不同类型的数据凑在一起做加减乘除,精度低的类型会自动向精度高的类型转换,这个过程叫类型提升(type promotion),完全不需要你写任何代码,编译器在背后悄悄完成。提升的顺序如下:
来看一个最简单的例子:一个 int 和一个 double 相加,int 会先被临时提升为 double,再参与运算,结果自然也是 double:
| 1 | int a = 3; |
| 2 | double b = 1.5; |
| 3 | double c = a + b; // a 自动提升为 3.0,再与 1.5 相加,c = 4.5 ✓ |
但下面这个例子最容易让初学者踩坑——两个 int 相除,结果依然是 int,即使你把它赋值给一个 double 变量也没用,因为类型提升只发生在运算的那一刻,而不是赋值的时候:
| 1 | int x = 7, y = 2; |
| 2 | double z = x / y; // 期望 z = 3.5,实际 z = 3.0 ✗ |
为什么会这样?把这一行拆开来看,运算是分两步走的:
x / y 先以整数方式计算得到 3,然后才把 3 转成 3.0 赋给 z。要得到 3.5,必须在做除法之前就让其中一个数变成小数——这正是下一节"强制类型转换"要解决的问题。当自动转换不满足需求时,可以由程序员手动指定转换方式,明确告诉编译器"我要把这个值当成另一种类型来用"。C++ 里常见的写法有两种:
(类型) 表达式,例如 (double)x。这是从 C 语言继承下来的写法,简洁但不够安全(无法检查转换是否合理)。类型(表达式),例如 double(x)。写法更像"调用一个函数",是 C++ 里更推荐的写法,竞赛中最常见。回到刚才 7 / 2 的例子,只要在除法发生之前把其中一个数转换成 double,编译器就会用小数除法计算:
| 1 | int x = 7, y = 2; |
| 2 | |
| 3 | double r1 = (double)x / y; // C 风格:先把 x 转成 7.0,r1 = 3.5 ✓ |
| 4 | double r2 = double(x) / y; // 函数式:效果相同,r2 = 3.5 ✓ |
| 5 | double r3 = double(x / y); // ⚠️ 陷阱:括号只包住了结果,先整除得 3,再转成 3.0 |
x 单独转换后再参与除法,第 5 行却是把整个 x / y 的结果转换——这时候除法已经用整数方式算完了,转换来得太晚,救不回丢失的小数部分。写强制转换时,一定要确认括号包住的是"转换前的那个数",而不是"运算后的结果"。强制转换也常用于 char 和 int 之间的互转,这在处理字符时非常实用(下一节 ASCII 编码会详细展开):
| 1 | int code = int('A'); // code = 65 |
| 2 | char ch = char(code + 1); // ch = 'B' |
类型(表达式) 的 C++ 函数式转换简洁够用。在正式工程项目中,C++ 还提供了四种更安全、意图更明确的命名转换:static_cast、dynamic_cast、const_cast、reinterpret_cast,写法是 static_cast<类型>(表达式)。竞赛入门阶段先了解即可,不必强求现在就用。把本节的内容汇总成几条最容易踩坑的规则,写代码时可以对照检查:
int / int 永远是整数除法,哪怕结果赋值给 double 也无法挽回丢失的小数。必须在除法之前把其中一个操作数转换成浮点类型。(int)3.9 的结果是 3,不是 4!小数部分直接被砍掉。如果需要四舍五入,要用 <cmath> 里的 round() 函数(见 2.4 节),再转换成整数。int big = 300; char c = (char)big;,由于 char 最大只能表示到 127,c 不会等于 300,而是发生"溢出绕回",变成一个意想不到的负数。(double)x / y 而不是 (double)(x / y),一字之差,结果完全不同。