C语言中“==”的确切含义是什么?

3

我对C语言中运算符==的确切含义有些困惑。

它是比较变量代表的数学值(取决于它们的类型)还是变量背后的比特模式?具体来说:

int x = 0x80000000;
unsigned y = x;
x==y // true

所以尽管x是一个很大的负数而y是一个很大的正数,它们是相等的(我猜是因为它们有相同的二进制模式)。

int64_t x = 0x8000000000000000;
int y = x;
x==y // false

这里不重要的是x和y的前32位(最低有效位)是否相同。因此,在这种情况下,C会查看变量所表示的值。

有哪些官方规则?有没有权威参考资料(在K&R中没有找到任何有用的信息)?

我使用gcc编译器进行上述示例。


“这里并不重要的是x和y中最初(最低有效)的32位是否相同” - 是的,因为32个最高有效位相同。 - Robert Harvey
我认为你只是缺乏关于计算机系统中位的工作原理的知识。无论你是否理解,第二个例子都是一种有损转换;当你将y赋值给x时,你将不可避免地丢失一些位,因为y是一个32位数字,而x是一个64位数字,没有办法存储所有64位。因此,这两个数字根本不会相等。 - Robert Harvey
演示在此:https://replit.com/@robertwharvey/UrbanUprightDrivers#main.c - Robert Harvey
@MSalters:对于原帖作者目前的理解水平来说,这可能需要一年时间才能消化。 - Robert Harvey
@RobertHarvey 现在是开始的好时机 :D - Antti Haapala -- Слава Україні
显示剩余5条评论
3个回答

7
在询问比较是基于值还是位模式之前,你已经得出了结论,因为有一个重要的步骤。在比较之前,== 的操作数会被转换为一个共同的类型。
例如,在比较一个32位二进制补码的int x和位模式1000…00002(表示 −2,147,483,648)以及一个带有相同位模式(表示 +2,147,483,648)的unsigned int y 时,如果用 x == y 进行比较,则x首先转换为unsigned int,此时结果为+2,147,483,648。然后将+2,147,483,648与+2,147,483,648进行比较,所以== 则报告它们相等。
根据 C 2018 6.5.9 (“Equality operators”) 4 的规定:

如果操作数都是算术类型,执行常规算术转换……

常规算术转换在6.3.1.8中以如下方式描述: 众多期望算术类型操作数的运算符会以类似方式执行转换并产生类似结果类型。目的是确定操作数和结果的公共实际类型。对于指定的操作数,每个操作数都将被转换……到与其对应实际类型相同的类型。
规则涉及一些技术细节,但在很大程度上,当你比较两个整数类型时,首先会将每个类型提升至至少为int,然后将较窄的类型转换为较宽的类型。如果它们是相同宽度但一个是无符号的,则有符号类型将被转换为无符号类型。这可能会改变值。
确定要比较的实际值后,== 的结果将基于这些值而不是位模式来定义。
(最常见的区别情况是带有浮点 +0 和 −0 的情况,它们表示相同的实数且比较相等但具有不同的表示。在大多数现代环境中,整数类型中的所有位模式表示不同的值,并且二进制浮点类型中的所有位模式代表不同的值或 NaN,除了+0和−0之外。有一些使用较少的浮点类型对某些值具有多个表示方式,类似于3.5•107和35•106表示相同的数字。)
如果您比较带符号整型中的负值(经过推广后)与宽度相同或更宽的无符号类型,则在比较之前将更改有符号类型的值。因此,您有一定风险得到“数学上错误”的结果。

3
首先,让我们看一下x的初始化。假设一个32位的int,常量0x80000000具有类型unsigned int,值为2的31次方。因此,这个常量必须转换为int类型。C标准第6.3.1.3p3节规定了这种情况的处理方式:

否则,新类型是带符号的,且该值无法表示;结果要么是实现定义的,要么会引发实现定义的信号。

因此,进行实现定义的转换。在补码系统中,通常通过将所涉及的值的表示的低32位直接分配给要分配的对象来实现这一点。这导致x的值为-2的31次方。
现在,x 被分配给 y。这意味着该值从 int 转换为 unsigned int,并且该值为负数。因此,转换由第6.3.1.3p2节规定:

否则,如果新类型是无符号的,则通过反复添加或减去一个最大值加一的值,该最大值可以被表示为新类型,直到该值在新类型的范围内。

unsigned int 的最大值(假设为32位)为232-1,因此比这多一个是232。将232加上-231得到231,这就是存储在y中的值。
现在来看比较。
当两个不同的算术类型通过==运算符进行比较时,它们会经历通常算术转换
C标准的第6.3.1.8p1节阐述了关于如何转换两种整型的规定:
如果两个操作数具有相同的类型,则不需要进一步转换。否则,如果两个操作数具有带符号整数类型或都具有无符号整数类型,则将具有较小整数转换等级的操作数转换为具有更高等级的操作数类型。否则,如果具有无符号整数类型的操作数的等级大于或等于另一个操作数类型的等级,则将具有带符号整数类型的操作数转换为具有无符号整数类型的操作数类型。否则,如果具有带符号整数类型的操作数的类型可以表示具有无符号整数类型的操作数类型的所有值,则将具有无符号整数类型的操作数转换为具有带符号整数类型的操作数类型。否则,两个操作数都将转换为与具有带符号整数类型的操作数类型对应的无符号整数类型。

因为我们正在比较一个int和一个unsigned int,所以粗体段落适用于此。 因此,将x的值转换为unsigned int类型。

回到第6.3.1.3p2节的转换规则,假设32位的unsigned int的最大值为232-1,所以比这个多一点的是232。 将232加上x的值(即-231)得到231。 这与y的值相同,因此比较结果为真。

在第二个示例中,当x的类型为int64_t时,y的类型为int。当x被分配给y时,从int64_tint 的实现定义转换可能导致y为0,因为x的低32位都为0。

2
根据语言定义,确切的意思是:
6.2.6 类型的表示
6.2.6.1 一般
4 非位域对象中存储的值由 n × CHAR_BIT 位组成,其中 n 是该类型的对象大小(以字节为单位)。该值可以被复制到 unsigned char [n] 类型的对象中(例如,通过 memcpy);结果字节集称为值的“对象表示”。存储在位域中的值由 m 位组成,其中 m 是指定的位域大小。对象表示是位域在包含它的可寻址存储单元中所包含的 m 位。除 NaN 之外的两个具有相同对象表示的值比较相等,但比较相等的值可能具有不同的对象表示。
...
6.5.9 相等运算符
4 如果两个操作数都具有算术类型,则执行通常的算术转换。如果复合类型的实部相等并且虚部也相等,则复合类型的值相等。来自不同类型域的算术类型的任何两个值仅当其转换为通常算术转换确定的(复合)结果类型的结果相等时才相等。
5 否则,至少有一个操作数是指针。如果一个操作数是指针,另一个操作数是空指针常量,则将空指针常量转换为指针类型。如果一个操作数是指向对象类型的指针,而另一个操作数是指向 void 的限定或非限定版本的指针,则前者被转换为后者的类型。
6 如果两个指针都是空指针,则它们相等;如果两个指针都是指向同一对象(包括指向对象及其子对象的指针)或函数的指针,则它们相等;如果两个指针都是指向同一数组对象的最后一个元素之后的一个位置,则它们相等;或者,如果一个指针是指向一个数组对象的末尾,并且另一个指针是指向地址空间中紧随第一个数组对象之后的不同数组对象的开头,则它们相等。
109) 两个对象可能在内存中相邻,因为它们是较大数组的相邻元素或结构的相邻成员,它们之间没有填充,或者因为实现选择这样放置它们,即使它们是无关的。如果先前的无效指针操作(例如访问数组界外)产生未定义行为,则随后的比较也会产生未定义行为。 C 2011 Online Draft(C 2011网络草案)。
从语义上讲,运算符 == 比较的是值,而不是位 - 即使两个操作数具有完全不同的位表示,1.0 == 1将评估为true
然而,在比较中,整数1将首先转换为浮点数1.0,以便可以进行位比较。

然而,在比较的过程中,整数1将首先被转换为浮点数1.0,以便进行位比较。等等,为什么?没有任何阻止两个整数之间进行按位比较,而浮点数在这个领域有各种问题。 - Robert Harvey
如果你比较1.0 == 1,那么左边是一个double类型,右边将被转换为double类型,并且两个double值之间进行比较。这就是标准规定的;这也是C语言一直以来的做法。对于常量表达式,转换和计算可能在编译时完成。如果其中一个值是变量引用或涉及变量的表达式,则转换在编译时完成。精度可能存在问题是无关紧要的;标准规定了必须这样做。 - Jonathan Leffler

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接