为什么C语言中会有无符号和有符号的整数类型?

5

我是C语言的初学者。最近我学习了“二进制补码”以及其他表示负数的方法,知道为什么“二进制补码”是最合适的。

我想问的是,举个例子:

int a = -3;
unsigned int b = -3; //This is the interesting Part.

现在,针对 int 类型的转换,
标准规定如下:
6.3.1.3 有符号和无符号整数
当将具有整数类型的值转换为除 _Bool 以外的另一种整数类型时,如果该值可以由新类型表示,则保持不变。
否则,如果新类型是无符号的,则通过反复加上或减去比新类型中最大可表示值多一的值,直到该值在新类型的范围内为止进行转换。
第一段不能用于 -3,因为它无法被 unsigned int 表示。
因此,第二段发挥作用,我们需要知道 unsigned int 的最大值。它可以在 limits.h 中找到,称为 UINT_MAX。在这种情况下,最大值为 4294967295,因此计算如下:
-3 + UINT_MAX + 1 = -3 + 4294967295 + 1 = 4294967293  

现在,4294967293的二进制表示是11111111 11111111 11111111 11111101,而以2的补码形式表示的-3的二进制表示也是11111111 11111111 11111111 11111101,因此它们本质上是相同的位表示,无论我尝试把什么负整数赋给无符号整数,它们始终是相同的。所以无符号类型不是多余的。
现在我知道,根据标准,printf("%d" , b)是未定义的行为,但这不是更合理和更直观的做法吗?如果负数以2的补码表示,则表示将始终相同,这是我们现在所拥有的,而其他使用的方法很少,并且很可能在将来的开发中不会出现。
所以如果我们只能有一种类型,比如int,那么如果int x = -1,那么%d检查符号位并打印负数,如果符号位为1%u则始终将纯二进制数字(位)解释为它是。加法和减法已经处理了,因为使用了2的补码。所以这不是更直观和简单的做法吗?

2
标准仅描述了当将无符号数字解释为有符号数字时,99.9%的现代CPU表现出的行为。因此,是的,大多数机器都遵循标准。但是,由于这个原因,有符号和无符号并不是冗余的 - tofro
@tofro,您能详细说明一下吗?我不太理解您的意思。 - user7359847
2
例如,在比较时,了解数字是有符号还是无符号的解释非常重要。 - Ctx
4个回答

7
拥有有符号和无符号类型对于输入、输出和计算都很方便。例如,比较和除法有带符号和无符号的变体(顺便说一下,在位级别上,乘法对于无符号和二进制补码有符号类型是相同的,就像加法和减法一样,两者都可能编译成CPU的相同乘法指令)。此外,无符号操作在溢出时不会导致未定义行为(除以零除外),而有符号操作会导致未定义行为。总的来说,无符号算术是明确定义的,并且无符号类型具有单个表示(与三种不同的有符号类型不同,尽管现在实际上只有一种)。
有一个有趣的细节。现代C/C++编译器利用了有符号溢出会导致未定义行为这一事实。逻辑是它永远不会发生,因此可以进行一些额外的优化。如果实际上确实发生了,标准规定它是未定义行为,您的程序将合法地失败。这意味着您应该避免有符号溢出和所有其他形式的未定义行为。但是,有时您可以仔细编写代码,从而不会导致未定义行为,但使用有符号算术比使用无符号算术更有效率。
请研究未定义行为、未指定行为和实现定义行为。它们都列在标准的附录(J?)中。

也可以参考这个链接:http://stackoverflow.com/questions/41406570/how-is-int-type-changed-to-signed-and-unsigned-type-and-what-is-the-rationale-be - user7359847
1
@Stranger 问题在于,如果问题显示出严重的知识缺口,一个好的答案很可能会过于复杂或难以理解,并需要许多后续问题。这样的问题对于SO来说并不好。 - Alexey Frunze
1
@EML 我非常确定的一点是,在你将 0001 乘以 1000 的例子中,C 会非常明确地指出结果应该为 1000,而不是 xxxx1000。硬件乘法器生成了更多的乘积位是无关紧要的,因为从 C 标准的角度来看,这些位不存在。正是因为前半部分位数基本上被丢弃了,所以 C 允许实现将有符号和无符号的乘法视为相同的东西(只要编译器在此之前有效地遵循所有的“标准算术转换”规则)。 - mtraceur
@mtraceur - AF的回答说:“顺便说一句,在位级别上,无符号和二进制补码有符号类型的乘法是相同的,就像加法和减法一样。”我的评论是委婉地说“这是错误的”。在“位级别”上,它们非常不同。在位级别上,加法和减法是相同的;区别在于解释。这在乘法中并不正确。我举例的目的是为了证明这一点。我没有提到C语言;那是AF的解释,而不是承认位级别乘法的差异。 - EML
@EML 我让你做这个实验,你觉得怎么样? - Alexey Frunze
显示剩余12条评论

3
我的答案更抽象,我认为在C语言中你不需要关心整数在内存中的表示方式。C语言将这个问题抽象出来,这是非常好的。
unsigned声明一个整数是非常有用的。它假设该值永远不会为负数。就像浮点数处理实数一样,signed整数处理整数,而unsigned整数处理自然数。
当您创建算法时,其中负整数会导致未定义行为。您可以确信您的无符号整数值永远不会为负数。例如,当您迭代数组的索引时。负索引将导致未定义行为。
另外一件事是,当您创建公共API时,其中一个函数需要大小、长度、重量或任何不能为负数的值。这有助于用户了解该值的目的。
另一方面,一些人持不同意见,因为unsigned的算术运算不像人们最初预期的那样工作。因为当unsigned等于零时,减少后它将变为非常大的值。有些人期望它将等于-1。例如:
// wrong
for (size_t i = n - 1; i >= 0; i--) {
  // important stuff
}

这会产生一个无限循环,如果n等于零会更糟,编译器可能会检测到,但不是所有时间。
// wrong
size_t min = 0;
for (size_t i = n - 1; i >= min; i--) {
  // important stuff
}

使用无符号整数需要一些技巧:

size_t i = n;
while (i-- > 0) {
  // important stuff
}

在我看来,语言中拥有无符号整数类型是非常重要的,如果C语言没有它将不完整。


直到你需要与外部世界交流或需要一种快速完成语言不直接支持但硬件支持的操作时,你才会关心类型表示。 - Alexey Frunze

2

我认为一个主要原因是运算符和操作依赖于有符号性。

如果有符号类型使用二进制补码(并且您忽略了“如果”有时不成立的事实),则您已经观察到有符号和无符号类型的加法/减法行为相同。

在许多情况下,编译器需要有符号信息才能理解程序的目的。

1. 整数提升。

当较窄的类型转换为较宽的类型时,编译器将根据操作数的类型生成代码。

例如,如果您将“signed short”转换为“signed int”,并且“int”比“short”宽,则编译器会生成执行转换的代码,而该转换与“unsigned short”到“signed int”(符号扩展或否)不同。

2. 算术右移

如果实现选择,-1>>1仍然可以是-1,但0xffffffffu>>1必须是0x7fffffffu

3. 整数除法

类似地,-1/200xffffffffu/20x7fffffffu

4. 32位乘以32位,得到64位结果:

这有点难以解释,所以让我用代码来说明。

#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>

int main(void) {
    // your code goes here
    int32_t a=-1;
    int32_t b=-1;
    int64_t c = (int64_t)a * b;
    printf("signed: 0x%016"PRIx64"\n", (uint64_t)c);

    uint32_t d=(uint32_t)-1;
    uint32_t e=(uint32_t)-1;
    uint64_t f = (uint64_t)d * e;
    printf("unsigned: 0x%016"PRIx64"\n", f);

    return 0;
}

示例:http://ideone.com/k30nZ9

5. 当然,还有比较。


可以设计一种无符号/有符号混合语言,但是这样就需要将许多运算符分成两个或更多版本,以便程序员可以表达程序的目的,例如运算符/需要分为udivsdiv,运算符*需要分为umulsmul,整数提升需要显式,运算符>需要是scmpgt/ucmpgt……

这将是一个非常难用的语言,不是吗?


额外福利:所有指针通常具有相同的位表示,但具有不同的运算符[]->*++--+-


0

最简单和通用的答案是内存维护,C语言中的每个变量在声明时都会在主内存(RAM)中保留一些内存空间,例如: unsigned int var; 将保留2或4字节,并且范围为0到65,5350到4,294,967,295

而有符号的int将具有范围从-32,768到32,767-2,147,483,648到2,147,483,647

重点是有时您只需要正数,这些数字不能为负数,例如您的年龄显然不能为负数,因此您将使用'unsigned int'。同样,在处理可以包含与signed int相同范围的负数的数字时,我们将使用它。简而言之,良好的编程实践是根据我们的需求使用适当的数据类型,以便我们可以有效地使用计算机内存,使我们的程序更加紧凑。

据我所知,关于二进制补码,它与特定的数据类型或更具体地说是正确的基数有关。我们无法确定它是否是特定数字的二进制补码。但由于计算机处理二进制,例如在8位、32位和64位中,7的二进制补码是不同的,因为我们仍然需要考虑字节数。

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