C++ 类型转换完全指南:隐式转换和static_cast详解

隐式类型转换基础

考虑以下程序:

#include <iostream>

void print(double x) // print函数的参数类型为double
{
    std::cout << x << '\n';
}

int main()
{
    print(5); // 当我们传入一个int类型的值时会发生什么?

    return 0;
}

在上述示例中,print() 函数的参数类型为 double,但调用者传入的值为 5,其类型为 int。在这种情况下会发生什么呢?

在大多数情况下,C++ 允许我们将一种基本类型的值转换为另一种基本类型的值。将数据从一种类型转换为另一种类型的过程称为类型转换。因此,int 类型的参数 5 将被转换为 double 类型的值 5.0,然后复制到参数 x 中。print() 函数将打印此值,结果如下所示:

5

提醒

默认情况下,小数部分为 0 的浮点数值在打印时不会显示小数位(例如,5.0 会打印为 5)。

当编译器在我们没有明确要求的情况下为我们进行类型转换时,我们称之为隐式类型转换。上面的例子就体现了这一点——我们并没有明确告诉编译器将整数值 5 转换为 double 类型的值 5.0。相反,函数期望一个 double 类型的值,而我们传入了一个整数参数。编译器会注意到这种不匹配,并隐式地将整数转换为 double 类型。

下面是一个类似的例子,我们的参数是一个 int 类型的变量,而不是一个 int 类型的字面量:

#include <iostream>

void print(double x) // print函数的参数类型为double
{
    std::cout << x << '\n';
}

int main()
{
    int y { 5 };
    print(y); // y的类型为int

    return 0;
}

这与上面的例子完全相同。int 类型的变量 y 所持有的值 5 将被转换为 double 类型的值 5.0,然后复制到参数 x 中。

值的类型转换会产生一个新的值

类型转换过程不会修改提供要转换数据的值(或对象)。相反,转换过程将该数据作为输入,并产生转换后的结果。

关键要点

将一个值从一种类型转换为另一种类型的值,其行为类似于调用一个返回类型与转换目标类型相匹配的函数。要转换的数据作为参数传入,转换后的结果作为临时对象返回,供调用者使用。

在上面的例子中,转换并没有将变量 y 的类型从 int 改为 double,也没有将 y 的值从 5 改为 5.0。相反,转换以 y 的值(5)作为输入,并返回一个值为 5.0double 类型的临时对象。然后将这个临时对象传递给 print 函数。

对于高级读者

一些高级的类型转换(例如涉及 const_castreinterpret_cast 的转换)不会返回临时对象,而是重新解释现有值或对象的类型。

隐式类型转换警告

尽管隐式类型转换足以应对大多数需要类型转换的情况,但仍有少数情况它并不适用。考虑以下程序,它与上面的例子类似:

#include <iostream>

void print(int x) // 现在print函数的参数类型为int
{
    std::cout << x << '\n';
}

int main()
{
    print(5.5); // 警告:我们传入了一个double类型的值

    return 0;
}

在这个程序中,我们将 print() 函数的参数类型改为 int,而对 print() 函数的调用现在传入了一个 double 类型的值 5.5。与上面的例子类似,编译器将使用隐式类型转换将 double 类型的值 5.5 转换为 int 类型的值,以便将其传递给 print() 函数。

与最初的示例不同的是,当编译这个程序时,你的编译器会生成某种关于可能的数据丢失的警告。而且因为你已经开启了"将警告视为错误"的选项(你确实开启了,对吧?),你的编译器将终止编译过程。

提示

如果你想编译这个示例,你需要暂时禁用"将警告视为错误"选项。有关此设置的更多信息,请参阅课程 0.11 – 配置你的编译器:警告和错误级别。

当编译并运行时,这个程序打印以下内容:

5

请注意,尽管我们传入了值 5.5,但程序打印了 5。因为整数值不能持有小数部分,当 double 类型的值 5.5 隐式转换为 int 类型时,小数部分被丢弃,只保留了整数值。

由于将浮点值转换为整数值会导致任何小数部分被丢弃,因此当编译器进行从浮点到整数的隐式类型转换时,会警告我们。即使我们传入一个没有小数部分的浮点值,如 5.0,编译器可能仍会警告我们这种转换是不安全的,尽管在这种特定情况下,转换为整数值 5 时实际上并没有丢失值。

关键要点

某些类型转换(例如从 charint)总是会保留正在转换的值,而其他类型转换(例如从 doubleint)可能会在转换过程中导致值被改变。不安全的隐式转换通常会生成编译器警告,或者(在使用大括号初始化的情况下)生成错误。

这是大括号初始化成为首选初始化形式的主要原因之一。大括号初始化将确保我们不会尝试用一个在隐式类型转换时会丢失值的初始化器来初始化变量:

int main()
{
    double d { 5 }; // 好的:从int到double是安全的
    int x { 5.5 }; // 错误:从double到int不安全

    return 0;
}

相关内容

隐式类型转换是一个复杂的话题。我们将在后续课程中更深入地探讨这个话题,从课程 10.1 – 隐式类型转换开始。

通过static_cast操作符介绍显式类型转换

回到我们最近的 print() 示例,如果我们故意想将一个 double 类型的值传递给一个接受整数的函数(知道转换后的值会丢弃任何小数部分吗?)关闭"将警告视为错误"选项只是为了使我们的程序能够编译是一个糟糕的主意,因为这样我们每次编译时都会收到警告(我们很快就会学会忽略这些警告),并且我们可能会忽略更严重问题的警告。

C++ 支持第二种类型转换方法,称为显式类型转换。显式类型转换允许我们(程序员)明确地告诉编译器将一个值从一种类型转换为另一种类型,并且我们对这种转换的结果负全责。如果这种转换导致值的丢失,编译器不会发出警告。

为了执行显式类型转换,在大多数情况下我们将使用 static_cast 操作符。static_cast 的语法看起来有点奇怪:

static_cast<new_type>(expression)

static_cast 以一个表达式的值作为输入,并将该值转换为由 new_type(例如 intboolchardouble)指定的类型。

关键要点

每当看到 C++ 语法(不包括预处理器)使用尖括号(<>)时,尖括号之间的内容很可能是一个类型。这通常是 C++ 处理需要参数化类型的代码的方式。

让我们使用 static_cast 更新之前的程序:

#include <iostream>

void print(int x)
{
    std::cout << x << '\n';
}

int main()
{
    print( static_cast<int>(5.5) ); // 显式地将double类型的值5.5转换为int类型

    return 0;
}

因为我们现在明确请求将 double 类型的值 5.5 转换为 int 类型的值,所以编译器在编译时不会生成关于可能的数据丢失的警告(这意味着我们可以保持"将警告视为错误"选项启用)。

相关内容

C++ 支持其他类型的强制转换。我们将在后续课程 10.6 – 显式类型转换(强制转换)和 static_cast 中更多地讨论不同类型的强制转换。

使用static_cast将char转换为int

在关于字符的课程 4.11 – 字符中,我们看到使用 std::cout 打印 char 类型的值时,该值会被当作字符打印:

#include <iostream>

int main()
{
    char ch{ 97 }; // 97是'a'的ASCII码
    std::cout << ch << '\n';

    return 0;
}

这将打印:

a

如果我们想打印整数值而不是字符,我们可以通过使用 static_cast 将值从 char 转换为 int 来实现:

#include <iostream>

int main()
{
    char ch{ 97 }; // 97是'a'的ASCII码
    // 将变量ch的值作为int打印
    std::cout << ch << " has value " << static_cast<int>(ch) << '\n';

    return 0;
}

这将打印:

a has value 97

值得注意的是,static_cast 的参数作为一个表达式进行求值。当我们传入一个变量时,该变量会被求值以产生其值,然后该值被转换为新的类型。变量本身不会因将其值转换为新类型而受到影响。在上面的例子中,变量 ch 仍然是一个 char 类型,并且即使我们将其值转换为 int 类型后,它仍然持有相同的值。

使用static_cast进行符号转换

可以使用 static_cast 在有符号整数值和无符号整数值之间进行转换。

如果要转换的值可以在目标类型中表示,转换后的值将保持不变(只有类型会改变)。例如:

#include <iostream>

int main()
{
    unsigned int u1 { 5 };
    // 将u1的值转换为有符号int
    int s1 { static_cast<int>(u1) };
    std::cout << s1 << '\n'; // 打印5

    int s2 { 5 };
    // 将s2的值转换为无符号int
    unsigned int u2 { static_cast<unsigned int>(s2) };
    std::cout << u2 << '\n'; // 打印5

    return 0;
}

这将打印:

5
5

由于值 5 既在有符号 int 的范围内,也在无符号 int 的范围内,因此值 5 可以安全地转换为这两种类型。

如果要转换的值不能在目标类型中表示:

  • 如果目标类型是无符号的,值将进行模运算包装。我们在课程 4.5 – 无符号整数及其为何应避免中讨论了模运算包装。
  • 如果目标类型是有符号的,在 C++20 之前,结果是实现定义的,从 C++20 开始,将进行模运算包装。

以下是一个将两个值转换为目标类型无法表示的值的例子(假设是 32 位整数):

#include <iostream>

int main()
{
    int s { -1 };
    std::cout << static_cast<unsigned int>(s) << '\n'; // 打印4294967295

    unsigned int u { 4294967295 }; // 最大的32位无符号整数
    std::cout << static_cast<int>(u) << '\n'; // 在C++20之前是实现定义的,从C++20开始是-1

    return 0;
}

从 C++20 开始,这将产生以下结果:

4294967295
-1

有符号整数值 -1 无法表示为无符号整数。结果模运算包装为无符号整数值 4294967295

无符号整数值 4294967295 无法表示为有符号整数。在 C++20 之前,结果是实现定义的(但可能为 -1)。从 C++20 开始,结果将模运算包装为 -1

警告

在 C++20 之前,将无符号整数值转换为有符号整数值如果要转换的值不能在有符号类型中表示,将导致实现定义的行为。

std::int8_t和std::uint8_t可能表现得像字符而不是整数

正如在课程 4.6 – 定宽整数和size_t中提到的,大多数编译器定义并处理 std::int8_tstd::uint8_t(以及相应的快速和最小定宽类型)与 signed charunsigned char 类型完全相同。现在我们已经了解了字符是什么,我们可以展示这可能会导致什么问题:

#include <cstdint>
#include <iostream>

int main()
{
    std::int8_t myInt{65};      // 用值65初始化myInt
    std::cout << myInt << '\n'; // 你可能期望这会打印65

    return 0;
}

因为 std::int8_t 自称为一个整数,你可能会被误导认为上面的程序将打印整数值 65。然而,在大多数系统上,这个程序将打印 A(将 myInt 作为有符号字符处理)。然而,这并不能保证(在某些系统上,它实际上可能打印 65)。

如果你想确保 std::int8_tstd::uint8_t 对象被视为整数,你可以使用 static_cast 将值转换为整数:

#include <cstdint>
#include <iostream>

int main()
{
    std::int8_t myInt{65};
    std::cout << static_cast<int>(myInt) << '\n'; // 总是会打印65

    return 0;
}

std::int8_t 被当作字符处理的情况下,从控制台输入也会导致问题:

#include <cstdint>
#include <iostream>

int main()
{
    std::cout << "Enter a number between 0 and 127: ";
    std::int8_t myInt{};
    std::cin >> myInt;

    std::cout << "You entered: " << static_cast<int>(myInt) << '\n';

    return 0;
}

这个程序的一个示例运行:

Enter a number between 0 and 127: 35
You entered: 51

以下是发生的情况。当 std::int8_t 被当作字符处理时,输入例程将我们的输入解释为一系列字符,而不是一个整数。因此,当我们输入 35 时,我们实际上输入了两个字符,'3''5'。因为一个字符对象只能持有一个字符,所以 '3' 被提取('5' 留在输入流中,供以后可能提取)。由于字符 '3' 的 ASCII 码点为 51,值 51 被存储在 myInt 中,我们稍后将其作为整数打印出来。

相比之下,其他定宽类型将始终以整数值的形式进行打印和输入。

测验时间

问题 #1

编写一个简短的程序,要求用户输入一个字符。使用 static_cast 打印该字符的值及其 ASCII 码。

程序的输出应与以下内容匹配:

Enter a single character: a
You entered 'a', which has ASCII code 97.

显示答案

问题 #2

修改你在问题 #1 中编写的程序,改用隐式类型转换而不是 static_cast。你能想到多少种不同的方法来实现这一点?

注意:你应该优先使用显式转换而不是隐式转换,所以在实际程序中不要这样做——这仅仅是测试你对隐式转换可能发生位置的理解。

显式类型转换详解

static_cast的基本语法

static_cast 的语法看起来有点奇怪:

static_cast<new_type>(expression)

static_cast的实际应用

让我们使用 static_cast 更新之前的程序:

#include <iostream>

void print(int x)
{
    std::cout << x << '\n';
}

int main()
{
    print( static_cast<int>(5.5) ); // 显式地将double类型的值5.5转换为int类型

    return 0;
}

常见类型转换场景

字符与整数的转换

在关于字符的课程中,我们看到使用 std::cout 打印 char 类型的值时,该值会被当作字符打印:

#include <iostream>

int main()
{
    char ch{ 97 }; // 97是'a'的ASCII码
    std::cout << ch << '\n';

    return 0;
}

有符号与无符号整数的转换

可以使用 static_cast 在有符号整数值和无符号整数值之间进行转换:

#include <iostream>

int main()
{
    unsigned int u1 { 5 };
    // 将u1的值转换为有符号int
    int s1 { static_cast<int>(u1) };
    std::cout << s1 << '\n'; // 打印5

    return 0;
}

特殊情况和注意事项

8位整数类型的特殊处理

正如在课程 4.6 中提到的,大多数编译器定义并处理 std::int8_tstd::uint8_tsigned charunsigned char 类型完全相同:

#include <cstdint>
#include <iostream>

int main()
{
    std::int8_t myInt{65};      // 用值65初始化myInt
    std::cout << myInt << '\n'; // 你可能期望这会打印65

    return 0;
}

实践练习

测验题目

  1. 编写一个简短的程序,要求用户输入一个字符。使用 static_cast 打印该字符的值及其 ASCII 码。

  2. 修改你在问题 #1 中编写的程序,改用隐式类型转换而不是 static_cast。你能想到多少种不同的方法来实现这一点?

注意:你应该优先使用显式转换而不是隐式转换,所以在实际程序中不要这样做——这仅仅是测试你对隐式转换可能发生位置的理解。

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

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

公众号二维码

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