⭐️ C语言进阶 ⭐️数据在内存中的存储(深度剖析)

135 阅读12分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路

目录

前言

数据类型

C语言基本的内置类型

类型的意义

类型的基本归类

整形在内存中的存储

原码、反码、补码

意义

大小端

什么是大端小端

 为什么有大端和小端

一道笔试题

练习

浮点型在内存中的存储

常见的浮点数

浮点数存储

 浮点数存储规则

IEEE 754规定

特别规定

指数E从内存中取出


前言


本文主要讲解点

  1. 数据类型详细介绍
  2. 整形在内存中的存储:原码、反码、补码
  3. 大小端字节序介绍及判断
  4. 浮点型在内存中的存储解析

数据类型


C语言基本的内置类型

char //字符数据类型
    unsigned char      //signed(有符号)/unsigned(无符号)
    signed char
//大多数编译器默认char为 signed char 即有符号类型(有的认为是unsigned char)
//对于 short int long 都认为是有符号类型
short //短整型
    unsigned short 
    signed short 
int //整形
    unsigned int
    signed int
long //长整型
    unsigned long 
    signed long
long long   //更长的整形
float //单精度浮点数
double //双精度浮点数
//C语言有没有字符串类型?    有字符串,没有字符串类型

类型的意义

  1. 使用这个类型开辟内存空间的大小(大小决定了使用范围)
  2. 如何看待内存空间的视角(电脑存取数据看待的视角)

类型的基本归类

  • 整形家族
char
     unsigned char
     signed char
short
     unsigned short [int]
     signed short [int]
int
     unsigned int
     signed int
long
     unsigned long [int]
     signed long [int]
  • 浮点数家族
float
double
  • 构造类型
> 数组类型
> 结构体类型 struct
> 枚举类型 enum
> 联合类型 union
  • 指针类型
int *pi;
char *pc;
float* pf;
void* pv;
  • 空类型
void 表示空类型(无类型)//理论不会开辟空间,要么当做为一个占位符,故不能定义变量

//通常应用于函数的返回类型、函数的参数、指针类型 

整形在内存中的存储


变量的创建会在内存中开辟空间,而空间的大小根据类型来决定

原码、反码、补码

原码、反码和补码三种表示方法均有符号位和数值位

符号位都是用0表示“正”,用1表示“负”(符号位依旧符合二进制运算)

  •  正整数

原、反、补码都相同

  • 负整数

原码:直接将二进制按照正负数的形式翻译成二进制就可以

反码:将原码的符号位不变,其他位依次按位取反就可以得到了

补码:反码+1就得到补码

  • 对于整形来说

数据存放内存中其实存放的是补码

而数据的读取是使用原码

意义

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

使用补码可以将符号位和数值域统一处理

同时加法和减法也可以统一处理(CPU只有加法器)

而补码与原码相互转换的运算过程是相同的(共用一套法则)(不需要额外的硬件电路)

大小端


对于数据在内存存储补码时的字节排列顺序是有差异的(对于不同编译器)

什么是大端小端

大端:指数据的低位保存在内存的高地址中,而数据的高(权)位,保存在内存的低地址中

小端:指数据的低位保存在内存的低地址中,而数据的高(权)位,保存在内存的高地址中

 为什么有大端和小端

计算机系统是以字节为单位,每个地址单元都对应着一个字节,一个字节为8 bit

而C语言中除了8 bit的char之外,还有其他类型(大于8bite)以及寄存器宽度不一样

必然会涉及存储时字节安排的问题,而并没有规定,也没有科学的理由说服彼此,就有大小端之分

注:char类型没有大小端(8bite)

一道笔试题

设计一个小程序来判断当前机器的字节序

int check_sys()
{
 int i = 1;//补码:00000000000000000000000000000001 (16进制)0x00000001
 return (*(char *)&i);//(char*)&i是取的i存储在低地址首字节的地址
}                     //*解引用如果得到1则为小端(低权位放在低地址)
int main()            //如果得到0则是大端(高权位放在低地址)
{
 int ret = check_sys();
 if(ret == 1)
 {
 printf("小端\n");
 }
 else
 {
 printf("大端\n");
 }
 return 0;
}

练习


  • 例题一
//输出什么?
#include <stdio.h>
int main()
{
    char a= -1;
    signed char b=-1;
    unsigned char c=-1;
    printf("a=%d,b=%d,c=%d",a,b,c);
    return 0;
}
    char a= -1;
//-1
//原:10000000000000000000000000000001
//反:11111111111111111111111111111110
//补:11111111111111111111111111111111
//char类型为1字节,存储时会发生截断
//存储补码:11111111(从后面截断)
//整型提升:(补)11111111111111111111111111111111
//需要整型提升看数据类型
//%d:以有符号整型(4字节)打印
//需要打印看打印类型
//原码:10000000000000000000000000000001
    signed char b=-1;
//char一般默认为signed char(故与a一致)
    unsigned char c=-1;
//存储补码:11111111
//整型提升:(补)00000000000000000000000011111111
//符号位为0,是正数,原反补相同
//原:00000000000000000000000011111111

输出结果:-1 -1 255
  • 例题2
2.
#include <stdio.h>
int main()
{
    char a = -128;
    printf("%u\n",a);
    return 0;
}
    char a = -128;
//补:10000000
//整型提升:111111111111111111111111110000000
//%u:以无符号整型打印
//原:11111111111111111111111110000000

输出结果:4294967168
  • 例题3
#include <stdio.h>
int main()
{
    char a = 128;
    printf("%u\n",a);
    return 0;
}
    char a = 128;
//原:10000000000000000000000010000000
//补:11111111111111111111111110000000
//(截断)补:10000000(原码与-128一样,不过规定这是-128)
//char类型的范围是-128—127
//与例题2过程一样

输出结果:4294967168
  • 例题4
int i= -20;
unsigned  int  j = 10;
printf("%d\n", i+j);
//按照补码的形式进行运算,最后格式化成为有符号整数
int i= -20;
unsigned  int  j = 10;
//-20的补:11111111111111111111111111101100 
// 10的补:00000000000000000000000000001010 
//  i+j补:11111111111111111111111111110110 
//有符号整型打印:(原)10000000000000000000000000001010

输出结果:-10
  • 例题5
unsigned int i;
for(i = 9; i >= 0; i--)
{
    printf("%u\n",i);
}
//i是无符号数
//当i为负数:例如i=-1时
//原:10000000000000000000000000000001
//补:11111111111111111111111111111111
//条件判定(以无符号数参与表达式,结果是i被认为是一个非常大的数),一直都能执行语句(死循环)

浮点型在内存中的存储


常见的浮点数

3.14159

1E10

浮点数家族包括: float、double、long double 类型 

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

浮点数存储

  • 例如
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;
}
  • 输出结果

说明:浮点数存储与整型存储完全不同

 浮点数存储规则

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

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

例如

十进制的5.0
写成二进制是 101.0 ,相当于 1.01×2^2 
得出s=0,M=1.01,E=2

IEEE 754规定

  • 对于32位的浮点数

最高的1位是符号位s,接着的8位是指数E,剩下的23位为有效数字M

  •  对于64位的浮点数

最高的1位是符号位S,接着的11位是指数E,剩下的52位为有效数字M

特别规定

  • 对于M

在计算机内部保存M时(因为1≤M<2),默认这个数的第一位总是1

因此可以被舍去,只保存后面的 xxxxxx部分(节省1位有效数字)

比如保存1.01的时候,只保存01,等到读取的时候,再把第一位的1加上去

  • 对于E

E为一个无符号整数(unsigned int)

如果E为8位,它的取值范围为0-255;如果E为11位,它的取值范围为0-2047

但对于科学计数法来说E是可以出现负数的

所以存入内存时E的真实值必须再加上一个中间

对于8位的E,这个中间数是127;对于11位的E,这个中间数是1023

指数E从内存中取出

  • E不全为0或不全为1

指数E的计算值减去127(或1023),得到真实值,再将 有效数字M前加上第一位的1

  • E全为0

浮点数的指数E等于1-127(或者1-1023)即为真实值

有效数字M不再加上第一位的1,而是还原为0.xxxxxx的小数

这样做是为了表示±0,以及接近于 0的很小的数字

  • E全为1

这时,如果有效数字M全为0,表示±无穷大(正负取决于符号位s)

回到上述例题

输出结果怎么是这样呢?

 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);

解:int n = 9是以整型存储的视角将9赋值给n

9 -> 0000 0000 0000 0000 0000 0000 0000 1001(整型存储)

打印浮点数时则是以浮点存储的视角来看待这串内容

拆分0000 0000 0000 0000 0000 0000 0000 1001

得到s=0,E=00000000 ,M=000 0000 0000 0000 0000 1001(指数E全为0,符合第二种情况)

浮点数V就写成: V=(-1)^0 *0.00000000000000000001001*2^(-126)=1.001*2^(-146)

这时V是一个很小的接近于0的正数,得到输出结果为:0.000000

 *pFloat = 9.0则是以浮点数存储的角度n的空间的内容修改为9.0 

 此时浮点数9.0 -> 1001.0 >(-1)^0*1.001*2^3 ->即 s=0, M=1.001,E=3+127=130

9.0 ->0 10000010 001 0000 0000 0000 0000 0000(浮点数存储)

当这串内容以整型打印时(即以整型存储的视角看待)得到输出结果:1091567616