说我有以下内容:
int i = 23;
float f = 3.14;
if (i == f) // do something
i
将被提升为 afloat
并且将比较两个float
数字,但是 a 可以float
代表所有int
值吗?为什么不将 theint
和 the都推广float
到 adouble
呢?
说我有以下内容:
int i = 23;
float f = 3.14;
if (i == f) // do something
i
将被提升为 afloat
并且将比较两个float
数字,但是 a 可以float
代表所有int
值吗?为什么不将 theint
和 the都推广float
到 adouble
呢?
当在积分提升int
中提升到unsigned
时,负值也会丢失(这导致了0u < -1
真实的乐趣)。
像 C 中的大多数机制(在 C++ 中继承)一样,通常的算术转换应该从硬件操作的角度来理解。C 的制造者非常熟悉他们所使用的机器的汇编语言,他们编写 C 是为了让他们自己和喜欢自己的人在编写在此之前都用汇编语言编写的东西(例如 UNIX核心)。
现在,处理器通常没有混合类型的指令(将浮点数添加到双精度,将整数与浮点数进行比较等),因为这会浪费晶圆上的空间——你必须实现您想要支持不同类型的操作码数倍。您只有“将 int 添加到 int”、“将浮点数与浮点数进行比较”、“将无符号与无符号相乘”等指令,首先需要进行通常的算术转换——它们是两种类型到指令的映射与他们一起使用最有意义的家庭。
从习惯编写低级机器代码的人的角度来看,如果您有混合类型,那么在一般情况下您最有可能考虑的汇编指令是那些需要最少转换的指令。浮点的情况尤其如此,在这种情况下,转换的运行时间很昂贵,尤其是在 1970 年代早期,当 C 语言被开发时,计算机速度很慢,并且浮点计算是在软件中完成的。这显示在通常的算术转换中——只有一个操作数被转换(除了long
/的一个例外unsigned int
,其中long
可以转换为unsigned long
,这不需要在大多数机器上做任何事情。也许在任何适用例外的地方都不需要)。
因此,编写通常的算术转换是为了完成汇编编码员大多数时候会做的事情:您有两种不适合的类型,将一种类型转换为另一种,这样它就可以了。这是您在汇编代码中所做的,除非您有特定的理由不这样做,并且对于习惯于编写汇编代码并且确实有特定原因强制进行不同转换的人来说,明确要求转换是自然的。毕竟,你可以简单地写
if((double) i < (double) f)
顺便说一句,有趣的是,在这种情况下,它unsigned
在层次结构中高于int
,因此与 的比较int
将以unsigned
无符号比较结束(因此0u < -1
从头开始)。我怀疑这是一个指标,人们在过去认为与其说unsigned
是限制int
不如说是对其值范围的扩展:我们现在不需要符号,所以让我们使用额外的位来获得更大的值范围。如果您有理由预期 an 会溢出,您会使用它——在 16 位sint
的世界中这是一个更大的担忧。int
甚至double
可能无法表示所有int
值,具体取决于包含多少位int
。
为什么不将 int 和 float 都提升为 double 呢?
可能是因为将这两种类型转换为double
比使用其中一个操作数(已经是 a float
, as )成本更高float
。它还将引入与算术运算符规则不兼容的比较运算符的特殊规则。
也无法保证浮点类型将如何表示,因此假设转换int
为double
(或什至long double
)进行比较将解决任何问题是盲目的。
类型提升规则被设计为简单并且以可预测的方式工作。C/C++ 中的类型自然是按它们可以表示的值范围“排序”的。有关详细信息,请参阅此。虽然浮点类型不能表示整数类型表示的所有整数,因为它们不能表示相同数量的有效数字,但它们可能能够表示更广泛的范围。
为了具有可预测的行为,当需要类型提升时,总是将数字类型转换为具有较大范围的类型,以避免在较小的类型中溢出。想象一下:
int i = 23464364; // more digits than float can represent!
float f = 123.4212E36f; // larger range than int can represent!
if (i == f) { /* do something */ }
如果转换为整数类型,则浮点数f
在转换为 int 时肯定会溢出,导致未定义的行为。另一方面,转换i
为f
只会导致精度损失,这是无关紧要的,因为f
具有相同的精度,因此比较仍有可能成功。此时由程序员根据应用要求来解释比较结果。
最后,除了双精度浮点数在表示整数时遇到相同的问题(有效数字的数量有限)之外,在这两种类型上使用提升会导致 具有更高的精度表示i
,而f
注定要具有原始精度,因此,如果i
有效数字比f
开头多,则比较将不会成功。现在这也是未定义的行为:比较可能对某些夫妇(i
, f
)成功,但对其他人则不然。
a 可以
float
代表所有int
值吗?
对于一个典型的现代系统,其中int
和float
都以 32 位存储,没有。必须付出一些东西。32 位的整数不会将 1 对 1 映射到包含分数的相同大小的集合上。
将
i
提升为 afloat
并将两个float
数字进行比较...</p>
不必要。你真的不知道将应用什么精度。C++14 §5/12:
浮动操作数的值和浮动表达式的结果可以用比类型要求更高的精度和范围来表示;类型不会因此而改变。
虽然i
提升后具有标称类型float
,但该值可以使用double
硬件来表示。C++ 不保证浮点精度丢失或溢出。(这在 C++14 中并不新鲜;它自古以来就继承自 C。)
为什么不将 the
int
和 the都推广float
到 adouble
呢?
如果您想在任何地方都获得最佳精度,double
请改用,您将永远不会看到float
. 或者long double
,但这可能会运行得更慢。考虑到一台机器可能提供多种替代精度,这些规则被设计为对大多数有限精度类型的用例相对合理。
大多数时候,快速和宽松就足够了,因此机器可以自由地做任何最简单的事情。这可能意味着舍入的单精度比较,或双精度且不舍入。
但是,这样的规则最终是妥协,有时它们会失败。为了在 C++(或 C)中精确地指定算术,它有助于明确地进行转换和提升。许多超可靠软件的风格指南完全禁止使用隐式转换,并且大多数编译器都会提供警告以帮助您删除它们。
要了解这些妥协是如何产生的,您可以仔细阅读C 基本原理文档。(最新版本涵盖 C99。)它不仅仅是 PDP-11 或 K&R 时代的无意义的包袱。
令人着迷的是,这里的许多答案都从 C 语言的起源开始争论,明确地将 K&R 和历史包袱命名为 int 在与 float 组合时转换为 float 的原因。
这是将责任归咎于错误的各方。在 K&R C 中,没有浮点计算之类的东西。 所有浮点运算均以双精度完成。出于这个原因,整数(或其他任何东西)从未隐式转换为浮点数,而只是转换为双精度数。浮点数也不能是函数参数的类型:如果您真的、真的、真的想避免转换为双精度型,则必须将指针传递给浮点型。为此,函数
int x(float a)
{ ... }
和
int y(a)
float a;
{ ... }
有不同的调用约定。第一个得到一个浮点参数,第二个(现在不再允许作为语法)得到一个双参数。
单精度浮点算术和函数参数仅在 ANSI C 中引入。Kernighan/Ritchie 是无辜的。
现在有了新的单浮点表达式(单浮点以前只是一种存储格式),还必须有新的类型转换。无论 ANSI C 团队在这里选择什么(我会不知所措)都不是 K&R 的错。
Q1:浮点数可以代表所有的 int 值吗?
IEE754 可以将所有整数完全表示为浮点数,最多约为 2 23,如this answer中所述。
Q2:为什么不将 int 和 float 都提升为 double?
标准中用于这些转换的规则是对 K&R 中的规则的轻微修改:修改适应了添加的类型和值保留规则。添加了显式许可证以执行比绝对必要的“更宽”类型的计算,因为这有时可以生成更小更快的代码,更不用说更频繁地正确答案了。只要获得相同的最终结果,也可以通过 as if 规则以“更窄”的类型执行计算。显式转换始终可用于获取所需类型的值。
以更广泛的类型执行计算意味着给定的float f1;
和float f2;
,f1 + f2
可以double
精确计算。这意味着给定的和int i;
可以精确计算。但正如评论中所说的hvd,它不需要以双精度计算。float f;
i == f
double
i == f
C标准也这么说。这些被称为通常的算术转换。以下描述直接取自 ANSI C 标准。
...如果任一操作数的类型为 float ,则另一个操作数将转换为类型 float 。
这是解释这一点的另一种方式:隐式执行通常的算术转换以将它们的值转换为通用类型。编译器首先执行整数提升,如果操作数仍然具有不同的类型,则将它们转换为以下层次结构中出现最高的类型:
来源。
在创建编程语言时,会直观地做出一些决定。
例如,为什么不将 int+float 转换为 int+int 而不是 float+float 或 double+double?如果 int->float 包含相同的位,为什么要调用它?为什么不调用 float->int 促销?
如果您依赖隐式类型转换,您应该知道它们是如何工作的,否则只需手动转换。
某些语言可能在设计时根本没有任何自动类型转换。并不是设计阶段的每一个决定都可以有充分的理由在逻辑上做出。
带有鸭子类型的 JavaScript 在幕后做出了更加模糊的决定。设计一种绝对合乎逻辑的语言是不可能的,我认为它涉及哥德尔不完备定理。你必须平衡逻辑、直觉、实践和理想。
问题是为什么:因为它速度快、易于解释、易于编译,而这些都是当时 C 语言发展起来的非常重要的原因。
您可能有不同的规则:对于算术值的每次比较,结果都是比较实际数值的结果。如果比较的表达式之一是一个常量,那将是微不足道的,比较有符号和无符号 int 时的一条附加指令,如果比较 long long 和 double 并在 long long 不能表示为 double 时想要正确的结果,则相当困难。(0u < -1 将是错误的,因为它会比较数值 0 和 -1 而不考虑它们的类型)。
在 Swift 中,通过禁止不同类型之间的操作很容易解决这个问题。
规则是为 16 位整数(所需的最小大小)编写的。具有 32 位整数的编译器肯定会将两边都转换为双精度值。无论如何,现代硬件中没有浮点寄存器,因此它必须转换为双精度。现在,如果您有 64 位整数,我不太确定它的作用。long double 是合适的(通常是 80 位,但它甚至不是标准的)。