我们在 4.12 -- 类型转换和 static_cast 简介一课中介绍了类型转换。回顾该课中最重要的几点:
- 将数据从一种类型转换为另一种类型的过程称为“类型转换”。
- 当需要一种数据类型但提供了另一种不同的数据类型时,编译器会自动执行隐式类型转换。
- 通过使用强制转换运算符(例如
static_cast
)来请求显式类型转换。 - 转换不会改变被转换的数据。相反,转换过程将该数据作为输入,并生成转换后的结果。
- 将一个值转换为另一种类型的值时,转换过程会生成一个目标类型的临时对象来保存转换结果。
在本章的前半部分,我们将深入探讨类型转换的工作原理。我们将从本课中的隐式转换开始,并在即将到来的 10.6 -- 显式类型转换(强制转换)和 static_cast 课中介绍显式类型转换(强制转换)。由于类型转换无处不在,因此了解在需要转换时底层发生了什么非常重要。这些知识也与理解重载函数(可以与其他函数同名的函数)的工作方式相关。
作者注
在本章中,我们将重点讨论值到其他类型值的转换。一旦我们引入了先决条件主题(例如指针、引用、继承等...),我们将介绍其他类型的转换。
为什么需要转换
对象的值存储为位序列,对象的数据类型告诉编译器如何将这些位解释为有意义的值。不同的数据类型可能以不同的方式表示“相同”的值。例如,整数值 3
可能存储为二进制 0000 0000 0000 0000 0000 0000 0000 0011
,而浮点值 3.0
可能存储为二进制 0100 0000 0100 0000 0000 0000 0000 0000
。
那么当我们做这样的事情时会发生什么呢?
float f{ 3 }; // initialize floating point variable with int 3
在这种情况下,编译器不能仅仅将用于表示 int
值 3
的位复制到为 float
变量 f
分配的内存中。如果它这样做,那么当 f
(类型为 float
)被求值时,这些位将被解释为 float
而不是 int
,鬼知道我们会得到什么 float
值!
题外话…
以下程序实际上将 int
值 3
打印为 float
:
#include <iostream>
#include <cstring>
int main()
{
int n { 3 }; // here's int value 3
float f {}; // here's our float variable
std::memcpy(&f, &n, sizeof(float)); // copy the bits from n into f
std::cout << f << '\n'; // print f (containing the bits from n)
return 0;
}
这会产生以下结果:
4.2039e-45
相反,整数值 3
需要转换为等效的浮点值 3.0
,然后才能存储在为 f
分配的内存中(使用 float
值 3.0
的位表示)。
隐式类型转换发生时
当某种类型的表达式在需要另一种类型的上下文中提供时,编译器会自动执行**隐式类型转换**(也称为**自动类型转换**或**强制转换**)。C++ 中绝大多数类型转换都是隐式类型转换。例如,隐式类型转换发生在以下所有情况中:
当使用不同数据类型的值初始化(或赋值给)变量时
double d{ 3 }; // int value 3 implicitly converted to type double
d = 6; // int value 6 implicitly converted to type double
当返回值的类型与函数声明的返回类型不同时
float doSomething()
{
return 3.0; // double value 3.0 implicitly converted to type float
}
当使用具有不同类型操作数的某些二元运算符时
double division{ 4.0 / 3 }; // int value 3 implicitly converted to type double
在 if 语句中使用非布尔值时
if (5) // int value 5 implicitly converted to type bool
{
}
当传递给函数的参数类型与函数参数类型不同时
void doSomething(long l)
{
}
doSomething(3); // int value 3 implicitly converted to type long
那么编译器是如何知道如何将值转换为不同类型的呢?
标准转换
作为核心语言的一部分,C++ 标准定义了一系列转换规则,称为“标准转换”。**标准转换**指定了各种基本类型(以及某些复合类型,包括数组、引用、指针和枚举)如何转换为同一组内的其他类型。
截至 C++23,有 14 种不同的标准转换。这些可以大致分为 5 个通用类别:
类别 | 含义 | 链接 |
---|---|---|
数字提升 | 将小整数类型转换为 int 或 unsigned int ,以及将 float 转换为 double 。 | 10.2 -- 浮点数和整数提升 |
数值转换 | 其他不是提升的整数和浮点数转换。 | 10.3 -- 数值转换 |
限定符转换 | 添加或删除 const 或 volatile 的转换。 | |
值类别转换 | 改变表达式值类别的转换 | 12.2 -- 值类别(左值和右值) |
指针转换 | 从 std::nullptr 到指针类型,或指针类型到其他指针类型的转换 |
例如,将 int
值转换为 float
值属于数值转换类别,因此编译器要执行这种转换,只需应用 int
到 float
的数值转换规则。
数值转换和数值提升是这些类别中最重要的,我们将在后续课程中更详细地介绍它们。
致进阶读者
以下是标准转换的完整列表:
类别 | 标准转换 | 描述 | 另请参阅 |
---|---|---|---|
值类别转换 | 左值到右值 | 将左值表达式转换为右值表达式 | 12.2 -- 值类别(左值和右值) |
值类别转换 | 数组到指针 | 将 C 风格数组转换为指向第一个数组元素的指针(又称数组退化) | 17.8 -- C 风格数组退化 |
值类别转换 | 函数到指针 | 将函数转换为函数指针 | 20.1 -- 函数指针 |
值类别转换 | 临时对象实体化 | 将值转换为临时对象 | |
限定符转换 | 限定符转换 | 从类型中添加或删除 const 或 volatile | |
数字提升 | 整数提升 | 将较小的整数类型转换为 int 或 unsigned int | 10.2 -- 浮点数和整数提升 |
数字提升 | 浮点数提升 | 将 float 转换为 double | 10.2 -- 浮点数和整数提升 |
数值转换 | 整数转换 | 不是整数提升的整数转换 | 10.3 -- 数值转换 |
数值转换 | 浮点数转换 | 不是浮点数提升的浮点数转换 | 10.3 -- 数值转换 |
数值转换 | 整数-浮点数转换 | 转换整数和浮点数类型 | 10.3 -- 数值转换 |
数值转换 | 布尔转换 | 将整数、无作用域枚举、指针或成员指针转换为布尔值 | 4.10 -- if 语句简介 |
指针转换 | 指针转换 | 将 std::nullptr 转换为指针,或将指针转换为 void 指针或基类 | |
指针转换 | 成员指针转换 | 将 std::nullptr 转换为成员指针或将基类的成员指针转换为派生类的成员指针 | |
指针转换 | 函数指针转换 | 将非抛异常函数指针转换为函数指针 |
类型转换可能失败
当调用类型转换时(无论是隐式还是显式),编译器将确定是否可以将值从当前类型转换为所需类型。如果找到了有效的转换,则编译器将生成所需类型的新值。
如果编译器找不到可接受的转换,则编译将因编译错误而失败。类型转换可能因多种原因而失败。例如,编译器可能不知道如何在原始类型和所需类型之间转换值。
例如
int main()
{
int x { "14" };
return 0;
}
因为没有从字符串字面量“14”到 int
的标准转换,所以编译器会产生错误。例如,GCC 会产生错误:prog.cc:3:13: error: invalid conversion from 'const char*' to 'int' [-fpermissive]
。
在其他情况下,特定功能可能会阻止某些类别的转换。例如:
int x { 3.5 }; // brace-initialization disallows conversions that result in data loss
即使编译器知道如何将 double
值转换为 int
值,当使用大括号初始化时,也会禁止收窄转换。
也存在编译器无法确定几个可能的类型转换中哪一个是最佳选择的情况。我们将在 11.3 -- 函数重载解析和歧义匹配 课中看到这种示例。
描述类型转换如何工作的完整规则集既冗长又复杂,而且在大多数情况下,类型转换“就是有效”。在接下来的几节课中,我们将介绍关于标准转换您需要了解的最重要的事情。如果某些不常见的情况需要更精细的细节,则完整规则在隐式转换的技术参考文档中有详细说明。
开始吧!