章节目录

数值转换

本节阅读量:

在上一课中,介绍了数值提升,这是将特定的较窄的数字类型转换为可以有效处理的较宽的数字类型(通常是int或double)。

C++支持另一类数值类型转换,称为数值转换。这些数值转换涵盖基本类型之间的其他类型转换。

有五种基本类型的数值转换。

1
2
3
4
short s = 3; // int to short
long l = 3; // int to long
char ch = s; // short to char
unsigned int u = 3; // int to unsigned int
1
2
float f = 3.0; // double to float
long double ld = 3.0; // double to long double
1
int i = 3.5; // double to int
1
double d = 3; // int to double
1
2
bool b1 = 3; // int to bool
bool b2 = 3.0; // double to bool

安全与潜在的不安全转换

与数值提升(始终保持值,因此“安全”)不同,某些数值转换在某些情况下不保持值。这种转换被称为“不安全”(尽管“潜在不安全”更准确,因为在其他情况下,这些转换是保值的)。

数值转换中的三个类别:

  1. 目标类型可以精确的保存当前类型的所有值,这是安全的数值转换

例如,int到long和short到double是安全的转换,因为源值始终可以转换为目标类型的等效值。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
int main()
{
    int n { 5 };
    long l = n; // okay, 产出 long 5

    short s { 5 };
    double d = s; // okay, 产出 double 5.0

    return 0;
}

编译器通常不会针对隐式的保留源值转换产生警告。

使用保值转换,转换的值始终可以转换回源类型,从而产生与原始值等效的值:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include <iostream>

int main()
{
    int n = static_cast<int>(static_cast<long>(3)); // 将 int 3 转到 long 再转回 int
    std::cout << n << '\n';                         // 打印 3

    char c = static_cast<char>(static_cast<double>('c')); // 将 char 'c' 转到 double 再转回 char
    std::cout << c << '\n';                               // 打印 'c'

    return 0;
}
  1. 数值重解释的转换,是潜在不安全的转换。因为转换后的值可能不在原来的类型的范围内。有符号与无符号之前的转换就属于这一种。

例如,将signed int转换为unsigned int时:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
int main()
{
    int n1 { 5 };
    unsigned int u1 { n1 }; // okay: 转换成 unsigned int 5 (值会保留)

    int n2 { -5 };
    unsigned int u2 { n2 }; // bad: 会生成超过 signed int范围的超大数字

    return 0;
}

在u1的情况下,有符号int值5被转换为无符号int值5。在这种情况下保留该值。

在u2的情况下,有符号int值-5被转换为无符号int。由于无符号int不能表示负数,因此结果将是一个不在有符号int范围内的大整数值。在这种情况下,不会保留该值。

这种值更改通常是不符合预期的,并且通常会导致程序表现出意外的或实现定义的行为。

使用重新解释转换转换的值可以转换回源类型,从而产生与原始值相等的值(即使初始转换产生的值超出源类型的范围)。

1
2
3
4
5
6
7
8
9
#include <iostream>

int main()
{
    int u = static_cast<int>(static_cast<unsigned int>(-5)); // 将 '-5' 转换成 unsigned 再转回来
    std::cout << u << '\n'; // prints -5
    
    return 0;
}
  1. 有损转换,是潜在不安全的转换。因为可能会丢失数据。

例如,double到int是可能导致数据丢失的转换:

1
2
int i = 3.0; // okay: 转换到 int 3 (值会保留)
int j = 3.5; // 数据丢失: 转换到 int 3 (分数部分 0.5 丢失)

从双精度到单精度的转换也可能导致数据丢失:

1
2
float f = 1.2;        // okay: 转换到 float 1.2 (值会保留)
float g = 1.23456789; // 数据丢失: 转换到 float 1.23457 (精度丢失)

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include <iostream>

int main()
{
    double d { static_cast<double>(static_cast<int>(3.5)) }; // 将 double 3.5 转 int 再转回
    std::cout << d << '\n'; // 打印 3

    double d2 { static_cast<double>(static_cast<float>(1.23456789)) }; // 将 double 1.23456789 转 float 再转回
    std::cout << d2 << '\n'; // 打印 1.23457

    return 0;
}

例如,如果将双精度值3.5转换为int值3,则分数分量0.5将丢失。当3转换回双精度时,结果是3.0,而不是3.5。

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


关于数值转换的更多信息

数值转换的特定规则复杂而众多,因此这里是需要记住的最重要的事情。

在所有情况下,将值转换为范围不支持该值的类型将导致可能意外的结果。例如:

1
2
3
4
5
6
7
8
9
int main()
{
    int i{ 30000 };
    char c = i; // char 的范围是 -128 to 127

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

    return 0;
}

在这个例子中,我们为类型为char的变量分配了一个大整数(char 范围是-128到127)。这会导致字符溢出,并产生意外的结果:

1
48

请记住,无符号值的溢出是有标准定义的,有符号值的溢出会产生未定义的行为。

只要值适合较小类型的范围,则从较大的整数或浮点类型转换为相同族中的较小类型通常是有效的。例如:

1
2
3
4
5
6
7
    int i{ 2 };
    short s = i; // 从 int 转 short
    std::cout << s << '\n';

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

这会产生预期的结果:

1
2
2
0.1234

在浮点值的情况下,由于较小类型的精度损失,可能会发生一些舍入。例如:

1
2
    float f = 0.123456789; // double 0.123456789 有 9 个有效数字, 单 float 值能支持 7 为
    std::cout << std::setprecision(9) << f << '\n'; // std::setprecision 定义在 iomanip 头文件

在这种情况下,我们会看到精度损失,因为单精度不能完全保存双精度的值:

1
0.123456791

只要值适合浮点类型的范围,从整数转换为浮点数通常可以工作。例如:

1
2
3
    int i{ 10 };
    float f = i;
    std::cout << f << '\n';

这会产生预期的结果:

1
10

只要值适合整数的范围,从浮点转换为整数可以工作,但任何小数都会丢失。例如:

1
2
    int i = 3.5;
    std::cout << i << '\n';

在此示例中,分数(.5)丢失,留下以下结果:

1
3

虽然数值转换规则可能看起来很可怕,但在现实中,如果您试图执行危险的操作(不包括一些有符号/无符号转换),编译器通常会警告您。


10.1 数值提升

上一节

10.3 窄化转换、列表初始化和constexpr初始化

下一节