向size_t添加或指定整数文字
在CI中,可以看到许多代码将size_t
变量添加或赋予整数文字。
size_t foo = 1; foo += 1;
这里发生了什么转换,是否会发生size_t
“升级”为int
然后转换回size_t
? 如果我在最大程度上,那还会环绕吗?
size_t foo = SIZE_MAX; foo += 1;
这是定义的行为吗? 它是一个无符号类型size_t
,它添加了一个signed int
(可能是一个更大的类型?)并转换回size_t
。 有签名整数溢出的风险吗?
写一些像foo + bar + (size_t)1
而不是foo + bar + 1
这样有意义吗? 我从来没有看到这样的代码,但我想知道如果整数促销是麻烦的话是否有必要。
C89没有说明size_t
将如何排名或者究竟是什么排名:
结果的值是实现定义的,其类型(无符号整数类型)是在头中定义的size_t。
当前的C标准允许在执行以下代码时可能导致未定义行为的实现,但是这样的实现不存在,并且可能永远不会:
size_t foo = SIZE_MAX; foo += 1;
类型size_t为无符号类型1 ,最小范围为: 2 [0,65535]。
类型size_t可以定义为unsigned short类型的同义词。 无符号短类型可以定义为具有16个精度位,范围为:[0,65535]。 在这种情况下,SIZE_MAX的值为65535。
int类型可以定义为具有16个精度位(加一个符号位),二进制补码表示和范围:[ – 65536,65535]。
表达式foo + = 1,相当于foo = foo + 1(除了foo只被评估一次,但这里没有关系)。 变量foo将使用整数提升3进行提升。 它将被提升为int类型,因为int类型可以表示size_t类型的所有值,size_t的等级,是unsigned short的同义词,低于int的等级。 由于size_t和int的最大值相同,因此计算会导致签名溢出,从而导致未定义的行为。
这适用于当前的标准,它也适用于C89,因为它对类型没有任何更严格的限制。
避免任何可以想象的实现的带符号溢出的解决方案是使用无符号整数常量:
foo += 1u;
在这种情况下,如果foo的排名低于int,则使用通常的算术转换将其提升为unsigned int。
1 (引自ISO / IEC 9899 / 201x 7.19通用定义2)
为size_t
这是sizeof运算符的结果的无符号整数类型;
2 (引自ISO / IEC 9899 / 201x 7.20.3其他整数类型的限制2)
size_t的限制
SIZE_MAX 65535
3 (引自ISO / IEC 9899 / 201x 6.3.1.1布尔,字符和整数2)
如果可以使用int或unsigned int,则可以在表达式中使用以下内容:
具有整数类型(int或unsigned int除外)的对象或表达式,其整数转换等级小于或等于int和unsigned int的等级。
如果int可以表示原始类型的所有值(由宽度限制,对于位字段),该值将转换为int; 否则,它将转换为unsigned int。 这些被称为整数促销。 整数促销不会更改所有其他类型。
这取决于,因为size_t
是实现定义的无符号整数类型。
因此涉及size_t
操作将引入促销,但这些操作取决于size_t
实际是什么,以及表达式实际涉及的其他类型。
如果size_t
等于unsigned short
(例如16位类型),那么
size_t foo = 1; foo += 1;
将(语义上)将foo
提升为int
,添加1
,然后将结果转换回size_t
以存储在foo
。 (我说“语义上”,因为这是根据标准的代码的含义。编译器可以自由地应用“似乎”规则 – 即做任何它喜欢的事情,只要它提供相同的净效果)。
另一方面,如果size_t
等效于long long unsigned
(例如64位有符号类型),那么相同的代码会将1
提升为long long unsigned
类型,将其添加到foo
的值,并存储结果回到foo
。
在这两种情况下,除非发生溢出,否则净结果是相同的。 在这种情况下,没有溢出,因为int
和size_t
都保证能够表示值1
和2
。
如果发生溢出(例如,添加更大的整数值),则行为可以变化。 有符号整数类型(例如int
)的溢出会导致未定义的行为。 unsigned
整数类型的溢出使用模运算。
至于代码
size_t foo = SIZE_MAX; foo += 1;
可以进行相同的分析。
如果size_t
等于unsigned short
则foo
将转换为int
。 如果int
等效于有signed short
,则它不能表示SIZE_MAX
的值,因此转换将溢出,结果是未定义的行为。 如果int
能够表示比short int
更大的范围(例如它等于long
),则foo
到int
的转换将成功,递增该值将成功,并且存储回size_t
将使用模运算并生成结果0
。
如果size_t
等于unsigned long
,则值1
将转换为unsigned long
,并将其添加到foo
将使用模运算(即产生零的结果),并将其存储到foo
。
假设size_t
实际上是其他无符号整数类型,则可以进行类似的分析。
注意:在现代系统中, size_t
与int
相同或小于int
是不常见的。 但是,这样的系统已经存在(例如,Microsoft和Borland C编译器针对具有80286 CPU的硬件上的16位MS-DOS)。 还有16位微处理器仍在生产中,主要用于具有较低功耗和低吞吐量要求的嵌入式系统,以及针对它们的C编译器(例如针对Infeon XE166微处理器系列的Keil C166编译器)。 [注意:我从来没有理由使用Keil编译器,但是,鉴于它的目标平台,如果它支持的16位size_t
与该平台上的native int
类型相同或更小,那就不足为奇了]。
foo += 1
表示foo = foo + 1
。 如果size_t
比int
更窄(也就是说, int
可以表示size_t
所有值),那么foo
在表达式foo + 1
被提升为int
。
这可能溢出的唯一方法是INT_MAX
== SIZE_MAX
。 从理论上讲,这是可能的,例如16位int和15位size_t
。 (后者可能有1个填充位)。
更有可能的是, SIZE_MAX
将小于INT_MAX
,因此代码将由于超出范围的分配而由实现定义 。 通常,实现定义是“明显的”,高位被丢弃,因此结果将为0
。
作为一个实际的决定,我不建议修改你的代码以满足这些情况(15位size_t
,或非显而易见的实现定义),这可能从未发生过,也永远不会发生。 相反,您可以执行一些编译时测试,如果确实发生了这些情况,将会出错。 编译时断言INT_MAX < SIZE_MAX
在这个时代是实用的。