[深入浅出C语言]浅析整型

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第6天,点击查看活动详情

前言

        本文是讲解数据类型的第一篇,主要讲了整型的方方面面的知识,包括了整型介绍、原码反码及补码的介绍、整型溢出问题和大小端字节序问题,是以笔者的学习经验与心得为基础的,然而新手上路,文章拙劣而纰漏难免,欢迎指正,希望于你有益。


前置知识

类型的意义

        虽然我们这里要讲的是整型,可是你有没有想过数据类型到底有怎样的意义?

        在继续讨论之前,我有一个问题:为什么要根据类型,开辟一块空间,直接将内存整体使用不好吗?

        不好。

        任何时刻,都不是你一个程序在运行,还有很多其他程序也在运行。你整块用了,让别人怎么办?

        另外,你全都用了,一定需要在任何时刻,全部都用完吗?对于暂时不用,但是给你了,对计算机来讲,就是浪费。

        C中为何要有类型?本质上对内存进行合理化划分,按需索取

        那么,问题又来了,我使用部分内存,使用多少由什么决定?其实是由你的场景决定,你的计算场景,决定了你使用什么类型的变量进行计算。你所使用的类型,决定了你开辟多少字节的空间大小。

        所以,C语言中,为什么会有这么多的类型?就是为了满足不同的计算场景

        比如,整形计算,字符计算,字符串计算,浮点数计算等。

        本质:用最小成本,解决各种多样化的场景下的问题

同时,类型还具有如下意义:

  1. 决定了开辟内存空间的大小(大小决定了使用范围)。

  2. 决定了如何看待内存空间的视角。

比如:定义了一个int型变量a,那么往后对a的存和取都是以int的标准去看待的。

关于位、字节和字

        位(bit)是最小的存储单元,即一个二进制位,仅能存储0或1。

        字节(byte)是常用的存储单元,基本上大部分机器1字节均为8位,至少在衡量存储单位的时候是这样的。

        字(word)是设计计算机时给定的自然存储单位,目前的个人计算机有1字长为32位的,计算机字长越大,其数据转移就越快,允许的内存访问也更多。

整型讲解

int类型

        是有符号整型,必须为整数。

        取值范围与计算机系统有关,一般而言,存储一个int要占用一个机器字长,16位的机器就是用两个字节来存储int,取值范围就是-32768~32767。而目前个人计算机一般是32位的,也就是用4个字节存储int。

变量声明

        类型关键字 + 变量名;(其他变量声明都是这样)

        int a;

        int b, c, d, e;

        可以单独声明,也可以同时声明多个变量。

变量命名

       名字必须是由字母,数字或下划线组成,不能有特殊字符,同时也不能以数字开头。

       同时变量名不能是关键字。

       如:int double = 10.0;(错)

       int 2people = 100;(错)

       int _2peo = 100;(对)

       变量获得值,有三种途径,一是赋值,如a = 10; ,二是通过函数获得值,如scanf(),三是初始化获得值,可以直接在声明中完成,如int weight  = 100; ,不过要注意不要把未初始化和初始化变量搁在一块,容易误会,如int q, p =100; 。

关于进制的显示

转换说明

十进制--%d

八进制--%o

十六进制--%x

       若要显示八进制和十六进制的前缀0,0x和0X,必须分别使用%#o,%#x,%#X。

其他整型

        当int无法满足各种需求的时候,就需要其他整型了。

        C语言提供3个附属关键字修饰基本整数类型int,有short、long和unsigned(无符号即非负数)。

        为了适应不同的机器,C语言只规定了short占用内存不能多于int,long占用内存不能少于int,而int又与机器字长有关。现在大多都以16或32位存储int(依计算机自然字长定),32位存储long,16位存储short,为存储64位的整数引入了long long类型。

        C标准只对基本数据类型规定了允许的最小大小。

        对于16位机,short和int的最小大小是16位。对于32位机,long的最小大小是32位。

        unsigned short和unsigned int最小是16位,unsigned long最小是32位,而long long和unsigned long long就是64位。

这也太多了吧,到底该如何选择整型来使用呢?

        对于unsigned类型,常用于计数,因为计数一般是非负数,同时unsigned可以表示更大的正数。

        对于那些long占用内存大于int的系统,一般超出int而在long范围内的数用long类型,但是使用long会减慢运算速度,所以需要斟酌一下,如非必要尽量不用long。

        对于那些long占用内存与int相同的系统,当需要用到32位的整数时使用long可能会更好,能提高可移植性,即使把程序从32位机移植到16位机后仍然可以正常工作。

        为什么呢?因为16位机的long是32位的,int是16位的,原程序若选用int存储32位的整数,移植之后会溢出,而选用long则刚好接收,不用担心溢出。

        类似地,如果是确实需要用到64位的整数才考虑用long long,不然会拖慢运行速度。

        对于常量,通常程序代码中的数字都会被存储为int类型,如果超出了int范围的话编译器会将其视为long int ,再超出就视为unsigned long,要是再超出就视为long long甚至是unsigned long long。

        八进制和十六进制常量若超出范围则是这样:int-->unsigned int-->long-->unsigned long-->long long-->unsigned long long。

        要是有某些特殊需求要把一些较小常量用long存储呢?可以在值的末尾加上后缀l(小写L)或L。

        比如0x55L,100L。LL就是long long后缀,再加上个u或U就是unsigned long long后缀了。

image.png

原码、反码和补码

        有符号整数在计算机中怎么存储与计算呢?我们知道计算机是用二进制数存储一切数据,那怎么去用二进制数表示负数呢?

        前人想到了取最前面的一位作为符号位来规定正负数,我们下面拿一个字节来举例说明

image.png

原码:第七位作为符号位,当该位值为1时表示负数,为0时表示非负数,这就是原码。

        比如:-100原码

image.png

反码: 非负数反码与原码相同,负数反码为原码除了符号位以外按位取反。

        按位取反就是对于单独的一个位的值,如果原来是0就变成1,原来是1就变成0。

        比如:-100的反码

image.png

补码 :非负数补码与原码相同,负数补码为反码+1。

        比如:-100的补码

image.png

计算机存储和运算用的是补码

        因为补码利于计算机准确计算,而原码便于我们识别数值。

        在计算机系统中,数值一律用补码来表示和存储。

        原因在于,使用补码,可以将符号位和数值域统一处理。

        同时,加法和减法也可以统一处理(CPU只有加法器)。此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。

        以一个字节范围来举例:

        计算5+6

image.png

        计算-5+6,溢出的第九位去掉,只取八位

image.png

以上就是原反补码的简单介绍了,接下来我们继续深入了解。

整数溢出问题

引出问题

        实际上溢出行为是C标准未定义行为,不同编译器可能状况不一样,就拿VS来研究一下。

        在VS下unsigned int,int和long都是四字节,short是两字节。

        无论上网一搜还是课本一查,都发现发现int范围为-2148483648~2147483647 ,而unsigned int范围是0~4294967295 ,那你有没有想过为什么有符号整数正负范围不对称?

        看看下面演示代码,思考一下为什么是这样。

image.png

在VS下,int溢出以后从最小值-2147483648开始,分析一下:

image.png

        你可能会觉得有点不对劲,按照2147483647 + 1后的补码逆推一下原码会得到0000000000000000000000000000000,那不就是0了吗??

        欸等会儿,0的原码也是这个呀,那为什么printf函数打印a+1得到的却是-2147483648?

剖析因果

        我们一点一点来讲,话说在还没有出现反码和补码的时候,人们发现用原码存储和计算会出现一些奇怪的结果。

        这里以下都用一个字节来演示。

        比如2-1,计算机没有减法这一概念,在执行算术减法时会用加法来计算,即2+(-1),则有

image.png

        得到结果居然是-3!

        多少有点离谱了,要是只用原码一遇到负数或减法就得出大问题。

        所以大伙就说用反码试试吧,那好咱就来试试

        还是2+(-1)

image.png

        不断往前进1得到了九位二进制数,但是我们只要八位,多出的一位舍去,就得到了1。

        这下不就对上了嘛,计算无误。

        那是不是这样就解决一切问题了呢?我们来看看下面这个例子

        对于1+(-1),得到的11111111取原码就是10000000。

image.png

        小伙汁,你看看原码10000000是什么数?是不是想说-0?是不是有点不对劲,怎么会有-0这种东西呢?0既不是负数也不是正数,况且原码00000000已经能表示0了,难不成来个+0=-0=0吗?滑天下之大稽!

        后来大伙捣鼓来捣鼓去,最终搞出了补码,那我们试试补码能不能解决问题。

        对于1+(-1),得到九位二进制数,只要八位,舍去第九位,得到00000000就是0,这不就成了嘛。

image.png

        可是原码10000000的数到底是啥?emm,反正不能是-0,实际上我们这里只能由果溯因。

        应该问一下为什么一个字节范围是-128~127 而不是-127~127 。首先-127~127 一共有多少个数?答案是255个,而八位二进制数最多能表示256个十进制数,这不就漏了一个嘛,我们看一下:

image.png

        发现了没有?漏了哪一个呢?对,就是10000000,对它按位取反得11111111,再加1得1 00000000,就变成了九位,只要八位,去掉第九位就得到00000000即它的补码。

        现在我们看看-128如何,原码应当为1 10000000有九位,第九位作为符号位,按位取反得1 01111111,再加1得1 10000000发现补码与原码相同。一个字节只要八位,原码去掉第九位就得到了10000000对吧,这不就对上了吗。

        1000 0000  和-128原码丢弃最高位后余下的8位相同,而且即使去掉第九位后的-128和一个字节范围内的其他数(-127~127)运算也不会影响结果, 所以才敢这么表示-128。

        也就是说-128八位原码与补码都为10000000

        比如:-128 + 125 = -3

image.png

        那么127+1结果会是多少呢?答案是-128!01111111+00000001=10000000不就是前面讲了这么一长串的-128嘛。

        这也就意味之数值溢出后从最小值重新记起

回到int范围的问题上来

        其实原理上差不多,意思就是达到最大正数值后再+1数值溢出了呗,具体参照先前讲的内容就行了。

image.png

        这样的话上面一行printf()的输出就弄清楚了,那下面一行的printf()输出为什么会是-1,0,1呢? 

        问题出在哪呢?

        对,b是无符号整型而使用了%d,导致出现意外结果。

        我们结合前面讲的原反补码来分析一下为什么会这样。

        首先提一下前面没讲到的,无符号整型没有所谓所谓原反补码之说,最高位不用来作符号位,所以可以表示的正数值范围更大,无符号整型对应的二进制数就是存在计算机里的二进制数,不需要经过原码到补码的变化过程。相对而言,有符号整型存在计算机里和用于参与计算的都是补码。

        有符号整型int最大正数值是2147483647 < 4294967295,而%d就是告诉计算机我要输出的类型是有符号整型,最高位看成符号位,那好,上图。

image.png

        也就是说当成有符号类型的话,4294967295对应二进制数看成补码,求出的原码对应的值就是-1,所以说会输出-1。

        那b+1输出0,b+2输出1就同理了吧。

        其实要用printf的话最好就把转换说明(比如%d之类的)和数据类型对应好,不要使用不匹配的转换说明,否则可能会出大问题。

        在使用%u后输出数值就正常了。

image.png

大小端字节序问题

字节序

         字节序,简而言之,就是以字节为单位数据在内存中的存放顺序。

         要注意的是,数据在内存中是以二进制形式存储的,在显示的时候一般都以十六进制形式显示,我们后面讲解时都是用十六进制显示。

什么叫大端小端

        大端(存储)模式,是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中。

        小端(存储)模式,是指数据的低位保存在内存的低地址中,而数据的高位,,保存在内存的高地址中。

         为什么要叫这个名字呢?据说当时的命名者根据《格列夫游记》里两个党派关于吃鸡蛋时是从大头敲开还是从小头敲开发生不小的争执的故事而取名的,其实这样命名也比较形象,高位看成大头端,低位看成小头端,则可以有:

image.png

         浮点型也有大小端字节序问题,其实只要是占用内存超过两个字节的数据类型都有大小端字节序问题。

为什么会有大端和小端

        为什么会有大小端模式之分呢?这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为8 bit。但是在C语言中除了8 bit的char之外,还有16 bit的short 型,32 bit的long型(要看具体的编译器),是不是都超过了一个字节?在内存中以怎样的顺序存储,最起码都有两种可能的顺序,另外,对于位数大于8位的处理器,例如16位或者32 位的处理器,由于寄存器宽度不大于一个字节,那么必然存在如何将多个字节安排的问题

        因此就导致了大端存储模式和小端存储模式。 例如:一个 16bit 的 short 型 x ,在内存中的地址为 0x0010 , x 的值为 0x1122 ,那么 0x11 为 高字节,0x22 为低字节。对于大端模式,就将 0x11 放在低地址中,即 0x0010 中, 0x22 放在高 地址中,即 0x0011 中。小端模式,刚好相反。

        我们常用的 X86 结构是小端模式,而 KEIL C51 则 为大端模式。很多的ARM,DSP都为小端模式。有些ARM处理器还可以由硬件来选择是大端模式 还是小端模式。具体是大端还是小端我们可以设计小程序测试一下。

如何测试大小端

         将1存入四个字节的空间(int类型),我们只要取出第一个字节存储的值,看看是不是1就行了。

image.png

实现代码

#include <stdio.h>

int check_sys()
{
     int i = 1;
     return (*(char *)&i);//把类型转换成char*后解引用只读取第一个字节的值
}                     //要是小端就返回1,大端就返回0

int main()
{
     int ret = check_sys();
     if(ret == 1)
     {
         printf("小端\n");
     }
     else
     {
         printf("大端\n");
     }
     return 0;
}

复制代码

以上就是本文的全部内容了,感谢观看,你的支持就是对我最大的鼓励~

v2-f8717db0beb4aa82fb4210753c816045_720w.jpg

猜你喜欢

转载自juejin.im/post/7130437623625023495