在C中,我看到很多代码向size_t
变量添加或分配整数文本。
size_t foo = 1;
foo += 1;
这里发生了什么转换,size_t
是否会"升级"为int
,然后再转换回size_t
?如果我在最高点的话,这还会发生吗?
size_t foo = SIZE_MAX;
foo += 1;
这是定义的行为吗?它是一个无符号类型size_t
,它添加了一个有符号的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 类型size_t可以被定义为类型unsigned short的同义词。类型无符号短可以定义为具有16个精度位,范围为:[065535]。在这种情况下,SIZE_MAX的值为65535。 类型int可以定义为具有16个精度位(加上一个符号位(、2的补码表示和范围:[-6553665535]。 表达式foo+=1等价于foo=foo+1(除了foo只计算一次,但这在这里无关紧要(。变量foo将使用整数升级3进行升级。它将被提升为int类型,因为int类型可以表示size_t类型的所有值,而size_t的rank作为无符号short的同义词,低于int的rank。由于size_t和int的最大值相同,计算会导致有符号溢出,导致未定义的行为。 这适用于当前标准,C89也应该适用,因为它对类型没有任何更严格的限制。 对于任何可以想象的实现,避免有符号溢出的解决方案是使用无符号整数常量: 在这种情况下,如果foo的秩低于int,则会使用常用的算术转换将其提升为无符号int。 1(引用自ISO/IEC 9899/201x 7.19通用定义2( 2(引用自ISO/IEC 9899/201x 7.20.3其他整数类型的极限2( 3(引用自ISO/IEC 9899/201x 6.3.1.1布尔值、字符和整数2(foo += 1u;
size_t
其是sizeof运算符的结果的无符号整数类型;
大小限制_t
SIZE_MAX 65535
以下内容可用于表达式中,只要int或unsigned int可以使用:
具有整数类型(int或无符号int除外(的对象或表达式其整数转换秩小于或等于int的秩,并且无符号整数
如果int可以表示原始类型的所有值(受宽度限制,对于位字段(,则该值被转换为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
实际上是其他无符号积分类型,也可以进行类似的分析。
注:在现代系统中,与int
大小相同或更小的size_t
是不常见的。然而,这样的系统已经存在(例如,微软和Borland C编译器针对80286 CPU硬件上的16位MS-DOS(。还有16位微处理器仍在生产中,主要用于具有较低功耗和低吞吐量要求的嵌入式系统,以及针对它们的C编译器(例如,针对Infeon XE166微处理器系列的Keil C166编译器(。[注意:我从来没有理由使用Keil编译器,但考虑到它的目标平台,如果它支持与该平台上的本机int
类型相同或更小的16位size_t
,也就不足为奇了]。
foo += 1
表示foo = foo + 1
。如果size_t
比int
窄(即,int
可以表示size_t
的所有值(,则在表达式foo + 1
中,foo
被提升为int
。
唯一可能溢出的方法是如果INT_MAX
==SIZE_MAX
。理论上这是可能的,例如16位int和15位size_t
。(后者可能有1个填充位(。
更有可能的是,SIZE_MAX
将小于INT_MAX
,因此由于超出范围的分配,代码将是实现定义的。通常,实现定义是"显而易见"的,高位被丢弃,因此结果将是0
。
作为一个实际的决定,我不建议为了满足这些情况(15位size_t
,或不明显的实现定义(而修改代码,这些情况可能从未发生过,也永远不会发生。相反,您可以进行一些编译时测试,如果这些情况确实发生,就会出现错误。编译时断言INT_MAX < SIZE_MAX
在当今时代是实用的。