C语言 为什么补码通过 printf 的行为不同



我正在阅读有关按位运算符的一章,我遇到了 1 的补码运算符程序,并决定在 Visual C++上运行它。

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

它给出有效的输出:251

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

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

它给出输出-5 .

为什么没有用?

在此语句中:

printf("%d",~c);

在应用~(按位补码(运算符之前,c将转换为 int 1 类型。这是因为整数提升,这些提升被调用到~的操作数。在这种情况下,unsigned char类型的对象被提升为(有符号的(int,然后(在~运算符计算之后(由printf函数使用,并匹配%d格式说明符。

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

另一方面,在此代码中:

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

将发生以下步骤:

  • 由于~c整数促销的对象(如上所述(
  • ~c 右值被评估为(有符号(int值(例如 -5 (
  • d=~cint 隐式转换为 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(。

char在第二个代码段中操作~之前printf语句中提升为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  

2 的补码形式以十进制-5

请注意,默认将char c提升为int

也在
d = ~c;

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

C11: 6.5.16.1 简单赋值 (p2(:

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

6.5.16(第3页(:

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

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

§ 6.5.3.3 一元算术运算符

  1. ~运算符的结果是其 (提升的(操作数(即,结果中的每个位都设置为 当且仅 如果未设置转换操作数中的相应位(。这 对操作数执行整数提升,结果具有 升级的类型。如果升级的类型是"无符号类型",则 表达式 ~E 等效于其中可表示的最大值 键入减号E"。

由于unsigned char类型比int类型窄(因为它需要的字节更少(,因此由抽象机器(编译器(执行的隐式类型提升和变量c的值在编译时(在应用补码操作之前(~被提升为int。它是正确执行程序所必需的,因为~需要一个整数操作数。

§ 6.5 表达式

  1. 一些运算符(一元运算符~二元运算符<<>>&^|, 统称为按位运算符(需要具有具有 整数类型。这些运算符生成的值取决于 整数,并且具有有符号类型的实现定义和未定义方面。

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

注意:

  1. c的值在表达式~c中提升为 int,但c的类型仍然unsigned char - 其类型不。 不要混淆。

  2. 重要提示:~操作的结果是int类型!,请检查以下代码(我没有 vs-编译器,我使用的是 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;
    }
    

    编译它,然后运行:

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

    注意~c 的结果大小与 int 相同,但不等于 unsigned char — 此表达式中~运算符的结果为 int ! 如前所述 6.5.3.3 一元算术运算符

    1. 一元-运算符的结果是其(提升的(操作数的负数。整数 升级对操作数执行,结果具有升级类型。

现在,正如@haccks在他的回答中也解释的那样 - 在 32 位机器上~c的结果和 c = 4 的值是:

1111 1111 1111 1111 1111 1111 1111 1011

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

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

    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在他的回答中建议的那样,使用显式强制转换。

当应用 ~ 运算符c它被提升为 int 时,结果也是一个int

然后

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

它给出了 op -5。 为什么它不起作用?

而不是:

printf("%d",~c);

用:

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

以获得与第一个示例中相同的结果。

~操作数经历整数提升,默认参数提升应用于可变参数函数的参数。

整数提升,来自标准:

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

最新更新