第四节 · 转换状态 · 类型转换

篮子与瓶子最大的区别在于,一个用于装固体,一个用于装液体。

不过,篮子里其实也可以装液体——只不过最终液体会漏光;瓶子里也可以装固体——也许这固体得切成丁或是压扁成条才可能放得进去,也许根本就没办法放进去。

再看 C++ 中的数据类型,它们也各自有着各自的烦恼——整数虽然能变为浮点数,但浮点数变整数后会丢失小数部分;整数能转换为字符,但浮点数要转换为字符实在是不太方便,等等。

现在,我们就来看看它们各自的烦恼吧。

最常见的转换操作莫过于算术上的类型转换了。我们时常会碰到各种不同数字间的类型转换,例如:

  • 某些情况下,我们需要抛弃浮点数的小数部分,例如 3.14 变为整数 3。

  • 为了截取低位的整数,原本占了 8 个字节的 long long 类型的数字需要压成仅占 1 字节的 char 类型。

  • 在整数与浮点数进行计算时,如果不先把整数转换为浮点数,计算后的结果便会与你所想象的有很大出入。

  • 当我们发觉原本的浮点数类型精度不够用时,需要将其转换为精度更高的浮点数类型。

为了能让接下来对类型转换的研究顺利进行,我们需要先回顾一下之前所学的一些基本数据类型:

  • 整数类型,依据占用的空间从小到大排序,分别有boolcharshortintlonglong long。其中bool同样也专用于表达布尔值,char同样也是字符类型。

  • 浮点数类型,也依据占用的空间从小到大排序,分别由floatdoublelong double

  • 给类型前加unsigned前缀,意味着该类型将不能表达负数,并且正数的表达范围会比原本类型大一倍。

  • 布尔值、浮点数不能使用unsigned前缀,只能由整数使用。

C++ 可以在暗中帮助我们完成大部分的转换——将在运算符右侧的类型转换为与左侧相同的类型,这种转换叫作隐式转换

隐式转换中算术类型的转换遵循这样一些规则,很好理解:

  • 如果两侧均为整数类型,且左侧的类型比右侧的大,那么右侧的类型会被提升为左侧的类型,其中原本的数字不受影响。例如:

    unsigned int un_i = 3;
    int i = 2;
    un_i + i == 5;        // 右侧的 int 类型被提升为 unsigned int 类型
    long l = 5;
    l + un_i == 8;        // 右侧的 unsigned int 类型被提升为 long 类型
  • 如果两侧均为整数,且均比int小,则会先全部转换为int,再作具体的计算。这个规则不干扰上一条规则——如果欲进行的操作是赋值,那么计算完成后得到的int类型结果会被裁剪为与左侧相同的类型。例如:

    unsigned short a = 1;
    unsigned char b = 2;
    auto c = a + b;    // 两个均比 int 小的整数类型的运算结果类型是 int 类型,故 auto 推断为 int 类型
    char d = a + b;    // 虽然运算结果是 int 类型,但在计算完成后就根据左侧类型自动裁剪为 char 类型了
  • 如果左侧为浮点数类型,右侧的类型会自动转换为与左侧相同的浮点数类型。例如:

    1.5 + 2;    // 右侧的 int 类型自动转换为 double 类型
    1.5L + 2;    // 右侧的 int 类型自动转换为 long double 类型
  • 如果左侧为浮点数类型,且右侧为一个比int小的整数类型,右侧也将先提升为int类型再进行转换。例如:

    short s = 1;
    1.5 + s;    // 先将 s 提升为 int 类型,再转换为 float 类型
  • 如果左侧为布尔值类型,右侧为整数,则视右侧的值决定——右侧值为 0 时转换为 false,右侧值不为 0 时转换为 true。如果右侧为浮点数,则会先转换为int类型再进行判断。例如:

    bool a = 1;        // true
    bool b = 2;        // false
    bool c = 23333;    // true

能做到隐式转换的不仅是数字、字符,还有数组、指针:

  • 将数组转换为指针后,该指针指向的是数组的第一个元素。这种隐式转换本质上是数组到指针的退化(decay)。例如:

    int arr[10];
    int *p = arr;    // p 指向 arr[0]
  • 空指针(nullptr)能转换为任何一种指针类型。这种特性可用于初始化一个指针变量,也可用于标记某指针所指向的内容已经失效。例如:

    int *i = nullptr;    // 将 i 初始化为一个空指针
    int *j = &i;        // 将 j 初始化为指向 i 的指针
    j = nullptr;        // 再清空 j,使其成为空指针
  • 任意非常量指针均能转换为void *指针,任意常量指针均能转换为const void *指针。例如:

    int n = 3;
    int *i = &n;    // 将 i 初始化为一个指向 n 的指针
    void *j = i;    // i 可以无阻直接转换为 void * 指针
  • 指向同一类型非常量的指针可转换为常量指针。例如:

    int n = 3;
    int *i = &n;        // i 指向 n,且如果通过 i 访问 n,n 可更改
    const int *j = i;    // j 指向 n,且如果通过 j 访问 n,n 不可更改

我们将在后续章节中,继续学习有关类类型的转换——为我们自己创造的类型定制转换规则。

有些时候,我们不得不以某种方式强行改变对象的类型——当隐式转换不够用时,显式转换便派上用场了。

在正式介绍专用于显式转换类型的运算符之前,有必要严肃强调,强行转换对象很可能会导致不可预料的后果,在这么做之前一定要想清楚你正在做什么!

  • static_cast

这应该是最常见也最常用的转换运算符了。它可以接受除了常量向非常量转换以外的任意类型转换。

它的格式是这样的:

static_cast< 类型 >( 对象 )

尖括号内填写想要转换到的类型,小括号内则是被转换的对象。另外,你可以填写一个引用类型,转换后的结果将会是一个左值。

使用它转换类型时,如果是将一个占用空间较大的整数向占用空间较小的整数类型转换(例如intshort),或是将精度较高的浮点数向精度较低的浮点数类型转换,会造成一定的数据丢失。

通常来讲,机器会丢弃高位部分的数据,例如:

int i = 65793;
auto u_s = static_cast<unsigned short>(i);    // i 被裁剪为 257
auto u_c = static_cast<unsigned char>(i);    // i 被裁剪为 1
  • const_cast

先前我们了解过,static_cast不能接受从常量到非常量的转换。要想突破这一层限制,我们可以先使用const_cast对常量解除其常量属性。请注意,这种转换操作只对顶层const有效;如果对底层const这么做,并尝试修改转换后得到的对象,其行为是未定义的,将产生不可预测的后果。

它的格式与static_cast类似,但所填的类型必须与被转换对象的非常量形式一致:

const_cast< 对应的类型 >( 对象 )

例如:

int i = 1;
const int &ref_i = i;    // 这是一个顶层 const,表面上不可更改,实际指向的 i 可以更改
const int c_i = 2;        // 这是一个底层 const,它本身属性即为不可更改
const_cast<int &>(ref_i) = 3;        // 正确,可以消除顶层 const
const_cast<int>(c_i) = 3;            // 错误,不可以消除底层 const
  • reinterpret_cast

这种转换运算符用于从低层次的字节级别对某对象作出强制转换。它相较于static_cast更加“危险”,因为这种转换操作将无视所有的转换规则,C++ 将毫无保留地将浮点数的每一字节写入以目标类型创建的返回值对象。

如果用得正确,它可以帮助你转换一些不易转化为二进制位的类型到二进制位;如果胡乱使用,所造成的后果也将会是十分严重的。

它的格式也与其它转换运算符相仿:

reinterpret_cast< 类型 >( 对象 )

  • 旧式转换

这种转换方式相当于上文提及的static_castconst_cast的中和产物,现行标准下已不再使用,本手册不多作介绍。除非有特殊需求(如中国的 NOI 竞赛与 CSP 认证考级仍在使用 C++98 旧标准),我们都不应当使用这种转换方式。

  • dynamic_cast 将在后文介绍,它是 C++ 的面向对象中的重要的类型转换运算符。

习题

  • 尝试写一个程序,输入一个小数部分大于 5 位的浮点数,将其以类型转换的方式将其转换为仅 1 位小数的浮点数并输出。

Last updated