在C语言中如何对无符号整数进行加法运算

3

这里有两个非常简单的程序。我期望它们输出结果相同,但事实并非如此。第一个程序输出251,而第二个程序输出-5。我可以理解为什么会输出251,但我不明白第二个程序为什么会给我-5。

程序1:

#include <stdio.h>

int main()
{

unsigned char  a;
unsigned char  b;
unsigned int  c;

a = 0;
b= -5;

c =  (a + b);

printf("c hex: %x\n", c);
printf("c dec: %d\n",c);

}

输出:

c hex: fb
c dec: 251

程序2:

#include <stdio.h>

int main()
{

unsigned char  a;
unsigned char  b;
unsigned int  c;

a = 0;
b=  5;

c =  (a - b);

printf("c hex: %x\n", c);
printf("c dec: %d\n",c);

}

输出:

c hex: fffffffb
c dec: -5

3
你的两个程序都试图使用%d格式说明符打印一个 unsigned int 值,这是不合法的且行为未定义。为了有意义地在 printf 中输出一个无符号整数值,你需要使用 %u 或任何其他预期 unsigned int 参数的说明符。%x 是很好的选择,因为它预期 unsigned int 类型。但 %d 是完全不可接受的。这部分是你得到奇怪结果的原因之一。 - AnT stands with Russia
真的吗?仍然需要解释为什么我得到了我所得到的东西。 - user678392
2
正如AndreyT所解释的那样,这种行为是“未定义”的——这已经足够解释了。如果你研究二进制机器算术,你就可以弄清它为什么会以这种方式工作,但从技术上讲,你看到的结果并不是必需的——机器可以合法地打印“土豆”而不是“-5”,因为这种行为是“未定义”的。 - Hot Licks
@user678392:真的,真的。就像我说的,你的代码会产生未定义的行为。在所有有意义的情况下,它的行为基本上是随机的,或者最多是特定于实现的。虽然肯定可以想出一个确定性的解释来说明你得到了什么,但是这毫无用处。这是浪费时间。 - AnT stands with Russia
5个回答

12
在第一个程序中,b=-5;将251赋值给b。(转换为无符号类型时,总是将值对目标类型的最大值加一取模。)
在第二个程序中,b=5;只是将5赋值给b,然后c=(a-b);执行减法0-5作为类型int由于默认提升 - 简单地说,“小于int”类型总是在用作算术和位运算符的操作数之前被提升为int编辑: 我忽略了一件事:由于c的类型是unsigned int,第二个程序中的结果-5在分配给c时将被转换为unsigned int,导致UINT_MAX-4。这就是使用printf%x指示器看到的内容。当用%d打印c时,您会得到未定义的行为,因为%d需要一个(有符号)int参数,而您传递了一个带有一个不可表示为普通(有符号)int的值的unsigned int参数。

1
在汇编级别,问题可能是使用扩展符号位的指令执行了到 int 的提升,这就是为什么 0xfb 变成了 0xfffffffb。如果转换不使用扩展符号位,则会得到 0x000000fb,即十进制下的 251。 - aroth
@R...为什么第二个程序不直接执行(a + -(b),然后转换为C的数据类型呢?根据你的说法,编译器何时/在哪里执行数据类型转换似乎是任意的。 - user678392
转换何时/在哪里执行有些随意。在 C 中,系统允许在某些情况下选择性地将值“向上转换”为更宽的表示形式,只要不改变合法代码的行为就可以了。当您执行依赖于未定义行为的操作时,可能会得到奇怪的结果。但在这种情况下,问题并不在于 c=(a-b) 不同于 c=(a+-b),而是您根本没有编写第二个表达式,而是编写了 c = (a + 251) - Hot Licks
1
这一切都不是“任意的”,如果你指的是“由编译器决定”。它们在C语言规范中都被严格/准确地规定了。 - R.. GitHub STOP HELPING ICE
1
这个答案是正确的。在表达式 c = (a - b); 中,变量 a 的值被提升为类型 int;变量 b 的值也被提升为类型 int;减法运算被用于类型 int,得到结果 -5;然后 -5 被转换为 unsigned int,最终得到结果 UINT_MAX + 1 - 5 - caf
显示剩余3条评论

2

这里有两个不同的问题。第一个问题是,看起来相同的操作得到了不同的十六进制值。你所忽略的基本事实是,char被提升为int(同样适用于short),以进行算术运算。这是区别所在:

a = 0  //0x00
b = -5 //0xfb
c = (int)a + (int)b

在这里,a被扩展为0x00000000,而b被扩展为0x000000fb不是符号扩展,因为它是一个无符号字符)。然后进行加法运算,我们得到0x000000fb
a = 0  //0x00
b = 5  //0x05
c = (int)a - (int)b

在这里,a被扩展为0x00000000,而b被扩展为0x00000005。然后进行减法运算,得到0xfffffffb

解决方法是:只使用charint类型;混用它们可能会导致你意想不到的结果。

第二个问题是将一个无符号整数打印为-5,显然是有符号值。然而,在字符串中,你告诉printf打印它的第二个参数,解释为有符号整数(这就是"%d"的意思)。这里的诀窍是,printf不知道你传递的变量的类型。它仅按照字符串告诉它的方式来解释它们。以下是一个示例,我们告诉printf将指针作为整数打印:

int main()
{
    int a = 0;
    int *p = &a;
    printf("%d\n", p);
}

当我运行这个程序时,每次都会得到一个不同的值,这个值是 a 的内存位置转换成十进制的结果。你可能会注意到这种情况会引发警告。你应该阅读编译器给出的所有警告,并且只有在完全确定自己的意图时才可以忽略它们。


2
您正在使用格式说明符%d,它将参数视为带符号的十进制数(基本上是int)。
第一个程序中得到251是因为(unsigned char)-5等于251,然后您将其打印为带符号的十进制数字。它被提升为4个字节而不是1个字节,这些位都是0,所以该数字看起来像0000...251 (其中的251是二进制,我只是没有转换)。
第二个程序中得到-5是因为(unsigned int)-5是一些大的值,但如果强制转换为int,它就是-5。由于您使用printf的方式,它会被视为int类型。
使用格式说明符%ud来打印无符号十进制值。

为什么第二个程序使用了(unsigned int),而第一个程序使用了unsigned char? - user678392
@user 因为那是它们的类型。 - Seth Carnegie
卡内基?a和b都是无符号字符。c是一个无符号整数。我真的很困惑你从哪里获取你的数据类型。 - user678392

1

你所看到的是由于C标准定义了有符号类型转换为无符号类型(用于算术运算)以及底层机器如何表示数字(用于未定义行为结果)的结果。

当我最初回答时,我假设C标准没有明确定义有符号值应如何转换为无符号值,因为“标准没有定义有符号值应如何表示或在范围超出有符号类型时如何将无符号值转换为有符号值”。

然而,事实证明标准确实明确定义了从负有符号值转换为正无符号值的情况。对于整数而言,负有符号值x将被转换为UINT_MAX+1-x,就像它以二进制补码形式存储为有符号值,然后解释为无符号值一样。

因此,当你说:

unsigned char  a;
unsigned char  b;
unsigned int c;

a = 0; 
b = -5;
c = a + b;

由于C标准将-5转换为一个无符号类型的值UCHAR_MAX-5+1(255-5+1),因此b的值变为251。然后进行加法运算。这使得a+b与0 + 251相同,然后存储在c中。但是,当你说:

unsigned char  a;
unsigned char  b;
unsigned int c;

a = 0;
b = 5;
c = (a-b);

printf("c dec: %d\n", c);

在这种情况下,a和b被提升为无符号整数,以匹配c,因此它们的值保持为0和5。然而,在无符号整数运算中,0-5会导致下溢错误,这被定义为结果为UINT_MAX+1-5。如果这发生在提升之前,那么值将是UCHAR_MAX+1-5(即251)。
然而,你在输出中看到-5的原因是因为无符号整数UINT_MAX-4和-5具有完全相同的二进制表示,就像单字节数据类型中的-5和251一样,而且当你使用“%d”作为格式化字符串时,printf会将c的值解释为有符号整数而不是无符号整数。
由于从无符号值到有符号值的转换对于无效值没有定义,结果变成了实现特定的。在你的情况下,由于底层机器对于有符号值使用二进制补码,所以无符号值UINT_MAX-4变成了有符号值-5。

第一个程序之所以不会发生这种情况,是因为无符号整数和有符号整数都可以表示251,因此在两者之间进行转换是明确定义的,并且使用“%d”或“%u”没有关系。然而,在第二个程序中,它会导致未定义的行为并变成实现特定,因为您的UINT_MAX-4的值超出了有符号整数的范围。

底层发生了什么

始终要仔细检查您认为正在发生或应该发生的事情与实际发生的事情,因此让我们现在从编译器中查看汇编语言输出,以确切地了解正在发生的情况。以下是第一个程序的有意义部分:

    mov     BYTE PTR [rbp-1], 0   ; a becomes 0
    mov     BYTE PTR [rbp-2], -5  ; b becomes -5, which as an unsigned char is also 251
    movzx   edx, BYTE PTR [rbp-1] ; promote a by zero-extending to an unsigned int, which is now 0
    movzx   eax, BYTE PTR [rbp-2] ; promote b by zero-extending to an unsigned int which is now 251
    add     eax, edx  ; add a and b, that is, 0 and 251

请注意,尽管我们在字节b中存储了一个有符号值-5,但编译器在提升它时会通过零扩展数字来提升它,这意味着它被解释为11111011表示的无符号值而不是有符号值。然后将提升的值相加以成为c。这也是C标准定义有符号到无符号转换的方式--在使用二进制补码表示有符号值的体系结构上实现转换很容易。
现在看看程序2:
    mov     BYTE PTR [rbp-1], 0 ; a = 0
    mov     BYTE PTR [rbp-2], 5 ; b = 5
    movzx   edx, BYTE PTR [rbp-1] ; a is promoted to 32-bit integer with value 0
    movzx   eax, BYTE PTR [rbp-2] ; b is promoted to a 32-bit integer with value 5
    mov     ecx, edx 
    sub     ecx, eax ; a - b is now done as 32-bit integers resulting in -5, which is '4294967291' when interpreted as unsigned

我们可以看到在进行任何算术运算之前,a和b再次被提升为无符号整数,因此我们最终会得到两个无符号整数的差,由于下溢导致结果为UINT_MAX-4,这也等同于有符号值的-5。因此,无论您将其解释为有符号还是无符号减法,由于机器使用二进制补码形式,结果都与C标准匹配,不需要任何额外的转换。

在第一种情况下,它们仍然被提升。但是,在第一个程序中,值“-5”已经由于无符号字符赋值而转换为251,值为251的无符号字符在提升为无符号整数时仍然具有值251,因此a+b也是251。 - James O'Doherty
实际上,我可能在具体细节上有些偏差,因为我没有查看编译器生成的汇编代码,但归根结底,-5作为无符号字符变成了251,因为它们都是11111011,但当从无符号字符提升为无符号整数时,会转换为00000000 00000000 00000000 11111011,而当将-5提升为整数时,会从11111011变为11111111 11111111 11111111 11111011(在我的机器上),这是-5有符号和4294967291无符号。换句话说,一旦它变成无符号类型,提升就会扩展0而不是1。 - James O'Doherty
现在我有点困惑。当我计算 a - b 时,为什么结果不等于 0 +(251 的二进制表示)?如果相等的话,为什么会出现1的传递而不是0的? - user678392
首先进行类型提升。通过 a - b 运算,a 变成了 00000000 00000000 00000000 00000000,b 变成了 00000000 00000000 00000000 00000101。然后进行减法运算,由于下溢导致结果为 11111111 11111111 11111111 11111011 - James O'Doherty
这个答案从第一句话开始就是错误的。OP看到的是C语言要求的结果。实现细节可能显示特定实现如何实现与标准相匹配的行为,但它们无法解释为什么存在该行为。 - R.. GitHub STOP HELPING ICE
显示剩余8条评论

-1
将负数赋值给无符号变量基本上是违反规则的。你所做的是将负数转换为一个很大的正数。从技术上讲,你甚至不能保证在不同的处理器上转换是相同的——在补码系统(如果还存在的话)中,你会得到一个不同的值,例如。
所以你得到了你想要的结果。你不能指望有符号代数仍然适用。

把比特写出来,你就可以自己算出来。 - Hot Licks
我已经写完了这些比特。问题在于数据类型转换,而不仅仅是位模式。 - user678392
这个答案完全是错误的。将类型转换为无符号类型的结果始终是明确定义的;它是对目标类型的最大值加一取模后的余数,落在目标类型的范围内。 - R.. GitHub STOP HELPING ICE
@R.. -- 在一的补码机器上,这不是正确的。而且目标类型的范围也不是固定的。 - Hot Licks
@Daniel:你错了。有符号到无符号的转换不是位拷贝,而是转换。请参考手册。根据6.3.1.3:“如果新类型是无符号的,则通过反复添加或减去比新类型中可以表示的最大值多一个的值,直到该值在新类型的范围内为止。” - R.. GitHub STOP HELPING ICE
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接