[C语言数据存储深度解析]-内存数据搞不懂?三千字长文带你走进数据类型及其存储

MEUN

  • C语言的基本数据类型
  • 内置类型的基本归类
  • 整型数据在内存中的存储
  • 浮点型数据在内存中的存储
  • 写在最后

C语言的基本数据类型

  • 相信大家应该已经很熟悉C语言内置的几个基本数据类型了,这里仅简单罗列:

char------------字符数据类型
short-----------短整型
int---------------整型
long------------长整型
long long-----长整型(plus) (较老的编译器可能没有此类型)
float------------单精度浮点型
double---------双精度浮点型

  • 类型存在的意义:
  • 我们知道不同的数据类型会占用不同的内存空间。在使用时它们时,我们往往是按需使用,以免造成空间的浪费或溢出。
  • 类型还决定着我们看待内存空间的视角
int a=2021;
float A=2021.0f;

这里我们定义了两个变量,整型a和单精度浮点型A,这时存储a和A的两块空间都各自自占据着各自的“立场”,也就是说a开辟的空间中将按照整型的方式存和取,而A则是按照浮点型的方式存和取。

内置类型的基本归类

  • 整型

char / unsigned char(无符号类型) /signed char(有符号类型)
short /unsigned short/signed short
int / unsigned int / signed int
long /unsigned long/signed long
long long同上.

Tips:

  • short [int]是short未省略的写法,long等也一样。
  • 类型前未写(un)signed时,short、long、int等类型直接默认为有符号的对应类型,而char则取决于编译器,大部分编译器都解析其为signed char.

用例深度理解signed和unsigned:

我们知道一个char类型可以存放的是1byte,8bit的大小元素。
这里我们假设此内存中存放二进制数字a1(a2):11111111(补码)
有: unsigned char a1 中无符号位,也就是二进制序列中每一位都是有效位,补码就是其原码、反码,那么a1的值就是255;
亦:signed char a2 中有符号位,首位1代表负数,经过计算,a2的值是-1。

我们更直观的观察一下:

int main()
{
	unsigned char a1 = 255;
	printf("%d\n", a1);
	printf("------\n");
	char a2 = 255;
	printf("%d\n", a2);
	return 0;
}

先找到它们在内存中的数值:(调试起来并在内存窗口中查看)
a1:
[C语言数据存储深度解析]-内存数据搞不懂?三千字长文带你走进数据类型及其存储_第1张图片
a2:
[C语言数据存储深度解析]-内存数据搞不懂?三千字长文带你走进数据类型及其存储_第2张图片

可以看到它们的值都是ff(16进制),也就是11111111.
接着看程序运行结果:

故而证明了我们上述理解是正确的,其它类型的数据据此举一反三即可,可以说完全相同。

  • 浮点型

float----4byte
double----8byte(更高精度、更多占用)

  • 构造类型

数组类型
结构体类型-struct
枚举类型-enum
联合类型-union

Tips:

  • 数组的类型:如int arr[10],和之前其它类型一样,我们去掉我们定义的类型名(arr),剩下的就是类型,即此例类型为 int [10]
  • 此段代码可以很直观的展示:
#include
int main()
{
	int a = 10;
	int arr[10] = { 0 };
	printf("%d\n", sizeof(a));
	printf("%d\n", sizeof(int));
	printf("----------\n");
	printf("%d\n", sizeof(arr));
	printf("%d\n", sizeof(int [10]));
	return 0;
}

其结果为:
[C语言数据存储深度解析]-内存数据搞不懂?三千字长文带你走进数据类型及其存储_第3张图片

  • 指针类型

int *
char*
float*
···

  • 空类型

void表示空类型(无类型)
常应用于函数返回值类型、函数参数、指针类型等。

整型数据在内存中的存储

  • 计算机中的符号数有三种表示方法,即原码、反码和补码。
    下面我们看一看它们的概念:(点此可直接跳转)
    [C语言数据存储深度解析]-内存数据搞不懂?三千字长文带你走进数据类型及其存储_第4张图片

正数和无符号数的原码、反码、补码都是相同的。
对于整型来说:数据在内存中均以补码的形式进行存储和计算

为什么呢?
这是因为在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统一处理; 同时,加法和减法也可以统一处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。

  • 数据存储的又一特点(以整型为例研究)
    我们看这样一段代码
int main()
{
	int a = 10;
	int b = -10;
	return 0;
}

我们再看看其在内存中的存储:
[C语言数据存储深度解析]-内存数据搞不懂?三千字长文带你走进数据类型及其存储_第5张图片

乍一看,总感觉补码数据存放的顺序有些奇怪,似乎是反了?
我们仔细观察后发现,内存中自上而下,地址不断增加,每个地址指向的空间中的每个字节却是由高到低的顺,我们十进制为10的数字,在16进制存储时似乎本应是00 00 00 0a,但由于这里是在低地址放入了数据的低位,便反向排列,这就是小端存储模式

  • 有小端,就可能有大端[/大笑],我们引入其概念:(源地址)
    [C语言数据存储深度解析]-内存数据搞不懂?三千字长文带你走进数据类型及其存储_第6张图片
  • 它们存在的意义是什么?
  1. 在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一
    个字节,一个字节为8bit。但是在C语言中除了8bit的char之外,还有16bit的short型,32bit的long型(要看具体的编译器),另外,对于位数大于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如果将多个字节安排的问题。因此就导致了大端存储模式和小端存储模式。例如一个 16bit 的 short 型 x ,在内存中的地址为 0x0010 , x 的值为 0x1122 ,那么 0x11 为高字节, 0x22为低字节。对于大端模式,就将 0x11 放在低地址中,即 0x0010 中, 0x22 放在高地址中,即 0x0011 中。小端模式,刚好相反。我们常用的 X86 结构是小端模式,而 KEIL C51 则为大端模式。很多的ARM,DSP都为小端模式。有些ARM处理器还可以由硬件来选择是大端模式还是小端模式。

你能否写一个程序看看你所使用的编译器是大端存储模式还是小端存储模式?日后我会将程序写出来,你可以和我作一个对比

浮点型数据在内存中的存储

浮点型的数据种类前文我已提到,这里不再过多赘述,先来看一道题目:

int main()
{
int n = 9;
float *pFloat = (float *)&n;
printf("n的值为:%d\n",n);
printf("*pFloat的值为:%f\n",*pFloat);
*pFloat = 9.0;
printf("num的值为:%d\n",n);
printf("*pFloat的值为:%f\n",*pFloat);
return 0;
} 

先想一想,在脑海中给自己一个答案,然后继续往下看~

这是多年前一家公司的一道笔试题目,第一次做它的时候我也做错了,故而觉得其放在此处很有代表性,它的答案是:
[C语言数据存储深度解析]-内存数据搞不懂?三千字长文带你走进数据类型及其存储_第7张图片
你做对了吗?为什么结果如此呢?接下来我将解释浮点型数据在内存中的存储方式,若你耐心看完,倘若现在没有头绪的话,到时再回头看此题就会豁然开朗。


我们知道num和*pFloat在内存中表示的是一个数字,但解读的结果为何如此之大?我们需要知道浮点数在计算机内部的表示方法。
根据国际标准IEEE754,任意的一个二进制数字V可以表示成下面的形式:

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

这里需要知道十进制的浮点数中小数点之后数字的二进制改写写法:如5.0f,写成二进制是101.0,相当于1.01*2^2。按照上面的写法:我们容易算出S=0,M=1.01,E=2。

IEE754规定:对于32位单精度浮点数,最高位1位是符号位S,接着的8位数是指数E,剩下的23位为有效数字M。
[C语言数据存储深度解析]-内存数据搞不懂?三千字长文带你走进数据类型及其存储_第8张图片

ps:对于64位,s仍为1bit,E为11bit,M则是52bit。

  • 接下来是有效位E和M的存储方式:

首先是M:

IEEE 754对有效数字M和指数E有一些特别规定。
由于1<=M<=2,即M为1.xxxxxx。在规定中,保存M时,我们总是将第一位舍去(因为它默认是1),只保存 .xxxxxxx,在我们读取的时候再将其加上,这样做多保存一位有效数字。

然后是E:

E是一个无符号数,当E占8bit时,它的取值范围是0~255 ,当其占11bit时,它的取值范围是0~2047。我们h知道,科学计数法中的E可以出现负数,故IEEE 754规定E存入时必须在其真实值上加一个中间数,对于8位的E,加上127,对于11位,则加上1023。
比如2^10的E为10,所以保存为32位的浮点数时,必须保存成10+127=137,也就是10001001.

这里E在内存中取出还可以再分成三种情况:

  • E不全为0或不全为1
    指数E的计算值减去127(1023),得到真实值后再将M加上第一位的i。
  • E全为0
    此时有效数字M不再加1,而是直接还原为0.xxxxxx的小数。这样做是为了表示±0,以及接近于0很小的数字。
  • E全为1
    此时如果有效数字M全为0,则表示正(负)无穷大,这取决于S是什么。
  • 我们回到这一小节一开始的那个问题:
    我们先写出9的补码:00000000000000000000000000001001
  • 1.以int类型存入,以%d打印,毋庸置疑第一个打印结果是9;
  • 2.n由pFloat维护,pFloat的存在使得它把内存中9的补码认作浮点数序列,
    那么9的补码被理解为:
    0 00000000 00000000000000000001001
    即是:E全0,则E直接为1-127=-126。M也只拿小数点后的数字且不加1,M为0.00000000000000000001001。再根据标准还原出数字为:
    0.00000000000000000001001*2^(-126) 这个数字非常非常小,无限接近于0,又知道%f只能打印6位,那么容易知道结果是0.000000
  • 3 这一次是以浮点数的视角进行存储,那么它在内存中就是:
    1001.0(前面省略),即是(-1)^ 0 * 1.001*2^3
    E就是3然后再加上127,再写成8位二进制序列,S是0,M是1.001000……
    最后的序列就是:
    0 10000010 00100000000000000000000
    这个时候我们又以%d的形式打印,我们就认为内存里是个有符号整数,这个序列自动会被存取为整数序列而不再是浮点数序列,这也是本文得到重点,再次出现。那么它可以直接用计算器得到答案是:1091567616
  • 4 和1是如出一辙,答案自然是9.000000

写在最后

本文可能唯一有理解难度的地方就是浮点数数据相关的重点在于数据在内存中的存和取,内存对于其存的二进制序列如何判断和使用,再根据IEEE754标准进行转变。这也是本文的重点,希望能对你有所帮助。

你可能感兴趣的:(C语言深层,C语言小知识,c语言)