为什么我们在 C 中有无符号和有符号的 int 类型



我是C语言的初学者。我最近了解了2's Complement和其他表示负数的方法,以及为什么2's complement是最合适的方法。

我想问的是,例如,

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

现在,对于int类型的转换

该标准说:

6.3.1.3 有符号和无符号整数

当具有整数类型的值转换为 _Bool 以外的其他整数类型时,如果 该值可以用新类型表示,它保持不变。

否则,如果新类型是无符号的,则通过重复添加或 比新类型中可以表示的最大值多减 1 直到该值在新类型的范围内。

第一段不能使用,因为-3不能用unsigned int表示。

因此,第 2 段开始发挥作用,我们需要知道无符号 int 的最大值。它可以在limits.h中找到UINT_MAX。在这种情况下,最大值为4294967295,因此计算为:

-3 + UINT_MAX + 1 = -3 + 4294967295 + 1 = 4294967293  

现在二进制中的429496729311111111 11111111 11111111 11111101的,而 2 的补码形式的-311111111 11111111 11111111 11111101的,所以它们本质上是相同的位表示,无论我试图分配给无符号整数是什么负整数,它总是相同的。因此,无符号类型不是多余的。

现在我知道printf("%d" , b)根据标准是一种未定义的行为,但这不是一种合理和更直观的做事方式吗?因为如果负数表示为2's Complement,则表示将是相同的,这就是我们现在所拥有的,并且使用的其他方式很少见,并且很可能在未来的发展中不会出现。

因此,如果我们只能有一种类型说 int,现在如果int x = -1那么%d检查符号位并在符号位1时打印负数,并且%u总是按原样解释纯二进制数字(位)。由于使用2's complement,加法和减法已经处理。所以这不是更直观、更简单的做事方式吗?

输入、输出和计算都很方便。例如,比较和除法有有符号和无符号变体(顺便说一句,在位级乘法对于无符号和 2 的补码有符号类型是相同的,就像加法和减法一样,两者都可以编译成 CPU 的相同乘法指令)。此外,在溢出的情况下,无符号操作不会导致未定义的行为(除以零除外),而有符号操作会导致。总的来说,无符号算术是明确定义的,无符号类型只有一个表示形式(不像有符号类型的三种不同的表示形式,尽管现在在实践中只有一个)。

有一个有趣的转折。现代 C/C++ 编译器利用了签名溢出导致未定义行为的事实。逻辑是它永远不会发生,因此可以进行一些额外的优化。如果它真的发生了,标准说这是未定义的行为,你的错误程序在法律上被搞砸了。这意味着您应该避免签名溢出和所有其他形式的 UB。但是,有时您可以仔细编写永远不会产生 UB 的代码,但使用有符号算术比使用无符号算术更有效。

请研究未定义、未指定和实现定义的行为。它们都列在标准末尾的附件之一(J?

我的回答更抽象,在我看来,在 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
}

在我看来,在语言中拥有unsigned整数非常重要,没有 C 就不完整。

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

您已经观察到有符号和无符号类型的加/减行为相同,如果有符号类型使用 2 的赞美(并且您一直忽略了这个"如果"有时不是这种情况的事实。

在许多情况下,编译器需要签名信息来理解程序的用途。

1.整数推广。

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

例如,如果您将signed short转换为signed int并且intshort宽,编译器将生成执行转换的代码,并且该转换不同于"无符号短"到"有符号的int"(符号扩展与否)。

2. 算术右移

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

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......

那将是一种可怕的语言,不是吗?


奖励:所有指针通常具有相同的位表示形式,但具有不同的运算符[]->*++--+-

那么最简单和通用的答案是内存维护,当我们声明它时,C 语言中的每个变量都会在主内存 (RAM) 中保留一些内存空间,例如:unsigned int var;将保留2 or 4个字节,范围从0 to 65,5350 to 4,294,967,295.

虽然签名int的范围从-32,768 to 32,767-2,147,483,648 to 2,147,483,647.

关键是有时你只是正数,不能是负数,例如你的年龄显然它不能是负数,所以你会使用"无符号 int"。同样,在处理数字时,这些数字可以包含与我们将使用它signed int范围相同的负数。简而言之,一个好的编程实践是根据我们的需要使用适当的数据类型,这样我们就可以有效地使用计算机内存,我们的程序会更紧凑。

据我所知,2 补充了它关于特定数据类型或更具体的正确基础的所有内容。我们根本无法确定它是特定数字的 2 补码。但是由于计算机处理二进制文件,我们仍然以我们的方式拥有字节数,例如 2 的 7 位 8 位补码与 32 位和 64 位不同。

相关内容

  • 没有找到相关文章

最新更新