3

我有这个简单的 C 程序。

#include <stdlib.h>
#include <stdio.h>
#include <stdbool.h>

bool foo (unsigned int a) {
    return (a > -2L);
}

bool bar (unsigned long a) {
    return (a > -2L);
}

int main() {
    printf("foo returned = %d\n", foo(99));
    printf("bar returned = %d\n", bar(99));
    return 0;
}

当我运行它时输出 -

foo returned = 1
bar returned = 0

Godbolt 中重新创建

我的问题是为什么foo(99)返回 true 但bar(99)返回 false。

bar对我来说,返回 false是有道理的。为简单起见,假设 long 是 8 位,然后(使用二进制补码作为有符号值):

99 == 0110 0011
-2 == unsigned 254 == 1111 1110

所以很明显 CMP 指令会看到 1111 1110 更大并返回 false。

但我不明白函数的幕后发生了什么foo。的程序集foo似乎硬编码为总是 return mov eax,0x1。我本来希望foo做类似的事情bar。这里发生了什么?

4

4 回答 4

3

这包含在 C 类中,并在文档中指定。以下是您如何使用文档来解决这个问题。

2018 C 标准中,您可以在索引中查找>或“关系表达式”以查看它们在第 68-69 页上的讨论。在第 68 页,您将找到第 6.5.8 节,其中涵盖了关系运算符,包括>. 阅读它,第 3 段说:

如果两个操作数都具有算术类型,则执行通常的算术转换。

“常用算术转换”列在第 39 页定义的索引中。第 39 页有第 6.3.1.8 节“常用算术转换”。该子句解释算术类型的操作数被转换为通用类型,并给出了确定通用类型的规则。对于符号不同的两个整数类型,例如in ( unsigned longand ) ,它表示如果无符号类型的秩大于或等于另一个类型的秩,则将有符号类型转换为无符号类型。long intbara-2L

“Rank” 不在索引中,但您可以搜索文档以找到它在第 6.3.1.1 节中讨论,它告诉您 的 ranklong int大于 的 rank int,并且任何无符号类型与 具有相同的 rank对应的类型。

现在你可以考虑a > -2Lbar,在a哪里unsigned long。在这里,我们将 aunsigned long与 a 进行比较long。它们具有相同的等级,因此-2L转换为unsigned long。第 6.3.1.3 节讨论了有符号整数到无符号整数的转换。它表示该值是通过将其模ULONG_MAX+1 包装来转换的,因此signed long-2 产生一个大整数。然后将a值为 99 的 与一个大整数进行比较,结果为>假,因此返回零。

对于foo,我们继续使用通常的算术转换规则。当无符号类型的秩不大于或等于有符号类型的秩,但有符号类型可以表示无符号类型操作数的类型的所有值时,将无符号类型的操作数转换为操作数签名类型的。在fooaunsigned int-2Llong int。大概在您的 C 实现中,long int是 64 位,因此它可以表示 32-bit 的所有值unsigned int。所以这条规则适用,并a转换为long int. 这不会改变值。因此,将 的原始值a99 与 -2 与 进行比较,结果为>真,因此返回 1。

于 2022-01-09T14:58:04.633 回答
2

在第一个函数中

bool foo (unsigned int a) {
    return (a > -2L);
}

表达式的两个操作数a > -2L都具有类型(由于通常的算术转换,long第一个操作数被转换为类型,因为类型的等级大于类型的等级,并且使用的系统中的所有类型值都可以是由类型表示)。很明显,正值大于负值。longlongunsigned intunsigned intlong99L-2L

第一个函数可以产生0等于sizeof( long )的结果sizeof( unsigned int )。在这种情况下,该类型long无法表示该类型的所有(正)值unsigned int。因此,由于通常的算术转换,两个操作数都将转换为 type unsigned long

例如,foo使用MS VS 2019where sizeof( long )is equal to运行函数4sizeof( unsigned int )您将得到结果0

这是一个用 C++ 编写的演示程序,直观地展示了foo使用 MS VS 2019 调用函数的结果可以等于0.

#include <iostream>
#include <iomanip>
#include <type_traits>

int main()
{
    unsigned int x = 0;
    long y = 0;

    std::cout << "sizeof( unsigned int ) = " << sizeof( unsigned int ) << '\n';
    std::cout << "sizeof( long ) = " << sizeof(long) << '\n';

    std::cout << "std::is_same_v<decltype( x + y ), unsigned long> is "
              << std::boolalpha
              << std::is_same_v<decltype( x + y ), unsigned long>
              << '\n';
}

程序输出为

sizeof( unsigned int ) = 4
sizeof( long ) = 4
std::is_same_v<decltype( x + y ), unsigned long> is true

这通常是第一个函数的结果是实现定义的。

在第二个功能

bool bar (unsigned long a) {
    return (a > -2L);
}

两个操作数都具有类型unsigned long(同样由于通常的算术转换和类型的等级unsigned long并且signed long彼此相等,因此类型的对象signed long被转换为类型unsigned long)并-2L解释为unsigned long大于99

于 2022-01-09T14:53:25.087 回答
2

其原因与整数转换的规则有关。

在第一种情况下,您使用运算符将​​ aunsigned int与a进行比较,在第二种情况下,您将 a与 a进行比较。long>unsigned longlong

这些操作数必须首先使用通常的算术转换转换为通用类型。这些在C 标准的第 6.3.1.8p1 节中有详细说明,以下摘录重点关注整数转换:

如果两个操作数具有相同的类型,则不需要进一步转换。

否则,如果两个操作数都具有有符号整数类型或都具有无符号整数类型,则具有较小整数转换等级的类型的操作数将转换为具有较高等级的操作数的类型。

否则,如果无符号整数类型的操作数的等级大于或等于另一个操作数类型的等级,则将有符号整数类型的操作数转换为无符号整数类型的操作数的类型。

否则,如果有符号整数类型的操作数的类型可以表示无符号整数类型的操作数的所有值,则将无符号整数类型的操作数转换为有符号整数类型的操作数的类型。

否则,两个操作数都转换为与带符号整数类型的操作数类型对应的无符号整数类型。

在比较的情况下,unsigned int第二long个粗体段落适用。long具有更高的等级并且(假设long是 64 位和int32 位)可以保存所有值而不是unsigned intcan,因此unsigned int操作数a被转换为 a long。由于所讨论的值在 范围内long,第 6.3.1.3p1 节规定了转换是如何发生的:

当整数类型的值转换为除 以外的其他整数类型_Bool时,如果该值可以用新的类型表示,则保持不变

所以这个值被保留了,我们只剩下99 > -2哪个是真的了。

在比较 aunsigned long和 a的情况下long,第一个粗体段落适用。两种类型的等级相同但符号不同,因此将long常数-2L转换为unsigned long。-2 超出了 a 的范围,unsigned long因此必须进行值转换。此转换在 6.3.1.3p2 节中指定:

否则,如果新类型是无符号的,则通过在新类型中可以表示的最大值的基础上反复加减一,直到该值在新类型的范围内。

所以long值 -2 将被转换为unsigned long值 2 64 -2,假设unsigned long是 64 位。所以我们剩下 99 > 2 64 -2,这是错误的。

于 2022-01-09T15:00:29.357 回答
0

我认为这里发生的是编译器的隐式提升。当您对两个不同的原语进行比较时,编译器会将其中一个提升为与另一个相同的类型。我相信规则是使用具有较大可能值的类型作为标准。因此,在 foo() 中,您隐式地将您的参数提升为带符号的 long 类型,并且比较按预期工作。在 bar() 中,您的参数是无符号长整数,其最大值大于有符号长整数。在这里,编译器将 -2L 提升为 unsigned long,这变成了一个非常大的数字。

于 2022-01-09T14:54:26.867 回答