为什么我们在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之外的另一个整数类型时,如果该值可以由新类型表示,则它将保持不变。

否则,如果新类型是无符号的,则通过重复地添加或减去一个可以在新类型中表示的最大值来转换该值,直到该值在新类型的范围内。

第一段不能用作-3不能用unsigned int表示。

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

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

现在4294967293二进制是11111111 11111111 11111111 11111101-3 in 2的补码forms是11111111 11111111 11111111 11111101所以它们基本上是相同的位表示,无论我想要分配给无符号整数的负整数,它总是相同的。所以不是无符号类型冗余。

现在我知道printf("%d" , b)是根据标准的未定义行为,但这不是一个合理且更直观的方法。 如果否定被表示为2's Complement ,那么代表将是相同2's Complement ,这就是我们现在所拥有的,并且使用的其他方式很少,并且很可能不会在未来的发展中。

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

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

有一个有趣的转折。 现代C / C ++编译器利用了有符号溢出导致未定义行为的事实。 逻辑是它永远不会发生,因此可以进行一些额外的优化。 如果它确实发生了,标准说它是未定义的行为,你的错误程序是合法的搞砸了。 这意味着你应该避免签名溢出和所有其他forms的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更宽,则编译器将生成执行转换的代码,并且该转换不同于“unsigned short”到“signed int”(符号扩展与否)。

算术右移

-1>>1如果执行选择仍然可以为-1 ,但0xffffffffu>>1必须为0x7fffffffu

3.整数除法

类似地, -1/2 0xffffffffu/20x7fffffffu 0xffffffffu/20x7fffffffu

4. 32位乘以32位,结果为64位:

这有点难以解释,所以让我使用代码代替。

 #include  #include  #include  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.当然,比较。


可以设计一种无符号语言,但是很多运算符需要分成两个或更多版本,以便程序员可以表达程序的目的,例如运算符/需要拆分为udivudiv ,运算符*需要拆分为umulsmul ,整数提升需要明确,运算符>需要scmpgt / ucmpgt ………

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


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

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

signed int范围为-32,768 to 32,767-2,147,483,648 to 2,147,483,647

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

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