C++数值转换详解:类型转换规则与安全指南

在上节课(浮点与整型提升)中,我们介绍了数值提升,即把某些较窄的数值类型转换为更宽、CPU 处理更高效的目标类型(通常为 intdouble)。

C++ 还定义了另一大类数值类型转换,称为数值转换(numeric conversions),涵盖基本类型之间的其他转换。

关键洞察
凡已纳入数值提升规则的转换(浮点与整型提升),均称为数值提升,而非数值转换

数值转换共有五种基本情形:

  1. 将某整型转换为另一整型(排除整型提升):
    short s = 3;        // int → short
    long  l = 3;        // int → long
    char  ch = s;       // short → char
    unsigned int u = 3; // int → unsigned int
    
  2. 将某浮点类型转换为另一浮点类型(排除浮点提升):
    float       f  = 3.0;   // double → float
    long double ld = 3.0;   // double → long double
    
  3. 将某浮点类型转换为任意整型:
    int i = 3.5;    // double → int
    
  4. 将某整型转换为任意浮点类型:
    double d = 3;   // int → double
    
  5. 将整型或浮点型转换为 bool
    bool b1 = 3;    // int → bool
    bool b2 = 3.0;  // double → bool
    

附注
由于花括号初始化对部分数值转换限制更严(下节课详述),本课示例采用拷贝初始化,以便简化说明。

安全与不安全转换
与始终保值、因而“安全”的数值提升不同,许多数值转换是不安全的。所谓不安全,是指源类型的至少一个值无法在目标类型中得到完全相等的表示。

数值转换按安全性可分为三类:

1. 保值转换(value-preserving conversions)

目标类型可精确表示源类型的所有可能值,故为安全转换。
例如,intlongshortdouble 均安全,因为任何源值都能被目标类型精确表示。

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)

不安全且可能丢失数据
例如 doubleint

int i = 3.0;  // OK,得到 3
int j = 3.5;  // 小数部分 0.5 丢失,得到 3

doublefloat 也可能丢精度:

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)); // 精度丢失

编译器通常会对隐式有损转换给出警告(甚至报错)。

注意:某些转换的安全性与平台相关。
例如,intdouble 在大多数平台是保值的,因为 int 通常 4 字节,double 8 字节,可表示全部 int 值;但在 intdouble 均为 8 字节的架构上,该转换可能变为有损!
示例:将 long long(至少 64 位)与 double 来回转换:

std::cout << static_cast<long long>(static_cast<double>(10000000000000001LL));

输出:

10000000000000000

最后一位已丢失!

应尽量避免不安全转换。若必须使用,通常满足以下条件之一:

  • 可约束待转换值,使之必能被目标类型精确表示。例如,保证 int 非负后再转为 unsigned int
  • 对数据丢失不敏感。例如,将 intbool 只关心零或非零。

数值转换补充说明

具体规则繁多而复杂,记住以下要点即可:

  • 若目标类型范围不支持源值,结果往往出乎意料。例如:

    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
    

尽管数值转换规则看似复杂,编译器通常会在你尝试危险转换时发出警告(符号/无符号转换除外)。

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"