(C语言之路—–p7:数据在内存中的储存)

( 一 )有哪些数据类型

在前面的学习中我们已经了解了基本的内置类型,并知道了不同的内置类型的大小

char         //1byte

short        //2byte

int            //4byte

long         //4byte

long long //8byte

float         //4byte

double     //8byte 

这里有两点要注意的,就是int long类型的大小.C语言标准设定long类型的大小是大于等于int类型的大小的,并没有严格规定long类型的大小.而在16位机器上,int类型的大小是2个字节, ;在32位和64位的机器上,int类型的大小是4个字节.

1.类型的基本归类

①整型

char   --> signed char   unsigned  char

short  --> signed short   unsigned  short

int      -->  signed int       unsigned int

long   -->  signed long    unsigned  long

在整型中,数据又被分为有符号数无符号数,有符号数可以加前缀signed来修饰,而无符号数可以加unsigned来修饰.不过,int,short,long这三个数据类型前面不加signed的时候默认是有符号数.char的情况比较特殊,他不加前缀修饰默认是什么类型是由编译器决定的,不过大多数的编译器是默认为有符号数.

关于有符号数和无符号数,这里还有一点很重要,在用printf打印时,打印无符号数是用%u作为占位符而不是%d,%d是打印有符号的十进制数.

 如上图,当用printf打印-10时,要用%u才能得出正确的结果.

②浮点数

float

double

 注意,浮点数跟整型不同,没有分为有符号数和无符号数,其实这是因为浮点数于整数在内存中的存储方式是不同的,这两种数据类型在内存中存储的差别我们会在后文详细讲解.

③构造类型

数组类型 --> int [10]

结构体  --> struct

枚举   --> enum

联合类型  --> union

 构造类型也被称为自定义类型,就是我们自己创造的类型.这里可能有一些读者会感到奇怪,难道数组也是一个类型吗?数组也是一个自定义类型吗?首先,我们来回答第一个问题,对于数组是不是一种类型,我们可以用sizeof来验证.

首先sizeof()括号内加上数据类型会计算出数据类型的大小,由上图我们不仅可以知道,数组也有类型,我们还知道,数组的类型是怎么表示的.

那么数组为什么是自定义类型呢?这个也很好理解,因为是由我们来决定数组中放多少个元素,是由我们来决定数组类型的大小的.其实数组和结构体很像,我们可以把结构体内的每个成员看成是类型不同的元素(也有可能相同),所以结构体和数组都是自定义类型.至于枚举类型和联合类型,我们后面会讲到,这两个其实和结构体很相似.

④指针类型

int *pi;

char *pc;

flaot *pf;

void *pv;

当然,我们这里只是列举了一部分的指针类型,还有其他的指针类型,比如二级指针,指向数组的指针,指向函数的指针等等,我们在这里不多赘述(后面会有更详细的讲解),在这里只要我们知道有这个类型就行.

⑤空类型

void 表示空类型

通常运用于函数的返回类型,函数的参数,指针类型

这里提一嘴void *类型,void * 类型是空类型的指针,对于这类指针,我们无法访问和对指针进行+-运算(因为我们不知道一次性对空类型的指针访问多大的空间,而且我们也不知道空类型的指针+-运算指针会往后走几个字节),NULL就是一个典型的空类型的指针,我们没办法对NULL指针进行解引用或者运算.

( 二 )整型在内存中的存储

1.原码,反码与补码

①原码,反码和补码之间的关系

无符号数:无原码反码和补码之说

有符号数正数:原码反码补码相同

有符号数负数:原码除了最高位(符号位)以外都按位取反,  反码 + 1得到补码

②原码,反码和补码的产生.整数类型被分为有符号数和无符号数,有符号数又被分为正数和负数,在计算机中有一个地方是专门执行运算的,在寄存器中,但是这块地方最开始只支持加法运算(例如:1 + 1),并不支持减法运算,科学家们为了节约成本(如果专门为减法设计电路的话,会导致成本上升),想出一个妙招,就是将减法运算变成加法运算(例如: 1 - 1 ---> 1 + (-1)),并规定有符号数的最高位为符号位,符号位为0表示正数,符号位为1表示负数,符号位参与运算.但是这样并没有解决问题:

 1 + (-1)应该等于0,而不应该等于-2.于是科学家们发明了反码,就是让一个负数(为什么不是正数?因为在加法运算的时候,出现问题的是负数)除了最高位(符号位)以外的所有的位都按位取反.

于是我们就大概地解决了减法变成加法这个问题,为什么说是大概呢?我们看一下下面的那个例子

 

1 + (-1)的结果是(-0),很明显,在我们正常的计算中,是不会出现-0这个结果的,-0也没有什么意义,科学家为了解决这个问题,又发明了补码.就是让反码 + 1.

最后,这个问题被成功解决了,1 + (-1)最后的结果就是0, 由于0的最高位(符号位)是0,所以0是属于正数(尽管在数学中并非如此),0的原码,反码和补码都是他本身0.

到这里,可能有些读者会好奇,既然-0不存在了,那么1000 0000又变成了哪个数?

实际上,1000 0000变成了-128(补码),由于-128转换成原码是0,所以-128没有原码,只有补码.这样1000 0000(-0)变成了(-128),signed char可以存储的空间又大了一位,变成了[-128, 127],接下来,我们以char为例子,研究整型的数据存储范围.

2.整型的数据存储范围

整型被分为有符号数和无符号数,区别就是最高位到底是表示符号还是表示实实在在的值.

无符号char: 0000 0000 ~ 1111 1111(0 ~ 255),无符号char的数据存储类型比较简单明了

有符号char:(正数部分)0000 0000 ~ 0111 1111(0 ~ 127),(负数部分)1000 0000 ~ 1111 1111(-1 ~ -128)(这里要补充一点,有符号数在内存中都是以补码的形式存储的,以补码的形式进行运算的,以原码的方式进行输入输出).合起来就是(-128 ~ 127).

我们还留意到一点,当有符号char从0000 0000 变成 1111 1111的时候,其值是由0 变成 127 ,接着127变成-128,接着-128变成-1.我们对1111 1111再加上1,1111 1111就会变成0000 0000,也就是说,当我们不断地对有符号char +1,最终他会循环地从0变成127,127变成负数再变成0,我们可以用一张图来表示这样的关系.

不仅是char类型,short, int ,long等等整形都是一样的,只不过他们表示的范围大了一些而已.

3.大小端存储模式介绍(大小大于1byte的整型)

我们知道,每个指针指向一个字节,我们可以通过指针来访问内存中存储的数据,那么我们对于大于1个字节的数据,我们该怎么通过指针访问呢?以int为例,

int a = 0x11223344

 计算机访问a的时候,到底是从11访问到44,还是44访问到11,还是用其他的顺序来访问呢?

首先,我们先排除第三种可能,因为如果不是按顺序来访问内存的话,那么计算机就需要记住访问内存的顺序,假如你是个设计计算机者,你会这么干吗?我想你不会自讨苦吃的,那么就剩下两种访问内存的可能性了,就是按从小到大的顺序来访问,或者按照从大到小的顺序来访问,指针访问的顺序又跟数据在内存中的存储的顺序有关,而这种顺序也被称为字节序.

根据字节序的不同,正数在内存中的存储被分为大端存储小端存储.

大端存储的定义:低位的数据存储在高地址,高位的数据存储在低地址,这样的存储方式就叫做大端存储.

小端存储的定义:低位的数据存储在低地址,高位的数据存储在高地址,这样的存储方式就叫做小端存储.

我们这样说还是很抽象,我们举一个栗子.

 可以看到,0x11223344的低位44存储在低地址,而高位11存储在高地址.

把他按字节从低地址到高地址一个个输出,果然,vs中数据是按照小端存储的方式来存储的(注意,一个编译器中数据只有一种存储的形式,不会一个地方小段存储一个地方大端存储的).

除了上面看起来有点复杂的输出数据的方法,我们有没有更简便的方法检测存储方式呢?

 当check()返回1的时候,是小端存储,否则是大端存储.

 注意:以上都是拿大于1个字节的整型来讨论大小端存储的,长度为1个字节或者小于1个字节的数据是没有大小端存储的这个说法的,因为指针访问这种类型的数据的时候没有顺序问题.

( 三 ) 浮点数在内存中的储存

在计算机中,没有小数这个说法,但是有浮点数这个说法.

我们看一看常见的浮点数

3.1415926

1E10

浮点数包括:float, double, long double类型

浮点数表示的范围:已经在float.h中定义

1.浮点数在内存中是如何存储的?

根据国际IEEE(电气和电子工程协会)754,任意一个二进制浮点数V可以表示为下面的形式:

①(-1)^S * M * 2^E
②(-1)^s表示符号位,当s=0V为正数;当s=1V为负数。
③M表示有效数字,大于等于1,小于2
④2^E表示指数位。

比如:在十进制中的5.0,把其变成二进制是101.0,也就是相当于(-1)^0 * 1.01 * 2^2

按照上面V的格式,S=0, M=1.01, E=2.

如果是-5.0,那么就是(-1)^1 * 1.01 * 2^2

S = 1,M = 1.01, E = 2;

(如果读者还是觉得这样很难理解,不妨可以类比一下十进制中的科学表示法)

①IEEE 754规定:

对于32位的浮点数,最高的1位是符号位,接着的8位是指数,剩下的23位是有效数字.

对于64位的浮点数,最高的1位是符号位,接着的11位是指数,剩下的52位是有效数字. 

②对于有效数字M和指数E,IEEE 754还有一些特别的规定:

在前面说过,1 <= M < 2,也就是说有效数字M一定是写成1.xxxxxx的形式.

IEEE 754规定,在计算机内部保存M时,默认最高数的第一位总是1,因此可以被舍去,就是把1.xxxxxx前面的1舍去,留下.xxxxxx,最后使用的时候再加上1(视情况)就可以了.

把第一位的1舍去之后,可以保留的有效数字就多了一位,变成了24位.

③对于指数E,情况就会比较复杂

首先,指数E是一个无符号整型(unsigned int)

但是,在科学计数法中会有指数为负数的情况,那么此时无符号整数就不能表示负数.

IEEE 754规定,32位浮点数在指数E存入内存时必须要加上一个值127,64位浮点数则要加上1023.

在把指数E从内存中取出来时又分为3种情况:

E不全都是0或者不全都是1:

此时,在取出E时会-127,得到最终结果

E全为0:

说明在将指数E存入内存之前的数非常小,时-127(或-1023),  2^(-127)(或2^(-1023))是一个极其趋近于0的数,那么此时,有效数字不再加上第一位的1,而是还原为0.xxxxxx的小数,这样做是为了表示+-0,以及非常接近于0的数.

E全为1:

此时如果有效数字M全为0(实际的有效数字是1),那么此时表示+-无穷大

2.有关于浮点数的一些补充

①浮点数的大小为4byte,8byte,那么会不会有像整型一样有类似的大小端问题呢?答案是肯定的,不过在这里就不是叫大小端存储了,在VS上,浮点数像是整数一样,浮点数的低位存储在低地址,高位存储在高地址.

②浮点数不能用关系操作符进行比较(大于,小于,等于),因为浮点数的存储必然有精度问题(32位浮点数只能存24位有效数字,64位浮点数只能存53位有效数字),我们无法得知(以我们当前学的知识)在这个精度以外的数字是什么,所以我们无法通过关系操作符得出两个浮点数谁大谁小.

那么,有什么办法可以解决这个问题吗?有!就是相减,让两个浮点数相减,当得到的值小于某个我们人为规定的值时,那么我们就认为这两个浮点数是相等的,我们也可以通过比较两数相减的值的正负来判断这两个数谁大谁小.

以上是有关于数据在内存中存储的介绍,如果有不妥之处欢迎在评论区中指出~~~

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>