我是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
现在二进制中的4294967293
是11111111 11111111 11111111 11111101
的,而 2 的补码形式的-3
是11111111 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
并且int
比short
宽,编译器将生成执行转换的代码,并且该转换不同于"无符号短"到"有符号的int"(符号扩展与否)。
2. 算术右移
如果实现选择,-1>>1
仍然可以-1
,但必须0xffffffffu>>1
0x7fffffffu
3. 整数除法
类似地,-1/2
是0
,0xffffffffu/2
是0x7fffffffu
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.当然,还有比较。
可以设计一种无符号语言,但是很多运算符需要拆分成两个或多个版本,以便程序员可以表达程序的目的,例如运算符/
需要拆分为udiv
和sdiv
,运算符*
需要拆分为umul
和smul
, 整数提升需要显式,运算符>
需要scmpgt
/ucmpgt
......
那将是一种可怕的语言,不是吗?
奖励:所有指针通常具有相同的位表示形式,但具有不同的运算符[]
、->
、*
、++
、--
、+
、-
。
那么最简单和通用的答案是内存维护,当我们声明它时,C 语言中的每个变量都会在主内存 (RAM) 中保留一些内存空间,例如:unsigned int var;
将保留2 or 4
个字节,范围从0 to 65,535
或0 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 位不同。