"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+1
到 char
的转换;否则为假,因为 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+1
到 short
的转换,或者如果 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
被转换为 int
1,并且 1
已经是 int
。所以 sc + 1
比 SCHAR_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_MAX
和 1
的类型都是 int
,因此运算的类型是 int
,但是数学结果在 int
中不可表示,因此这是一个例外情况,并且该行为未由 C 标准定义。相反,在转换过程中,行为由标准部分定义:标准要求实现定义行为,并且它必须产生它定义的结果或定义信号。
在许多实现中,断言的计算结果为真。有关进一步讨论,请参阅下面的“不窄于 int
的有符号整数”部分。
有符号整数窄于 int
,前缀 ++
接下来,考虑从问题中提取的这种情况,除了我将 CHAR_MAX
和 CHAR_MIN
更改为 SCHAR_MAX
和 SCHAR_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 实现,只考虑 char
比 int
.
窄的实现
虽然所有这些断言在我的系统上都是正确的,但我显然调用了几个未定义的 and/or 特定于实现的行为。其中一些 显然不是真正的溢出。
参考
num = num + 1
does not cause an overflow.num
is automatically promoted toint
, and then the addition is performed inint
, which yields 128 without overflow. Then the assignment performs a conversion tochar
.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+1
到 char
的转换;否则为假,因为 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+1
到 short
的转换,或者如果 SHRT_MAX==INT_MAX
.
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
被转换为 int
1,并且 1
已经是 int
。所以 sc + 1
比 SCHAR_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_MAX
和 1
的类型都是 int
,因此运算的类型是 int
,但是数学结果在 int
中不可表示,因此这是一个例外情况,并且该行为未由 C 标准定义。相反,在转换过程中,行为由标准部分定义:标准要求实现定义行为,并且它必须产生它定义的结果或定义信号。
在许多实现中,断言的计算结果为真。有关进一步讨论,请参阅下面的“不窄于 int
的有符号整数”部分。
有符号整数窄于 int
,前缀 ++
接下来,考虑从问题中提取的这种情况,除了我将 CHAR_MAX
和 CHAR_MIN
更改为 SCHAR_MAX
和 SCHAR_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 实现,只考虑 char
比 int
.