char和通常的算术转换规则

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

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

我的理解,如“C书”中所示,“[n] o算术由C以比int短的精度完成”,这是应用积分推广的地方。 我似乎没有在标准中找到这个参考,我好像已经看过很多次了。

由于unsigned char是算术类型,并且通常算术转换的规则表明相同类型的操作数不需要转换,为什么需要进行积分提升?

我使用两个不同的编译器测试了这个。 我写了一个简单的程序,它添加了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 

两种情况下的结果代码都在单个字节上运行。 我得到类似的结果| 和&和逻辑|| 和&&。 这是否意味着该标准允许对charecter类型进行算术运算而不进行整体提升,或者它只是意味着这些编译器不是标准的兼容性?


额外:

事实certificate,这一切都取决于存储结果的类型。上面显示的示例仅在结果存储在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。

C 2011(n1570)6.3.1.8(“常用算术转换”)1表示考虑类型是否相同之前执行整数提升:

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

如果两个操作数具有相同的类型,则不需要进一步转换…

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

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

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

这意味着无论是否执行此操作,C实现都会得到相同的结果:

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

或这个:

  • 使用unsigned char算术添加值。

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

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

(如果编译器确实知道ab的值,或者可以certificate总和适合unsigned char ,那么编译器可以再次使用unsigned char算法。)

这是C99的相关部分:

6.3.1算术运算数
6.3.1.1布尔,字符和整数
1每个整数类型都有一个整数转换等级,定义如下:

2可以在任何可以使用int或unsigned int的表达式中使用以下内容:
– 具有整数类型的对象或表达式,其整数转换等级小于int和unsigned int的等级。
– _Bool,int,signed int或unsigned int类型的位字段。
如果int可以表示原始类型的所有值,则该值将转换为int; 否则,它将转换为unsigned int。 这些被称为整数促销。 整数促销不会更改所有其他类型。

我同意它是模糊的,但这是你可以找到最接近各种charshort_Boolintunsigned int

来自同一来源:

5.1.2.3程序执行
在抽象机器中,所有表达式都按语义指定进行计算。 实际实现不需要评估表达式的一部分,如果它可以推断出它的值未被使用并且不产生所需的副作用(包括由调用函数或访问易失性对象引起的任何副作用)。

10示例2执行片段
char c1,c2;
/ * …… * /
c1 = c1 + c2;
”整数提升”要求抽象机将每个变量的值提升为int大小,然后添加两个整数并截断总和。 如果可以在没有溢出的情况下添加两个字符,或者以静默方式溢出包装以产生正确的结果,则实际执行仅需要产生相同的结果,可能省略促销。

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

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

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

因此,在abstratc机器中,必须完成到(unsigned) int的转换。

但是,通过“似乎”规则,如果行为与严格实现抽象机器的行为无法区分,那么实现可能会以不同的方式执行。

因此,如果保证仅使用单个字节的计算与向int推进的计算具有相同的结果,则允许实现使用单字节算法。

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

短语“没有算术由C以比int短的精度完成”也是正确的。 如果你密切关注,你会发现算术确实以不小于int的精度完成。 但这并不意味着编译器被强制执行整数提升,因为它可以在示例程序中安全地执行字节操作并获得相同的精度。

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

特别是,对于你的情况,如果我们用升级到int(例如,到16位)进行计算:提升到int具有相同的值, b也是如此。 a + b值实际上是(a + b) mod 2^16 ,但是我们将它分配给unsigned char,它截取高8位,这与取结果mod 2^8((a + b) mod 2^16) mod 2^8 = (a + b) mod 2^8

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