10-3 数值转换

在上节课(10.2 — 浮点与整数提升)中,我们探讨了数值提升——即将特定窄数值类型转换为可高效处理的宽数值类型(通常为 int 或 double)。

C++ 还支持另一类数值类型转换,称为数值转换numeric conversions。这些转换涵盖基础类型之间的额外类型转换。

核心要点
凡属于数值提升规则(10.2 — 浮点与整数提升)范畴的类型转换,均称为数值提升而非数值转换。

  1. 数值转换包含五种基本类型:
short s = 3; // convert int to short
long l = 3; // convert int to long
char ch = s; // convert short to char
unsigned int u = 3; // convert int to unsigned int
  1. 浮点类型转换为其他浮点类型(不含浮点提升):
float f = 3.0; // convert double to float
long double ld = 3.0; // convert double to long double
  1. 浮点类型转换为整数类型:
int i = 3.5; // convert double to int
  1. 整数的类型转换为浮点类型:
double d = 3; // convert int to double
  1. 将整数类型或浮点类型转换为布尔类型:
bool b1 = 3; // convert int to bool
bool b2 = 3.0; // convert double to bool

补充说明…
由于大括号初始化严格禁止某些数值转换类型(下节详述),本节采用无此限制的复制初始化以保持示例简洁。


安全与不安全的转换

与数值提升(始终保持值不变且“安全”)不同,多数数值转换存在风险。不安全转换unsafe conversion指源类型中至少存在一个值无法转换为目标类型中等值的情况。

数值转换可归入三类安全级别:

  1. 值保留转换value-preserving conversions是安全的数值转换,目标类型能精确表示源类型所有可能值。

例如 int 转 long、short 转 double 均属安全转换,因源值总能转换为目标类型的等值。

int main()
{
    int n { 5 };
    long l = n; // okay, produces long value 5

    short s { 5 };
    double d = s; // okay, produces double value 5.0

    return 0;
}

编译器通常不会对隐式值保留转换发出警告。

经值保留转换的数值可始终回转为源类型,且结果值与原始值等价:

#include <iostream>

int main()
{
    int n = static_cast<int>(static_cast<long>(3)); // convert int 3 to long and back
    std::cout << n << '\n';                         // prints 3

    char c = static_cast<char>(static_cast<double>('c')); // convert 'c' to double and back
    std::cout << c << '\n';                               // prints 'c'

    return 0;
}
  1. 重新解释转换reinterpretive conversions 属于不安全的数值转换,转换后数值可能与源值不同,但不会丢失数据。有符号/无符号转换即属此类。

例如将有符号整型转换为无符号整型:

int main()
{
    int n1 { 5 };
    unsigned int u1 { n1 }; // okay: will be converted to unsigned int 5 (value preserved)

    int n2 { -5 };
    unsigned int u2 { n2 }; // bad: will result in large integer outside range of signed int

    return 0;
}

image

在 u1 案例中,有符号 int 值 5 被转换为无符号 int 值 5,因此值得以保留。

在 u2 案例中,有符号 int 值 -5 被转换为无符号 int。由于无符号 int 无法表示负数,结果将被模运算折叠为超出有符号 int 范围的大整数值,此时值未被保留。

此类值的变化通常是不希望发生的,往往会导致程序表现出意外或实现定义的行为。

相关内容
我们在第4.12节——类型转换与static_cast介绍中讨论了超出范围的值在有符号与无符号类型间如何转换。

警告
尽管重新解释转换存在安全隐患,但大多数编译器默认会禁用隐式有符号/无符号转换警告。

这是因为在现代C++的某些领域(例如操作标准库数组时),有符号/无符号转换往往难以避免。而实际上,此类转换绝大多数并不会导致数值变化。因此启用相关警告会导致大量针对实际无害的有符号/无符号转换的虚假警告(从而淹没真正有价值的警告)。

若选择禁用此类警告,请格外注意这些类型之间的意外转换(尤其是在向接收相反符号实参的函数传递形参时)。

通过重新解释转换转换的值可以转换回源类型,从而得到与原始值等效的值(即使初始转换产生的值超出了源类型的范围)。因此,重新解释转换在转换过程中不会丢失数据。

#include <iostream>

int main()
{
    int u = static_cast<int>(static_cast<unsigned int>(-5)); // convert '-5' to unsigned and back
    std::cout << u << '\n'; // prints -5

    return 0;
}

image

对于进阶读者
在 C++20 之前,将超出有符号值范围的无符号值进行转换,从技术上讲属于实现定义的行为(因为允许有符号整数使用与无符号整数不同的二进制表示形式)。实际上,在现代系统中这并非问题。

C++20 现要求有符号整数采用二进制补码表示。因此转换规则已调整,使得上述转换现被明确定义为重新解释转换(超出范围的转换将产生模回绕或模环绕modulo wrapping)。

请注意,尽管此类转换定义明确,但带符号算术溢出(即算术运算结果超出存储范围时发生的情况)仍属于未定义行为。

  1. 有损转换lossy conversions是指在转换过程中可能丢失数据的不安全数值转换。

例如,将双精度数转换为整数可能会导致数据丢失:

int i = 3.0; // okay: will be converted to int value 3 (value preserved)
int j = 3.5; // data lost: will be converted to int value 3 (fractional value 0.5 lost)

将double类型转换为float类型也可能导致数据丢失:

float f = 1.2;        // okay: will be converted to float value 1.2 (value preserved)
float g = 1.23456789; // data lost: will be converted to float 1.23457 (precision lost)

将丢失数据的值转换回源类型将导致该值与原始值不同:

#include <iostream>

int main()
{
    double d { static_cast<double>(static_cast<int>(3.5)) }; // convert double 3.5 to int and back
    std::cout << d << '\n'; // prints 3

    double d2 { static_cast<double>(static_cast<float>(1.23456789)) }; // convert double 1.23456789 to float and back
    std::cout << d2 << '\n'; // prints 1.23457

    return 0;
}

例如,当双精度数值 3.5 转换为整数值 3 时,小数部分 0.5 会丢失。当 3 重新转换为双精度数值时,结果为 3.0 而非 3.5。

编译器通常会在运行时执行隐式有损转换时发出警告(某些情况下会报错)。

警告

某些转换可能因平台不同而归入不同类别。

例如,整型数转换为双精度浮点数通常是安全的,因为整型数通常占用4字节,双精度浮点数通常占用8字节,在这种系统上所有可能的整型数值都能用双精度数表示。然而,某些架构中整型数和双精度浮点数都占用8字节。在这种架构上,整型数转换为双精度浮点数属于有损转换!

我们可以通过将一个长长(long long)值(必须至少为64位)转换为双精度浮点数再反转来证明这一点:

#include <iostream>

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

    return 0;
}

这段代码输出:

10000000000000000

注意我们的最后一位数字丢失了!

应尽可能避免不安全的转换。然而,这并非总是可行。当使用不安全的转换时,最常见的情况是:

  • 我们可以将待转换的值限制为仅能转换为相等值的情况。例如,当能保证整型数为非负数时,整型数即可安全地转换为无符号整型数。
  • 我们并不介意某些数据丢失,因为它们无关紧要。例如,将整型转换为布尔型会导致数据丢失,但通常我们对此并不介意,因为我们只是在检查整型值是否为0。

关于数值转换的更多说明

数值转换的具体规则复杂且繁多,以下是需要牢记的关键点:

  • 在所有情况下,将值转换为其范围无法支持该值的类型,都可能导致出人意料的结果。例如:
int main()
{
    int i{ 30000 };
    char c = i; // chars have range -128 to 127

    std::cout << static_cast<int>(c) << '\n';

    return 0;
}

在此示例中,我们将一个大整数赋值给类型为 char(取值范围为 -128 到 127)的变量。这导致 char 类型发生溢出,并产生意外结果:

image

image

  • 请注意,溢出对于无符号值是明确定义的,而对于有符号值则会产生未定义行为。
  • 从较大整型或浮点型转换至同类较小类型通常可行,前提是值能容纳于较小类型的范围之内。例如:
int i{ 2 };
short s = i; // convert from int to short
std::cout << s << '\n';

double d{ 0.1234 };
float f = d;
std::cout << f << '\n';

这产生了预期的结果:

image
image

  • 对于浮点数值,由于较小数据类型精度损失,可能会发生舍入误差。例如:
float f = 0.123456789; // double value 0.123456789 has 9 significant digits, but float can only support about 7
std::cout << std::setprecision(9) << f << '\n'; // std::setprecision defined in iomanip header

在此情况下,我们观察到精度损失,因为浮点数无法像双精度数那样保存高精度:

image
image

  • 将整数转换为浮点数通常可行,只要该值在浮点数类型的范围内。例如:
int i{ 10 };
float f = i;
std::cout << f << '\n';

这产生了预期的结果:

image
image

  • 将浮点数转换为整数时,只要数值在整数范围内即可转换,但任何小数部分都会丢失。例如:
int i = 3.5;
std::cout << i << '\n';

在此示例中,小数部分(.5)被舍弃,最终结果如下:

image

image

尽管数值转换规则看似复杂,但实际上编译器通常会在您尝试危险操作时发出警告(某些有符号/无符号转换除外)。

posted @ 2026-03-02 17:33  游翔  阅读(0)  评论(0)    收藏  举报