6

我知道这个问题已经被问过并且似乎回答了无数次,但我似乎无法将答案与我自己的经历相匹配。

C 标准规定,对于加法,“两个操作数都应具有算术类型”(6.5.6.1)。算术类型包括整数和浮点类型(6.2.5.18),最后整数类型是 char、short、int、long 和 long long,它们以有符号和无符号类型存在(6.2.5.4 和 6.2.5.6)。根据通常的算术转换规则“如果两个操作数具有相同的类型,则不需要进一步转换。” 到目前为止,一切都很好。

我的理解是,正如“The C Book”中所举例说明的那样,“[n]o 算术是由 C 以比 int 更短的精度完成的”,这是应用积分提升的地方。我在标准中找不到对此的参考,因为我似乎已经看过很多次了。

由于 unsigned char 是一种算术类型,并且通常算术转换的规则规定相同类型的操作数不需要转换,为什么需要整数提升?

我使用两种不同的编译器对此进行了测试。我写了一个简单的程序来添加字符:

unsigned char a = 1;
unsigned char b = 2;
unsigned char c = a + b;

目标平台是使用 8 位架构的 Atmel Mega8 uC。因此,如果操作数应进行整数提升,则整数加法将需要使用两个寄存器。

使用没有优化的 imagecraft avr 编译器编译它并启用严格和 ANSI C 可移植性选项会产生以下汇编代码:

mov R16, R20
add R16, R18

使用 avr-gcc(我不知道类似于 gcc 的 -strict 的 ANSI 开关):

$ avr-gcc -O0 -mmcu=atmega8 -S -c main.c

生成的程序集:

ldd r25,Y+1
ldd r24,Y+2
add r24,r25
std Y+3,r24

两种情况下的结果代码都在单个字节上运行。对于按位 | 我得到了类似的结果 和 & 和逻辑 || 和 &&。这是否意味着该标准允许在没有整体提升的情况下对字符类型进行算术运算,还是仅仅意味着这些编译器不符合标准?


额外的:

原来这一切都取决于存储结果的类型。上面显示的示例仅在结果存储在 char 时才成立,它不依赖于加法的结果。将 a 设置为 0xFF 并将 b 设置为 1 会产生完全相同的汇编代码。

如果 c 的类型更改为 unsigned int,则生成的程序集如下所示:

mov R2,R20
clr R3
mov R16,R18 
clr R17
add R16,R2 
adc R17,R3 

即使在结果可以保存在单个字节中的情况下,即a=1 和b=2。

4

5 回答 5

6

C 2011 (n1570) 6.3.1.8(“通常的算术转换”)1 指出在考虑类型是否相同之前执行整数提升:

否则,对两个操作数都执行整数提升。然后将以下规则应用于提升的操作数:

如果两个操作数的类型相同,则不需要进一步转换……</p>

因此,在 C 抽象机中,unsigned char值必须int在执行算术之前提升。unsigned char(对于和int具有相同大小的反常机器有一个例外。在这种情况下,unsigned char值被提升为unsigned int而不是int。这是深奥的,在正常情况下不需要考虑。)

在实际机器中,必须以与在抽象机器中执行操作相同的方式执行操作。因为只有结果很重要,所以实际的中间操作不需要与抽象机器完全匹配。

当将两个unsigned char值的总和分配给一个unsigned char对象时,总和将转换为 a unsigned char。这种转换实质上丢弃了超出unsigned char.

这意味着无论 C 实现是否这样做,都会得到相同的结果:

  • 将值转换为int.
  • int用算术添加值。
  • 将结果转换为unsigned char.

或这个:

  • unsigned char用算术添加值。

因为结果相同,所以 C 实现可以使用任何一种方法。

为了比较,我们可以考虑以下语句:int c = a + b;. 此外,假设编译器不知道 和 的ab。在这种情况下,使用unsigned char算术进行加法可能会产生与将值转换为int并使用int算术不同的结果。例如,如果a是 250 和b200,那么它们作为unsigned char值的总和是 194 (250 + 200 % 256),但它们的int算术总和是 450。因为存在差异,C 实现必须使用得到正确总和的指令, 450。

(如果编译器确实知道 and 的值,a或者b可以以其他方式证明总和适合 an unsigned char,那么编译器可以再次使用unsigned char算术。)

于 2012-10-11T14:22:40.403 回答
4

这是C99的相关部分:

6.3.1 算术操作数
6.3.1.1 布尔值、字符和整数
1 每个整数类型都有一个整数转换等级,定义如下:
...
2 在表达式中可以使用 int 或 unsigned int 的任何地方都可以使用以下内容:
—整数类型的对象或表达式,其整数转换等级小于 int 和 unsigned int 的等级。
— _Bool、int、signed int 或 unsigned int 类型的位域。
如果一个 int 可以表示原始类型的所有值,则将该值转换为 int;否则,它将转换为无符号整数。这些被称为整数促销。整数提升不会改变所有其他类型。

我同意它晦涩难懂,但这是您可以找到的最接近的各种charor shortor_Boolintor的转换unsigned int

来自同一来源:

5.1.2.3 程序执行
在抽象机中,所有表达式都按照语义的规定进行评估。如果一个实际的实现可以推断出它的值没有被使用并且没有产生所需的副作用(包括调用函数或访问易失性对象引起的任何副作用),则它不需要评估表达式的一部分。
...
10 示例 2 在执行片段
char c1, c2;
/* ... */
c1 = c1 + c2;
“整数提升”要求抽象机将每个变量的值提升为整数大小,然后将两个整数相加并截断总和。如果添加两个字符可以在没有溢出的情况下完成,或者溢出包装静默产生正确的结果,实际执行只需要产生相同的结果,可能会省略促销。

于 2012-10-11T14:53:23.020 回答
2

在 6.3.1.8(通常的算术转换,n1570)中,我们可以阅读

否则,对两个操作数都执行整数提升。然后将以下规则应用于提升的操作数:

所以整数提升是整数类型的常用算术转换的一部分。

因此,在抽象机器中,(unsigned) int必须进行转换。

但是根据“好像”规则,如果行为与严格实现抽象机的行为无法区分,则实现可能会做不同的事情。

因此,如果保证仅使用单个字节的计算与提升到 的计算具有相同的结果int,则允许实现使用单字节算术。

于 2012-10-11T14:05:19.413 回答
1

如果计算机能够对小于 的类型执行操作int,那么标准无论如何都不会阻止它。请记住,该标准试图为编译器保留尽可能多的选项,并将选择最佳方法的决定留给他们。

短语“C 不以比 int 更短的精度进行算术运算”也是正确的。如果您密切注意,您会发现算术确实以不小于int. 然而,这并不意味着编译器被迫进行整数提升,因为它可以安全地在示例程序中对字节执行操作并获得相同的精度。

于 2012-10-11T14:03:27.617 回答
0

我认为这里没有矛盾。编译器没有义务遵循任何特定的计算路径,只要可观察到的结果就好像它会遵循规定的方式。

特别是,对于您的情况,如果我们要通过提升到 int(例如,提升到 16 位)来进行计算:a提升到int具有相同的值,那么也是如此b。的值a + b实际上是(a + b) mod 2^16,但是我们将它分配给一个无符号字符,它会截断高 8 位,这与获取结果相同mod 2^8((a + b) mod 2^16) mod 2^8 = (a + b) mod 2^8

没有整数提升的计算将导致(a + b) mod 2^8,这是完全相同的。

于 2012-10-11T14:03:14.453 回答