"Overflow" 各种 C 数据类型的断言:根据 C 标准,哪些断言是正确的?

"Overflow" assertions on various C data type: which ones are guaranteed to be true according to the C Standard?

虽然所有这些断言在我的系统上都是正确的,但我显然调用了几个未定义的 and/or 特定于实现的行为。其中一些 显然不是真正的溢出。

参考:这就是我问这个问题的原因。

num = num + 1 does not cause an overflow. num is automatically promoted to int, and then the addition is performed in int, which yields 128 without overflow. Then the assignment performs a conversion to char.

This is not an overflow but, per C 2018 6.3.1.3, produces an implementation-defined result or signal. This differs from overflow because the C standard does not specify the behavior upon overflow at all, but, in this code, it specifies that the implementation must define the behavior. - Eric Postpischil

我在评论中添加了我认为是实际行为的内容。

因为我一直依赖于错误的观念,所以我宁愿不去假设任何事情。

#include <limits.h>
#include <assert.h>
#include <stdint.h>
#include <stddef.h>

int main(void)
{
    signed char     sc = CHAR_MAX;
    unsigned char   uc = UCHAR_MAX;
    signed short    ss = SHRT_MAX;
    unsigned short  us = USHRT_MAX;
    signed int      si = INT_MAX;
    unsigned int    ui = UINT_MAX;
    signed long     sl = LONG_MAX;
    unsigned long   ul = ULONG_MAX;
    size_t          zu = SIZE_MAX;

    ++sc;
    ++uc;
    ++ss;
    ++us;
    ++si;
    ++ui;
    ++sl;
    ++ul;
    ++zu;
    assert(sc == CHAR_MIN); //integer promotion, implementation specific ?
    assert(uc == 0); //integer promotion, implementation specific ?
    assert(ss == SHRT_MIN); //integer promotion, implementation specific ? 
    assert(us == 0); //integer promotion, implementation specific ?
    assert(si == INT_MIN); //overflow & undefined
    assert(ui == 0); //wrap around: Guaranteed
    assert(sl == LONG_MIN); //overflow & undefined ?
    assert(ul == 0); //wrap around: Guaranteed ?
    assert(zu == 0); //wrap around : Guaranteed ?
    return (0);
}

“Overflow” assertions on various C data type:

符合 C 标准

assert(uc == 0);
assert(us == 0);
assert(ui == 0);
assert(ul == 0);
assert(zu == 0);

我想你想测试一下 signed char sc = SCHAR_MAX; ... assert(sc == SCHAR_MIN);

当有符号类型的范围比int更窄时:
"result is implementation-defined or an implementation-defined signal is raised" 作为 ++ 重新分配的一部分。

当有符号类型与int一样宽或范围更广时:
UB 由于 ++.

期间有符号整数溢出
assert(sc == CHAR_MIN); //integer promotion, implementation specific ?

如果 char 已签名,则取决于实现定义的 CHAR_MAX+1char 的转换;否则为假,因为 CHAR_MIN != SCHAR_MIN。如果 CHAR_MAX==INT_MAX(可能,但不能满足托管实施的其他要求;参见 Can sizeof(int) ever be 1 on a hosted implementation?),那么原始 sc++ 是 UB。

assert(uc == 0); //integer promotion, implementation specific ?

永远正确。

assert(ss == SHRT_MIN); //integer promotion, implementation specific ? 

sc 情况相同的逻辑。取决于实现定义的 SHRT_MAX+1short 的转换,或者如果 SHRT_MAX==INT_MAX.

则为 UB
assert(us == 0); //integer promotion, implementation specific ?

永远正确。

assert(si == INT_MIN); //overflow & undefined

UB.

assert(ui == 0); //wrap around: Guaranteed

永远正确。

assert(sl == LONG_MIN); //overflow & undefined ?

UB.

assert(ul == 0); //wrap around: Guaranteed ?

永远正确。

assert(zu == 0); //wrap around : Guaranteed ?

永远正确。

以下所有引用均来自C 2018,正式版

有符号整数窄于 int,二进制 +

让我们先讨论这个案例,因为它是引发这个问题的案例。考虑这段代码,它没有出现在问题中:

signed char     sc = SCHAR_MAX;
sc = sc + 1;
assert(sc == SCHAR_MIN);

6.5.6 讨论了二进制 + 运算符。第 4 段说 是对它们执行的。这导致 sc + 1 中的 sc 被转换为 int1,并且 1 已经是 int。所以 sc + 1SCHAR_MAX 多了一个(通常是 127 + 1 = 128),并且在加法中没有溢出或表示问题。

然后我们必须执行6.5.16.1中讨论的赋值。第 2 段说“……右操作数的值被转换为赋值表达式的类型,并替换存储在左操作数指定的对象中的值。”所以我们必须把这个大于SCHAR_MAX的值转换成signed char,显然不能用signed char.

表示

6.3.1.3告诉我们整数的转换。对于这种情况,它说“......否则,新类型被签名并且不能在其中表示值;结果要么是实现定义的,要么是引发了实现定义的信号。”

因此,我们有一个实现定义的结果或信号。这与溢出不同,溢出是在表达式求值期间结果不可表示时发生的情况。 6.5 5 表示“如果在计算表达式期间出现 异常条件 (即,如果结果未在数学上定义或不在其类型的可表示值范围内),则行为未定义。”例如,如果我们计算 INT_MAX + 1,那么 INT_MAX1 的类型都是 int,因此运算的类型是 int,但是数学结果在 int 中不可表示,因此这是一个例外情况,并且该行为未由 C 标准定义。相反,在转换过程中,行为由标准部分定义:标准要求实现定义行为,并且它必须产生它定义的结果或定义信号。

在许多实现中,断言的计算结果为真。有关进一步讨论,请参阅下面的“不窄于 int 的有符号整数”部分。

有符号整数窄于 int,前缀 ++

接下来,考虑从问题中提取的这种情况,除了我将 CHAR_MAXCHAR_MIN 更改为 SCHAR_MAXSCHAR_MIN 以匹配 signed char类型:

signed char     sc = SCHAR_MAX;
++sc;
assert(sc == SCHAR_MIN);

我们使用一元 ++ 而不是二进制 +。 6.5.3.1 2 说“前缀 ++ 的操作数的值递增......”该条款没有明确说明执行通常的算术转换或整数提升,但它确实在第 2 段中说,“有关约束、类型、副作用和转换以及操作对指针的影响的信息,请参阅加法运算符和复合赋值的讨论。”这告诉我们它的行为类似于 sc = sc + 1;,上面关于二进制 + 的部分适用于前缀 ++,所以行为是相同的。

窄于 int 的无符号整数,二进制 +

考虑将此代码修改为使用二进制 + 而不是前缀 ++:

unsigned char   uc = UCHAR_MAX;
uc = uc + 1;
assert(uc == 0);

signed char一样,用int进行运算,然后转换为赋值目标类型。这种转换由 6.3.1.3 规定:“否则,如果新类型是无符号的,则通过比新类型可以表示的最大值重复加或减一来转换值,直到该值在新类型。”因此,从数学结果 (UCHAR_MAX + 1) 中减去最大值 (UCHAR_MAX + 1),直到该值在范围内。一次减法得到0,在范围内,所以结果为0,断言为真。

无符号整数窄于 int,前缀 ++

考虑从问题中提取的这段代码:

unsigned char   uc = UCHAR_MAX;
++uc;
assert(uc == 0);

与前面的前缀 ++ 情况一样,算法与上面讨论的 uc = uc + 1 相同。

不小于 int

的有符号整数

在此代码中:

signed int      si = INT_MAX;
++si;
assert(si == INT_MIN);

或此代码:

signed int      si = INT_MAX;
si = si + 1;
assert(si == INT_MIN);

算法是使用int执行的。在任何一种情况下,计算都会溢出,并且行为未由 C 标准定义。

如果我们思考实现将做什么,有几种可能性:

  • 在二进制补码实现中,将 INT_MAX 加 1 得到的位模式溢出到 INT_MIN 的位模式,这是该实现有效使用的值。
  • 在补码实现中,INT_MAX 加 1 的位模式溢出到 INT_MIN 的位模式,尽管它与我们熟悉的 [=] 的值不同64=](-2-31+1 而不是-2-31)。
  • 在符号和大小的实现中,将 INT_MAX 加 1 得到的位模式溢出到 −0 的位模式。
  • 硬件检测到溢出,出现信号
  • 编译器检测到溢出并在优化过程中以意想不到的方式转换代码。

不小于 int

的无符号整数

这种情况并不显着;行为与上面讨论的窄于 int 的情况相同:算术换行。

脚注

1 根据 Stack Overflow 其他地方的讨论,char(和 signed char)类型在理论上可能与int。这使 C 标准在 EOF 和其他可能的问题上变得紧张,并且 C 委员会当然没有预料到。这个答案无视这种深奥的 C 实现,只考虑 charint.

窄的实现