第二章:数据类型

第五节 无符号整数和有符号整数

首先我们编写一个下面的程序:


/*
冒牌程序员-毛哥
*/

/************unsigned.c***********/
#include<stdio.h>
int main()
{
    unsigned char   a;

    /*
	首先,可以使用运算符sizeof来查看某一个数据类型所占字节数。sizeof(type/variable)返回的数据的类型是啥
	呢?从占位符%ld来看,其应该是有符号的long类型的数据。
    */
    printf("unsigned char 类型所占字节数:%ld\n",sizeof(unsigned char));
    printf("unsigned char类型的变量a在内存中所占字节数:%ld\n",sizeof(a));
    /*
    以上两句话解决了unsigned char这个数据类型所占字节数。
    */

    a=100;
    /*
	可以在这一句话执行完毕后,查看a变量所在内存的二进制串。看看这个二进制串和无符号整数100之间的关系。根据其
	对应关系,可以得到unsigned char变量的取值范围。其取值范围是0-255。
    */

    /*
    有了取值范围,就可以看看这个数据类型是否是回环的,咋看呢?
    */
    a=255;  //调试的时候,这里打个断点,可以查看a=100执行后,a变量所在内存二进制串。
    a=a+1;
    printf("当a=255,取最大值以后,再给a加一,a = %hhu\n",a);
    /*
    所谓回环,其实就是一个圈,给最大值+1变成最小值,给最小值-1,就变成最大值
    */
    a=0;
    a=a-1;
    printf("当a=0,取最小值后,再给a减一,a=%hhu\n",a);
    return 0;
}

从上面的程序我们可以看出,unsigned char类型占一个字节。用-g选项来编译上面的这个程序,编译后用gdb来调试这个程序,在a=255行设置断点。然后当被调式程序暂停到这个断点后,可以查看a变量所在内存位置的二进制串(x/1tb)。我们查看到的二进制串为

冒牌程序员-毛哥 C语言入门教程(第二章:数据类型 第五节 无符号整数和有符号整数)_取值范围


我们把二进制串0b01100100转换成十进制的数就是100。这样我们就可以得出这样的结论:unsigned char类型的整数二进制串和其值的对应关系,就是二进制串所代表的这个无符号整数,就是遵守下面的公式:

冒牌程序员-毛哥 C语言入门教程(第二章:数据类型 第五节 无符号整数和有符号整数)_补码_02

这就是二进制串和char类型无符号整数之间的对应关系,是不是很无聊。

现在我们再来讨论其取值范围,按照上面的公式,最小值当然就是8位上,每一位都取0,根据公式,得到的值就是0。所以最小值就是0,最大值显然就是每一位都取最大值1,按照上面的公式,值就是255。

冒牌程序员-毛哥 C语言入门教程(第二章:数据类型 第五节 无符号整数和有符号整数)_无符号整数_03

这样,取值范围就确定了。


根据程序执行结果,还可以确定,unsigned char类型的无符号整数,是回环的。这种数据类型涉及的算法,就是我们小学学过的加减乘除四则运算。

冒牌程序员-毛哥 C语言入门教程(第二章:数据类型 第五节 无符号整数和有符号整数)_补码_04


这样,我们对unsigned char类型的无符号整数就整明白了。大家按照上述方法,对unsigned short,unsigned int,unsigned long,unsigned long long自行研究,可以得出以下结论:


数据类型

字节数

对应关系

取值范围

是否回环

unsigned char

1(8位)

二进制串到十进制的转换

0~255

unsigned short

2(16位)

二进制串到十进制的转换

0~2的16次方-1

unsigned int

4(32位)

二进制串到十进制的转换

0~2的32次方-1

unsigned long

8(64位)

二进制串到十进制的转换

0~2的64次方-1

unsigned long long

8(64位)

二进制串到十进制的转换

0~2的64次方-1


观察一下上表,我们会发现,每个类型取值范围最大值就是2的n次方然后减一(n为数据类型所占内存位数-1)。


以上是无符号整数,对应关系啥的比较简单,现在我们来看看有符号整数。


我们以int为例:


/******冒牌程序员-毛哥 int.c*/

#include<stdio.h>

int main()
{
    int x;

    /*先看看x所占字节数*/
    printf("size of x = %ld\n",sizeof(x));

    x=100;

    x=-100;//这个地方打个断点,查看100对应的二进制串

    /*在下一个语句上打断点,查看-100对应的二进制串*/
    /*
    此时,就可以验证所给的二进制串和有符号整数之间的对应关系了。

    有了这个关系,不难推算出int类型数据的取值范围。但是又一个很特殊的值,就是
    0x80000000,也就是只有最高位31位为1,其他位是0的一个二进制串,这个二进制串补码和源码还一样
    大家可以自己试一下。那么这个数到底是-0,还是其他数呢?我们必须去验证一下。

    我们取x=-(2的31次方-1),这个是按照上述规则,我们认为的最小的有符号int类型的数据
    */
    x=-(0x80000000-1);
    x--;
    /*
    暂停在这里,看看x在内存中的二进制串,就有结论了。
    */
    printf("看看这个时候,x的值,确定其是否是最小负数 x = %d\n",x);
    /*
        通过上一句,可以看出符号位位1,其他位为0的时候,代表最小的有符号数,也就是-2的31次方。
        这样我们就得到了int类型有符号整数的取值范围是-2的31次方到2的31次方-1。
        0只有正零,没有负零,负0被-2的31次方给霸占了。
    */

   /*
   此时再给x减一,那么看看他是多少
   */
    x--;
    printf("x = %d\n",x);
    /*结果说明有符号整数是回环的*/

   return 0;
}

将上述程序编译后(gcc int.c -o 1 -g),得到可执行文件1。对可执行文件进行调试(gdb 1)。然后在第x=-100这一行打上断点。查看此时x在内存中的值(x/1xw &x),结果如下:

冒牌程序员-毛哥 C语言入门教程(第二章:数据类型 第五节 无符号整数和有符号整数)_取值范围_05


根据结果,这不就是和无符号整数的二进制序列和值的对应关系吗,符合前述公式呀。我们再在x=-(0x80000000-1)这一行上打断点。查看内存中的二进制序列:

冒牌程序员-毛哥 C语言入门教程(第二章:数据类型 第五节 无符号整数和有符号整数)_补码_06


这回观察-100和二进制串,好像没啥对应关系了,我们一时也看不出-100和二进制串0xFFFFFF9C之间的关系。别急,我们看看下面的东西。

冒牌程序员-毛哥 C语言入门教程(第二章:数据类型 第五节 无符号整数和有符号整数)_补码_07


对于有符号整数,我们取出一位,也就是最高位来表示这个有符号整数的符号,这一位有两个取值,0和1,0表示正数,1表示负数。然后用剩下的位,来表示这个有符号整数的绝对值。比如,int类型的第31位,就是符号位,剩下的0-30位,就是一个无符号整数,这个无符号整数的值就是int类型有符号整数的绝对值。按照这个对照关系,100和0x00000064之间的对照关系没有问题,符合上述关系的描述。但是负数-100还是对不上。

下面我们要说一个新概念,补码的概念。一个数的补码就是,先对这个数求反,例如这个数是a,先求^a,然后再给这个数加一。其实就是: 结果=^a+1; 我们把这个结果,叫做数a的补码。

现在我们再来看-100,其对应的二进制串是:0xFFFFFF9C。我们现在把-100,按照31位是符号位,0-30位是其值绝对值位的方式,写成二进制形式如下:0x80000064。现在去掉符号位,变成0x00000064(注意这是一个31位的二进制串),对各位取反可以得到:0x7fffff9b,然后再给这个取反的结果加一,得到0x7fffff9c,这个时候再把31位的符号位合并进去,就可以得到0xffffff9c。此时再对照-100在内存中的二进制串,发现一样了。

总结,如上图,对于有符号正整数,其对应关系就是,符号位取0,其他位取这个有符号整数的绝对值。对于有符号负数,符号位取1,剩下其他位取其绝对值的补码。

最后,我们在看取值范围。还是以int为例。最大的正整数很好找,就是0x7FFFFFFF,其值为2的31次方-1。但是最小的负数就不好说了,好像是2的31次方-1,只不过是负的。现在又一个情况,就是绝对值为0,但是符号位可以位正或者负,这两数该怎么说,我们唯一可以肯定的是,这两中间必须又一个表示0这个数,我们现在规定,符号位是0,其他位为0,表示0这个值(其实这样是把0归到了正数的范围里面)。对于符号位位1,其他位位0的数,我们规定这个数是负的2的31次方。这样,取值范围就变成了负的2的31次方到正的2的31次方减一。可以参考上面程序的输出结果:


冒牌程序员-毛哥 C语言入门教程(第二章:数据类型 第五节 无符号整数和有符号整数)_取值范围_08


根据程序和输出结果,很容易判断,有符号整数也是回环的,也就是给最小的有符号整数减一,就变成了最大的有符号整数,最大的有符号整数加一,就变成最小的有符号整数了。

现在给大家一个思考问题,为什么有符号负数要使用补码?符号位加绝对值的办法不是挺好的吗?正数和负数的规则还一致,干嘛这么麻烦取补码?

大家可以试试把+100的二进制序列,和-100的二进制序列进行相加,看看结果,你会发现结果恰好是0。(有一个进位,进位舍去)。计算机的cpu中,加法其实比减法实现起来更方便,如果能通过加法,实现减法更好。现在好了,如果要求两个有符号整数相减的值,例如a-b的值,可以把b的符号位取反,其余位取其补码,这个时候b就变成了-b,然后可以通过计算a+(-b)的值,来替代a-b的值。问题解决。


总结:


数据类型

字节数

对应关系

取值范围

是否回环

char

1(8位)

补码

-2的7次方~2的7次方-1

short

2(16位)

补码

-2的15次方~2的15次方-1

int

4(32位)

补码

-2的31次方~2的31次方-1

long

8(64位)

补码

-2的63次方~2的63次方-1

long long

8(64位)

补码

-2的63次方~2的63次方-1