【问题标题】:why the result type of two operand should match the type of the operands?为什么两个操作数的结果类型应该与操作数的类型匹配?
【发布时间】:2020-06-14 12:28:19
【问题描述】:

我是 C 新手,我正在阅读一本教科书,其中显示了一个函数的一些错误代码,用于确定一个字符串是否比另一个长:

int strlonger(char *s, char *t) {
    return strlen(s) - strlen(t) > 0;
}

错误的原因是因为strlen的返回类型是无符号整数,所以当左操作数结果为负时,它会被强制转换为无符号类型,因此会产生不正确的结果,例如-1 将是最大的无符号值,当然大于 0。

看来strlen(s) - strlen(t)的结果也是无符号整数,但为什么一定要这样呢?我的意思是,例如,0u-1u 是 -1,-1 是有符号整数,那么 C 应该保持这个值 -1 而不会将其转换回无符号,因为我没有像这样编码:

...
unsigned int result = strlen(s) - strlen(t);
return result > 0;

还是C有一些特殊的规则,两个操作数的结果类型应该匹配操作数的类型?

【问题讨论】:

  • 如果你使用return strlen(s) > strlen(t);,你不会得到整数溢出/下流
  • 0u - 1u 是一个很大的正数,而不是-1
  • 如果您调用 UB(可能使用 printf("%d\n", -1U);),任何事情都可能发生(在示例情况下,您的程序可能打印 -1,尽管这远不能保证)

标签: c


【解决方案1】:

首先strlen 的结果是size_t,它可以不同于无符号整数。它是未签名的,但它可以有不同的大小https://godbolt.org/z/GrB5_z

运算结果必须适合结果类型,因此最佳类型与操作数相同(假设它们相同)

做什么 - 只是改变你的函数的逻辑。

int strlonger(const char *s, const char *t) 
{
    return strlen(s) > strlen(t);
}

即使是函数名也暗示了这种方法

比如0u-1u是-1,-1是有符号整数

不,不是。当从 0u 中减去 1u 时,无符号整数会环绕。所以得到的整数将设置所有位(对于 32 位无符号整数,它将是 0xffffffff)

【讨论】:

    【解决方案2】:

    ……为什么一定要这样?

    理论上,它不必是这样的,但在 C 中就是这样设计的。

    具体规则在 C 2018 6.3.1.8,“常用算术转换”中:

    许多期望算术类型的操作数的运算符会以类似的方式导致转换和产生结果类型。目的是为操作数和结果确定一个通用实数类型。对于指定的操作数,每个操作数都在不改变类型域的情况下转换为对应的实数类型是公共实数类型的类型。除非另有明确说明,普通实数类型也是结果对应的实数类型,其类型域是操作数相同的类型域,否则复杂...... [强调。 ]

    这段话告诉我们,如果xy的类型为unsigned int,那么x - y的结果也是unsigned int。 (它继续提供有关操作数具有混合类型的情况的进一步规则,例如一个double 和一个int,或者具有窄整数类型,例如char,但对于unsigned int,情况很简单操作数。)

    为什么这是一个好的设计?考虑减去unsigned int 的结果可能是什么。令 Munsigned int 的最大值。最小值当然是0。如果xMy是0,那么x - yM。如果x 为0,yM,则x -y 为-M。所以x - y 的结果可以是从 -M 到 +M 的任何值。如果unsigned intN 位,我们需要一个带有 N+1 位的有符号整数来保存任何可能的结果。

    如果我们将运算的结果类型定义为可以保存任何数学结果的类型,那么这种设计将是行不通的。减去两个 32 位 unsigned int 将需要 33 位整数类型。并且添加或减去其中两个将需要 34 位类型。进一步的操作将需要越来越广泛的类型。不仅硬件通常不支持必要的宽度,而且在循环中进行计算需要动态类型随着循环的每次迭代而增长。 (这里只考虑加法和减法。通过乘法,所需的大小会增长得更快。)

    因此,我们的设计必须对结果类型使用固定大小。它们应该是什么?无论我们将两个unsigned int 值相减的结果定义为unsigned int 还是int,结果类型中只能表示部分可能的数学结果。在大多数情况下,更简单地说结果类型与操作数类型相同。程序员有责任确保他们保持在类型的范围内,或者,如果他们想要不同的东西,编写代码以获得他们想要的结果。

    例如,如果你知道size_t 在你的C 实现中是unsigned int,并且你知道long int 更宽,那么你可以写(long int) x - y。这将x 显式转换为更广泛的(和签名的)类型。 y 也被隐式转换,按照上面引用的规则,结果以long int 类型产生。那么无论xy的值如何,都不会溢出。

    综上所述,编译器管理类型避免溢出是不可行的,所以留给程序员去做。

    【讨论】:

      猜你喜欢
      • 2018-04-23
      • 1970-01-01
      • 2012-09-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-06-17
      相关资源
      最近更新 更多