在编写程序时,我们经常会遇到不同类型的数据混在一起运算或赋值的情况,比如拿 int 整数和 double 小数相乘,或者把两个整数相除算出的平均值存进 double 变量里。
在 C++ 中,把一种数据类型的值转换成另一种数据类型,就叫做 “类型转换”。C++ 的类型转换主要分为 2 种:① 隐式类型转换;② 显式类型转换。
C++ 隐式类型转换
在 C++ 中,隐式类型转换也叫做 “自动转换”。这种转换是由编译器在背后自动完成的,不需要我们手动写转换代码,日常主要发生在 “混合算术运算” 和 “赋值与初始化” 这两类场景中。
1. 混合运算中的隐式转换
当不同类型的数据放在同一个算术表达式中运算时,C++ 必须先把它们转换成同一种类型,然后才能继续计算。混合运算中的自动转换遵循一个很好记的原则:朝着表示范围更大、精度更高的方向转换,尽量保证数据不丢失。常见的规则有以下 2 条:
- 整型与浮点型混合运算:编译器会先将整型自动转换为浮点型,再按浮点数计算。比如
int和double一起运算时,会先把int转换为double,最终算出的结果也是double类型。 - 较小的整型参与运算:像
char、short这类比int范围更小的类型,在参与普通算术运算时会先自动提升为int类型,这叫做 “整数提升”。比如'A' + 1会先把'A'的 ASCII 码 65 转为int,再算出整数 66。
示例 1:“int * double” 的隐式转换
#include <iostream>
int main() {
int count = 5;
double price = 1.5;
double total = count * price;
std::cout << total << '\n';
return 0;
}运行结果如下。
7.5分析:
在这个例子中,count 是 int 类型,price 是 double 类型。执行 count * price 时,C++ 会先把 count 参与运算的值自动转换为 double 类型的 5.0,再与 1.5 相乘得到 7.5。
这里小伙伴们要留意一点:发生转换的只是 count 在当前算式里产生的一份临时值,变量 count 本身在内存中的类型依然是 int,并不会因为参与了一次乘法就变成 double。
2. 赋值与初始化中的隐式转换
在给变量赋值或初始化时,如果等号(=)左右两边的类型不一致,同样会触发隐式类型转换。赋值时的规则很直接:等号右边的值,会自动转换为等号左边变量的类型。
如果我们把整数 95 赋给 double 变量,它会自动转为 95.0,数值不会有任何损失。但如果反过来把 double 小数赋给 int 整型变量,由于整型无法保存小数部分,小数位就会直接丢失。下面我们来看一个例子。
示例 2:赋值运算的隐式转换
#include <iostream>
int main() {
double positive = 3.9;
double negative = -3.9;
int a = positive;
int b = negative;
std::cout << a << '\n';
std::cout << b << '\n';
return 0;
}运行结果如下。
3
-3分析:
从运行结果可以看出,把浮点数赋值给整型变量时,C++ 并不会进行四舍五入,而是直接抹去小数部分,干脆利落地保留整数部分,因此 3.9 变成了 3,-3.9 变成了 -3。
提示 把高精度的 double 赋给低精度的 int 时容易丢失小数,这种转换也叫 “窄化转换”。在现代 C++ 中,如果使用花括号初始化变量,比如写成 int num{3.9};,编译器就会直接报错来帮我们拦截这类精度丢失。
C++ 显式类型转换
隐式类型转换是由编译器自动进行的,而 “显式类型转换” 则需要我们手动编写代码,明确指定把一个值转换成哪种目标类型,所以也叫 “强制类型转换”。
为什么有了自动转换,还需要手动做强制转换呢?我们先来看一个日常开发中非常容易踩坑的例子:把两个 int 整型变量相除,然后将结果保存到 double 变量里。
示例 3:两个整型变量相除
#include <iostream>
int main() {
int total = 15;
int count = 2;
double average = total / count;
std::cout << average << '\n';
return 0;
}运行结果如下。
7分析:
明明 average 定义成了 double 类型,为什么输出的却是 7 而不是 7.5 呢?根据上一节学过的运算符优先级,除号(/)的优先级高于赋值号(=),程序会先算完等号右边的 total / count,最后才赋给 average。对于这个例子来说,上面代码等价于下面两步:
int temp = total / count; // 先按整数除法算出 7
double average = temp; // 再转为 double 类型的 7.0可以看到,赋值时的隐式转换发生在整数除法结束之后,左边的 double 没法把前面已经丢掉的 0.5 补回来。而参与除法的是两个变量,我们又不能直接在变量名后面写 .0,这时就需要用到显式类型转换了。
在现代 C++ 中,对基础数值做显式类型转换时,最常用的是 static_cast 运算符。
语法:
static_cast<目标类型>(值或表达式)说明:
尖括号 <> 里面填写想要转换成的目标类型,比如 double 或 int。后面的小括号 () 里面填写需要转换的变量、数值或表达式。
示例 4:使用 static_cast 保留小数
#include <iostream>
int main() {
int total = 15;
int count = 2;
double average = static_cast<double>(total) / count;
std::cout << average << '\n';
return 0;
}运行结果如下。
7.5分析:
static_cast<double>(total) 先把除号左边的 total 临时转换成了 double 类型的 15.0,右边的 count 随后自动转换为 2.0,整个算式按浮点数除法算出了准确结果 7.5。
平时写代码时大家要特别注意:想让两个 int 变量相除保留小数,只需对其中一个变量使用 static_cast<double> 即可,千万不要把整个除法算式包进小括号里:
// 正确方式
double res = static_cast<double>(total) / count;
// 错误方式
double res = static_cast<double>(total / count);如果写成上面的错误方式,程序会先计算小括号里面的整数除法 total / count 得到整数 7,外面的 static_cast<double> 只是事后把 7 变成 7.0,小数部分依然丢失了。
反过来,当业务上确实需要把浮点数显式截断为整数时,我们可以使用 static_cast<int>。此时同样要留意转换的位置,“先转换再相加” 与 “先相加再转换” 的结果往往是不一样的。请看下面例子。
示例 5:“先转后加” 与 “先加后转”
#include <iostream>
int main() {
double a = 3.6;
double b = 10.6;
int sum1 = static_cast<int>(a) + static_cast<int>(b);
int sum2 = static_cast<int>(a + b);
std::cout << sum1 << '\n';
std::cout << sum2 << '\n';
return 0;
}运行结果如下。
13
14分析:
为什么两个算式的结果相差了 1 呢?因为浮点数的小数部分相加后可能会产生进位。计算 sum1 时,程序先把 a 和 b 各自截断为整数 3 和 10,相加得到 13。而计算 sum2 时,程序先算小括号内的 3.6 + 10.6 得到 14.2,最后再截断小数就得到了 14。
在实际开发中,如果商品单价、身高等数据从一开始就需要带小数,直接定义为 double 变量最省心。只有当总分、人数等数据原本就是整数,仅在求平均值等算式中需要临时改变运算类型时,我们才使用 static_cast 做显式转换。
static_cast 与 C 语言风格强制转换
在阅读一些早期 C++ 代码或 C 语言代码时,我们可能会见到像 (double)total 或 (int)(a + b) 这样直接用小括号括住类型名的写法,这叫做 C 语言风格的强制类型转换。在现代 C++ 开发中,我们更推荐使用 static_cast,主要原因有两点:
意图清晰,方便查找:小括号 () 在代码里到处都在用,而 static_cast 一眼就能看出这里做了强制类型转换,在大型项目中搜索排查非常方便。
编译期检查更安全:C 语言风格的 (类型) 转换限制太宽松,容易把毫不相干的类型强行转换而埋下隐患。而 static_cast 会在编译阶段做安全检查,能够提前拦截不合理的类型转换。
