在上节课(浮点与整型提升)中,我们介绍了数值提升,即把某些较窄的数值类型转换为更宽、CPU 处理更高效的目标类型(通常为 int
或 double
)。
C++ 还定义了另一大类数值类型转换,称为数值转换(numeric conversions),涵盖基本类型之间的其他转换。
关键洞察
凡已纳入数值提升规则的转换(浮点与整型提升),均称为数值提升,而非数值转换。
数值转换共有五种基本情形:
- 将某整型转换为另一整型(排除整型提升):
short s = 3; // int → short long l = 3; // int → long char ch = s; // short → char unsigned int u = 3; // int → unsigned int
- 将某浮点类型转换为另一浮点类型(排除浮点提升):
float f = 3.0; // double → float long double ld = 3.0; // double → long double
- 将某浮点类型转换为任意整型:
int i = 3.5; // double → int
- 将某整型转换为任意浮点类型:
double d = 3; // int → double
- 将整型或浮点型转换为
bool
:bool b1 = 3; // int → bool bool b2 = 3.0; // double → bool
附注
由于花括号初始化对部分数值转换限制更严(下节课详述),本课示例采用拷贝初始化,以便简化说明。
安全与不安全转换
与始终保值、因而“安全”的数值提升不同,许多数值转换是不安全的。所谓不安全,是指源类型的至少一个值无法在目标类型中得到完全相等的表示。
数值转换按安全性可分为三类:
1. 保值转换(value-preserving conversions)
目标类型可精确表示源类型的所有可能值,故为安全转换。
例如,int
→long
、short
→double
均安全,因为任何源值都能被目标类型精确表示。
int main()
{
int n{5};
long l = n; // OK,得到 long 5
short s{5};
double d = s; // OK,得到 double 5.0
}
编译器通常不会为隐式保值转换发出警告。
经保值转换后再转回源类型,总能恢复原值:
int n = static_cast<int>(static_cast<long>(3)); // 3 → long → int,仍为 3
char c = static_cast<char>(static_cast<double>('c')); // 'c' → double → char,仍为 'c'
2. 重解释转换(reinterpretive conversions)
不安全,但不丢失数据。转换后的值可能与源值不同,但所有位信息均被保留。
典型例子是带符号与无符号间的转换:
int main()
{
int n1{5};
unsigned u1{n1}; // OK,得到无符号 5,值不变
int n2{-5};
unsigned u2{n2}; // 有问题:-5 会被模 2^n 包装成巨大正数
}
此类值变通常不受欢迎,易导致意外或实现定义行为。
相关内容:符号/无符号范围外值的转换规则见 4.12 课 —— 类型转换与 static_cast 简介。
警告:尽管重解释转换不安全,大多数编译器默认关闭符号/无符号转换警告,因为现代 C++ 中此类转换有时难以避免,且多数情况下并未真正改变值。若保持禁用,请格外注意无意中向相反符号类型的函数参数传递值。
经重解释转换后再转回原类型,可恢复原值(即使中间结果曾超出原类型范围):
int u = static_cast<int>(static_cast<unsigned int>(-5)); // -5 → unsigned → int,仍为 -5
3. 有损转换(lossy conversions)
不安全且可能丢失数据。
例如 double
→int
:
int i = 3.0; // OK,得到 3
int j = 3.5; // 小数部分 0.5 丢失,得到 3
double
→float
也可能丢精度:
float f = 1.2; // OK,可精确表示
float g = 1.23456789; // 精度不足,变为 1.23457
若再转回源类型,所得值与原值不同:
double d = static_cast<double>(static_cast<int>(3.5)); // 3.5 → 3 → 3.0
double d2 = static_cast<double>(static_cast<float>(1.23456789)); // 精度丢失
编译器通常会对隐式有损转换给出警告(甚至报错)。
注意:某些转换的安全性与平台相关。
例如,int
→double
在大多数平台是保值的,因为 int
通常 4 字节,double
8 字节,可表示全部 int
值;但在 int
与 double
均为 8 字节的架构上,该转换可能变为有损!
示例:将 long long
(至少 64 位)与 double
来回转换:
std::cout << static_cast<long long>(static_cast<double>(10000000000000001LL));
输出:
10000000000000000
最后一位已丢失!
应尽量避免不安全转换。若必须使用,通常满足以下条件之一:
- 可约束待转换值,使之必能被目标类型精确表示。例如,保证
int
非负后再转为unsigned int
; - 对数据丢失不敏感。例如,将
int
转bool
只关心零或非零。
数值转换补充说明
具体规则繁多而复杂,记住以下要点即可:
若目标类型范围不支持源值,结果往往出乎意料。例如:
int i{ 30000 }; char c = i; // char 范围 -128~127,导致溢出 std::cout << static_cast<int>(c); // 可能输出 48
无符号溢出定义良好;有符号溢出则为未定义行为。
同族大类型向小类型转换,只要值在小类型范围内即可:
int i{ 2 }; short s = i; // OK double d{ 0.1234 }; float f = d; // OK
浮点情形可能因精度降低而舍入:
float f = 0.123456789; std::cout << std::setprecision(9) << f; // 输出 0.123456791
整型转浮点,只要值在浮点范围内即可:
int i{ 10 }; float f = i; // 10
浮点转整型,值需在整型范围内,小数部分被截断:
int i = 3.5; // 3
尽管数值转换规则看似复杂,编译器通常会在你尝试危险转换时发出警告(符号/无符号转换除外)。