为什么printf函数会导致补码的行为不同?

58

当我阅读关于位运算符的章节时,我遇到了1的补码运算符程序,并决定在Visual C++上运行它。

int main ()
{
   unsigned char c = 4, d;
   d = ~c;
   printf("%d\n", d);
}

它给出了有效的输出:251

然后,我决定直接打印~c的值,而不是使用变量d来保存~c的值。

int main ()
{
   unsigned char c=4;
   printf("%d\n", ~c);
}

它输出了-5

为什么它没有工作?


4
提示:数字“4”的补码等于数字“-5”的二进制补码表示。 - Vagish
3
这是“补码”(ones' complement)而不是“恭维”(compliment)。 - phuclv
2
@LưuVĩnhPhúc,你的补充值得赞扬。 - Déjà vu
6个回答

55
在这个语句中:
printf("%d",~c);

在应用按位取反(~)运算符之前,c被转换为int1类型。这是由于会针对~的操作数调用整型提升。在此情况下,unsigned char类型的对象被提升为(有符号的)int,然后被printf函数使用,匹配的格式说明符为%d

请注意,在这里,默认参数提升(因为printf是一个变参函数)并没有起到任何作用,因为对象已经是int类型了。

另一方面,在以下代码中:

unsigned char c = 4, d;
d = ~c;
printf("%d", d);

以下步骤发生:
  • 由于使用了 ~,c 会受到 整型提升(与上述描述方式相同)。
  • ~c 的 rvalue 被评估为 (signed) int 值(例如 -5)。
  • d=~c 隐式地将 int 转换为 unsigned char,因为 d 具有这种类型。可以将其视为 d = (unsigned char) ~c。请注意,d 不能为负数(对于所有无符号类型,这是一般规则)。
  • printf("%d", d); 调用 默认参数提升,因此将 d 转换为 int,并保留(非负)值(即 int 类型可以表示 unsigned char 类型的所有值)。

1) 假设 int 可以表示 unsigned char 的所有值(参见 T.C. 在下面的评论),但它很可能以这种方式发生。更具体地说,我们假设 INT_MAX >= UCHAR_MAX 成立。通常情况下,sizeof(int) > sizeof(unsigned char),字节由八位组成。否则,c 将转换为 unsigned int(如 C11 子条款 §6.3.1.1/p2 所述),并且格式说明符也应相应更改为 %u,以避免获得 UB(C11 §7.21.6.1/p9)。


9
但在d = ~c中也会发生这种情况。那么真正的区别在于,在d = ~c中发生了(反)转换为“无符号字符”,但在printf调用中没有发生此类转换。 - Marc van Leeuwen
@MarcvanLeeuwen:变量d的类型为unsigned char,因此它不能为负数(即使在转换为int后也不行,因为printf使用了默认参数提升)。实际上,在d=~c中,整数提升也已经发生了(就像我在上面的答案中描述的那样),但是赋值操作将int再次转换回unsigned char。另一方面,在第二种情况中,printf函数按原样获取int参数(即%d格式说明符是正确的),因此默认参数提升在这里没有任何作用。 - Grzegorz Szpetkowski
给其他读者:我更新了我的答案以反映Marc的观察。您可以在修订历史记录中找到原始(较短)答案。希望现在一切都清楚了。 - Grzegorz Szpetkowski
请注意,在某些系统中,unsigned char 可能会因为整数提升而被提升为 unsigned int(例如,如果 sizeof(int) == 1),这种情况下 printf 将会调用未定义的行为,因为它使用了错误的占位符。 - T.C.
@T.C.:你说得对,但是让我们假设(为了简单起见)我们生活在一个字节由八个比特组成的世界中。我知道标准没有强制执行它,但例如POSIX确实如此。 - Grzegorz Szpetkowski

27

printf语句中的char在第二个代码片段中的操作符~之前被提升为int。因此,c

0000 0100 (2's complement)  

转换为二进制(假定为32位机器)

0000 0000 0000 0000 0000 0000 0000 0100 // Say it is x  

它的按位补码等于该值的二进制补码减一 (~x = −x − 1)。

1111 1111 1111 1111 1111 1111 1111 1011  

在二进制补码中,-5用十进制表示。

请注意,在 char 类型的变量 c 提升为 int 时会发生默认提升。

d = ~c;

在补码操作之前,但结果被转换回unsigned char类型,因为dunsigned char类型。

C11: 6.5.16.1简单赋值(p2):

在简单赋值(=)中,右操作数的值将被转换为赋值表达式的类型,并替换左操作数指定的对象中存储的值。

6.5.16(p3):

赋值表达式的类型是左操作数经过左值转换后的类型。


17
要了解您的代码行为,您需要学习称为'整数提升'的概念(在对无符号字符操作数进行按位NOT操作之前,在您的代码中隐式发生)。如N1570委员会草案所述:

§ 6.5.3.3一元算术运算符

  1. ~运算符的结果是其(提升后的)操作数的按位补码(即,如果转换后的操作数中对应的位未设置,则结果中的每个位都设置)。对操作数执行整数提升,并使结果具有提升类型。如果提升类型是“ 'unsigned type'”,则表达式~E等效于该类型中可表示的最大值减去E“。
因为unsigned char类型比int类型窄(因为它需要更少的字节),所以在编译时(在应用补码操作~之前),抽象机器(编译器)执行隐式类型提升,并将变量c的值提升为int。这是程序正确执行所必需的,因为~需要一个整数操作数。 § 6.5表达式 4.一些运算符(一元运算符~和二元运算符<<>>&^|,统称为位运算符)需要具有整数类型的操作数。这些运算符产生的值取决于整数的内部表示,并且对于有符号类型具有实现定义和未定义的方面。

编译器足够聪明,能够分析表达式、检查语义、执行必要的类型检查和算术转换。这就是为什么在 char 类型上应用 ~ 时,我们不需要显式地编写 ~(int)c —— 这被称为显式类型转换(并且避免错误)。

注意:

  1. Value of c is promoted to int in expression ~c, but type of c is still unsigned char - its type does not. Don't be confused.

  2. Important: result of ~ operation is of int type!, check below code (I don't have vs-compiler, I am using gcc):

    #include<stdio.h>
    #include<stdlib.h>
    int main(void){
       unsigned char c = 4;
       printf(" sizeof(int) = %zu,\n sizeof(unsigned char) = %zu",
                sizeof(int),
                sizeof(unsigned char));
       printf("\n sizeof(~c) = %zu", sizeof(~c));        
       printf("\n");
       return EXIT_SUCCESS;
    }
    

    compile it, and run:

    $ gcc -std=gnu99 -Wall -pedantic x.c -o x
    $ ./x
    sizeof(int) = 4,
    sizeof(unsigned char) = 1
    sizeof(~c) = 4
    

    Notice: size of result of ~c is same as of int, but not equals to unsigned char — result of ~ operator in this expression is int! that as mentioned 6.5.3.3 Unary arithmetic operators

    1. The result of the unary - operator is the negative of its (promoted) operand. The integer promotions are performed on the operand, and the result has the promoted type.

现在,正如@haccks在他的answer中解释的那样,在32位机器上,对于c = 4的值,~c的结果是:

1111 1111 1111 1111 1111 1111 1111 1011

在十进制中,它是-5 ——这是你的第二个代码的输出!

在你的第一个代码中,还有一行很有趣需要理解b = ~c;,因为b是一个unsigned char变量,而~c的结果是int类型,所以为了容纳~c的结果值到b中,结果值(~c)被截断以适应unsigned char类型,如下所示:

    1111 1111 1111 1111 1111 1111 1111 1011  // -5 & 0xFF
 &  0000 0000 0000 0000 0000 0000 1111 1111  // - one byte      
    -------------------------------------------          
                                  1111 1011  
1111 1011的十进制等价物是251。你也可以使用以下方式获得相同的效果:
printf("\n ~c = %d", ~c  & 0xFF); 

或者按照@ouah在他的answer中建议,使用显式转换。


1
@haccks 谢谢,是的,我大多数时候来这里是为了学习而不是参与讨论,目前我主要从事Python、Django和Web开发方面的工作。 - Grijesh Chauhan
2
+int(PI/3) 为了引用相关规范部分,没有其他答案能够解释在这里为什么会发生类型提升!this->某些运算符(一元运算符 ~ 和二元运算符 <<、>>、&、^ 和 |,统称为位运算符)需要具有整数类型的操作数。今天让我感到非常开心。 - user719662
1
@vaxquis 谢谢,当我看到这篇文章时,我注意到大多数答案都在解释“结果不同”的原因,所以我决定添加我的答案并强调“为什么如此”的原因。-- 当你阅读我的答案时,我还缺少一份来自草稿的相关答案,现在已经添加了。 - Grijesh Chauhan

12

c应用于~运算符时,它会被提升为int类型,结果也是int

然后

  • 在第一个示例中,结果被转换为unsigned char,然后被提升为signed int并打印输出。
  • 在第二个示例中,结果作为signed int打印输出。

10

它给出了-5. 为什么它没起作用?

改为:

printf("%d",~c);

使用:

printf("%d", (unsigned char) ~c);
< p > 要获得与您第一个示例相同的结果。

~运算符会进行整数提升,对可变参数函数的参数进行默认参数提升。


8

整数提升,来自标准:

如果带有有符号整数类型的操作数的类型可以表示带有无符号整数类型的操作数的所有值,则带有无符号整数类型的操作数应转换为带有有符号整数类型的操作数的类型。


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