深度剖析数据在内存中的存储

目录

一、数据类型的介绍

1.1 类型的基本归类

1.1.1 整型家族

1.1.2 浮点数家族

1.1.3 构造类型(自定义类型)

1.1.4 指针类型

1.1.5 空类型

二、整型在内存中的存储

2.1 原码、反码、补码

2.2 大小端介绍

2.2.1 一道笔试题

知识点的补充:

signed char类型的取值范围:

2.3 几道练习题

2.3.1 示例1

 2.3.2 示例2

2.3.3 示例3

 2.3.4 示例4

2.3.5 示例5

2.3.6 示例6

2.3.7 示例7

三、浮点型在内存中的存储

3.1 一个例子

3.2 浮点数存储规则

3.2.1 E不全为0或不全为1

3.2.2 E全为0

3.2.3 E全为1

3.3 解释之前的题目

一、数据类型的介绍

c语言有哪些基本的数据类型呢?
char                             字符数据类型

short                            短整型

int                                整型

long                             长整型

long long                     更长的整型

float                             单精度浮点型

double                         双精度浮点型

类型的意义:

  • 不同的类型会开辟不同大小的内存空间(大小决定了使用范围)。
  • 类型决定了看待内存空间的视角(例如看到short,我们就认为内存中存储的是短整型)。

1.1 类型的基本归类

1.1.1 整型家族

     数值有正数和负数,

     有些数值,只有正数,没有负数,即没有符号位,例如(身高),在c语言中我们使用unsigned修饰的类型来表示。

     有些数值,有正数也有负数,例如(温度),在c语言中需要有符号位的类型来表示,即使用signed来修饰的类型。

  • char类型,由于char类型在内存中存储时,存储的是字符对应的ASCII码值,所以我们把char类型归类到整型家族中。char类型包括unsigned char(无符号的char类型)和signed char(有符号的char类型),在不同的编译器中,char类型表示的是有符号的char还是无符号的char有所差异,但是在VS编译器上,char类型表示的是有符号的char。
  • short类型,short类型包括unsigned short(无符号的short类型)和signed short(有符号的short类型),c语言规定short = signed short。
  • int类型,int类型包括unsigned int(无符号的int类型)和signed int(有符号的int类型),c语言规定int = signed int。
  • long类型,long类型包括unsigned long(无符号的long类型)和signed long(有符号的long类型),c语言规定long = signed long。

1.1.2 浮点数家族

  • float
  • double

1.1.3 构造类型(自定义类型)

  • 数组类型
  • 结构体类型  (结构体类型关键字:struct)
  • 枚举类型    (枚举类型关键字:enum)
  • 联合类型    (联合类型关键字:union)

1.1.4 指针类型

 指针类型包括:

  int * pa;        整型指针,*代表变量pa是指针,int代表pa指向的空间内存储的是整型。

  char * pb;     字符指针

  float * pc;     单精度浮点型指针

  void * pd;     空指针 

在下一篇文章中我们会讲解数组指针。

1.1.5 空类型

void表示空类型(无类型),通常应用于函数的返回类型,函数的参数,指针类型。

  • 应用于函数的返回类型:
    void text()
    {
    	;
    }

    他代表的是text这个函数没有返回值。

  • 应用于函数的参数:

    void text(void)
    {
    	;
    }

    即明确表示调用text函数不需要传参。

  • 应用于指针类型:对于void * 修饰的指针变量,任何地址都可以存放在这里,但是如果对他进行解引用操作会出问题,void * 修饰的指针变量一般用于临时存放一些地址。

二、整型在内存中的存储

     一个变量的创建需要在内存中开辟空间,开辟空间的大小是根据不同类型来决定的。

     我们都知道在内存中是按照二进制的形式来存储数据的,那么具体怎么存储呢?

2.1 原码、反码、补码

     计算机中的整数有三种二进制的表示方式,即原码、反码和补码。三种表示方法都由符号位数值位两部分构成,符号位用0表示“正”,用1表示“负”。符号位是最高位。

对于正数来说,原码、反码、补码相同。

对于负数来说,三种表示方式各不相同:

  • 原码:直接将数值按照正负数的形式翻译成二进制就可以得到原码。
  • 反码:原码的符号位不变,其余位按位取反。
  • 补码:反码+1。

例如:

	int a = 20;
	//变量a是int类型的
	//int类型在内存中占4个字节的空间
	//int类型有32个bite位的空间
	//00000000000000000000000000010100  a的原码(补码)
	//正数的原码、反码、补码相同
    int b = -10;
	//10000000000000000000000000001010     b的原码
	//11111111111111111111111111110101        反码
	//11111111111111111111111111110110        补码

对于整型来说:数据存放在内存中的其实是补码。

在计算机系统中,数值一律使用补码来表示和存储,有以下两点原因:

  • 使用补码,可以将符号位和数值位统一处理。
  • 使用补码,加法和减法也处理统一可以处理,此外原码与补码相互转换,其运算过程是相同的,不需要额外的硬件电路。

注意:在打印的时候,打印的是原码对于的数值。

在上述,我们已经知道了整型数值在内存中存放的是补码,那么具体怎么存储的?

2.2 大小端介绍

     在内存中,我们是以字节为单位进行存储的,存储一个int类型的变量a,int类型占4个字节,那么这四个字节在内存中具体是什么顺序呢,我们在VS编译器下来观察一下:

int main()
{
	int a = 0x12345678;
	//由于在内存显示时使用的是六进制的形式,所以为了方便观察变量a在内存中的存储我们直接将a的数值写为16进制的数值

	return 0;
}

     对于16进制的数来说两个16进制位代表一个字节,因此12代表一个字节的空间,34代表一个字节的空间,56代表一个字节的空间,78代表一个字节的空间。

     在内存中存储a,我们发现在存储时,a是按照字节为单位逆序存储的。

     一个数值只要超过一个字节,要存储在内存中,就有顺序的问题。

那么就有了小端存储和大端存储的概念了。

大端存储:是指数据的低位字节保存在内存的高地址中,而数据的高位字节保存在内存的低地址中。

小端存储:是指数据的低位字节保存在内存的低地址中,而数据的高位字节保存在内存的高地址中。

按图来看:

 

在上述的VS编译器中,使用的就是小端存储。

2.2.1 一道笔试题

请简述大端字节序和小端字节序的概念,设计一个小程序来判断当前机器的字节序。

  • 小端字节序存储:把一个数值的低位字节的内容,存放到低地址处,高位字节的内容放到高地址处。
  • 大端字节序存储:把一个数值的低位字节的内容,存放到高地址处,高位字节的内容放到低地址处。

题目分析:

     我们需要设计一个小程序来判断当前机器的字节序,我们知道字节序包括大端字节序和小端字节序,我们使用1来举例:

     我们发现在内存中存储变量a时,如果使用大端字节序存储,低地址第一个字节存储的是00,如果使用小端字节存储,低地址第一个字节存储的是01。

     所以在这里,我们发现观察低地址第一个字节就可以知道是大端字节存储还是小端字节存储,所以我们需要取出一个字节的空间来观察,就可以想到,把a的地址取出,由于a是int类型的,所以&a的类型是 int * 类型的,对 int * 类型的数据解引用访问4个字节,但是我们这里只需要一个字节就可以,所以我们将&a强制转换为 char * 类型的。

即:
  

int main()
{
	int a = 1;
	char* p = (char*)&a;
	if (*p == 1)
	{
		printf("小端\n");
	}
	else
	{
		printf("大端\n");
	}
	return 0;
}

 我们也可以把它分装成一个函数:
 

int check_sys()
{
	int a = 1;
	char* p = (char*)&a;
	if (*p == 1)
		return 1;
	else
		return 0;
}
int main()
{
	if (check_sys() == 1)
		printf("小端\n");
	else
		printf("大端\n");
	return 0;
}

知识点的补充:

signed char类型的取值范围:

对于signed char类型,在内存中占据8个bite位的空间,每个bite位是0或者1,就有以下多种可能:

下面我们可以使用一个图来具体表示:
 

 

2.3 几道练习题

2.3.1 示例1

下面代码输出什么?

#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
    // -1的反码:11111111111111111111111111111110
    // -1的补码:11111111111111111111111111111111
    // -1要放在char类型的变量a中,需要进行截断
    // a: 11111111

    signed char b = -1;
    // -1的原码:10000000000000000000000000000001
    // -1的反码:11111111111111111111111111111110
    // -1的补码:11111111111111111111111111111111
    //signed char类型是占内存1个字节,即8个比特位,要进行截断
    // b:11111111
    //注意:在这里是使用signed char来修饰b的,所以编译器就会认为b是一个有符号的字符类型。
    //即最高位的1是符号位

    unsigned char c = -1;
    // -1的原码:10000000000000000000000000000001
    // -1的反码:11111111111111111111111111111110
    // -1的补码:11111111111111111111111111111111
    // c:11111111
    //注意:在这里是使用unsigned char来修饰c的,所以编译器就会认为b是一个无符号的字符类型。
    //即最高位不是符号位

    printf("a=%d,b=%d,c=%d", a, b, c);
    //%d 是打印有符号整型,认为内存中存放的补码对应的是一个有符号数
    //%d是用来打印整型的,所以我们在打印时需要进行整型提升
    //a的类型是char类型,有符号位
    //a整型提升后:11111111111111111111111111111111
    //打印的时候打印的是原码对应的十进制
    //a的补码:11111111111111111111111111111111
    //a的反码:11111111111111111111111111111110
    //a的原码:10000000000000000000000000000001
    //所以打印 -1
    //b与a同理

    //c是无符号的char修饰的,在进行整型提升时,左边补0
    //c整型提升后的结果:00000000000000000000000011111111
    //%d进行打印的时候会把c当做一个有符号的整形来打印
    //即认为最高位0是符号位
    //打印的结果是00000000000000000000000011111111对应的十进制数字
    //即为255

 2.3.2 示例2

下面程序输出什么?
#include <stdio.h>
int main()
{
    char a = -128;
    printf("%u\n",a);
    return 0;
 }

char a = -128;
    //-128
    //-128的原码:10000000000000000000000010000000
    //-128的反码:11111111111111111111111101111111
    //-128的补码:11111111111111111111111110000000
    //-128要放在char类型的a中,char类型的变量在内存中占8个bite位的空间
    //需要发生截断
    //a的补码:10000000

    printf("%u\n", a);
    //%u打印无符号的整型,认为内存中存放的补码对应的是一个无符号数
    //a - 10000000
    //a的类型是char类型的,进行整型提升时高位补符号位的值即1
    //11111111111111111111111110000000  -  补码
    //%u打印无符号的整型,他会认为11111111111111111111111110000000对应的是一个无符号数
    //无符号数的补码与原码相同
    //所以打印的是11111111111111111111111110000000对应的十进制数
    //即:4294967168

2.3.3 示例3

下面代码输出什么?

#include <stdio.h>
int main()
{
    char a = 128;
    printf("%u\n",a);
    return 0; 
}

问题分析:

char a = 128;
	//128
	//128的原码:00000000000000000000000010000000
	//128的反码:01111111111111111111111101111111
	//128的补码:01111111111111111111111110000000
	//128要放在char类型的a中,char类型的变量在内存中占8个bite位的空间
	//需要发生截断
	//a的补码:10000000
printf("%u\n", a);
	//%u打印无符号的整型,认为内存中存放的补码对应的是一个无符号数
	//a - 10000000
	//a的类型是char类型的,进行整型提升时高位补符号位的值即1
	//11111111111111111111111110000000  -  补码
	//%u打印无符号的整型,他会认为11111111111111111111111110000000对应的是一个无符号数
	//无符号数的补码与原码相同
	//所以打印的是11111111111111111111111110000000对应的十进制数
	//即:4294967168

 2.3.4 示例4

下面代码输出什么?

int main()
{
	int i = -20;
	unsigned int j = 10;
	printf("%d\n", i + j);
	return 0;
}

printf("%d\n", i + j);
    //i的原码:10000000000000000000000000010100
    //i的反码:11111111111111111111111111101011
    //i的补码:11111111111111111111111111101100
    //j使用无符号的整型修饰,无符号位
    //j的原码(补码):00000000000000000000000000001010
    //11111111111111111111111111101100  -  i
    //00000000000000000000000000001010  -  j
    //i+j的补码:
    //11111111111111111111111111110110
    //%d打印有符号的整形,认为内存中存放的补码对应的是一个有符号数
    //即i+j对应的数字是一个有符号数,即最高位是符号位
    // i+j的原码:
    //10000000000000000000000000001010    
    //对应的十进制数是-10
    //所以打印的是-10

2.3.5 示例5

下面输出的是什么?

int main()
{
	unsigned int i;
	for (i = 9; i >= 0; i--) 
	{
		printf("%u\n", i);
	}
	return 0;
}

最终代码结果我们发现会死循环。

这是为什么呢?
unsigned int i;
    //i是使用unsigned int修饰的,即i是一个无符号的整型,无符号的整型没有符号位,即没有负数最小值就是0
    //所以i >= 0这个表达式是恒成立的
for (i = 9; i >= 0; i--) 
    {
        //当i=0,i-1为-1
        //-1的原码是:10000000000000000000000000000001
        //-1的反码是:11111111111111111111111111111110
        //-1的补码是:11111111111111111111111111111111
        //-1放到无符号整型i中,就不再是-1
        //对于无符号整型,他会认为没有符号位即他认为-1的原码与补码相同
        //都是11111111111111111111111111111111
        //其对应的数是一个非常大的整数

所以会死循环。

2.3.6 示例6

下面代码输出什么?

int main()
{
    char a[1000];
    int i;
    for(i=0; i<1000; i++)
   {
        a[i] = -1-i;
   }
    printf("%d",strlen(a));
    return 0;
 }

题目分析:

 strlen求字符串的长度时,遇到‘\0’(0)停止,即a[i]为0时,之前的元素个数。

//a[i]里面存放的数据都是char类型的,char类型的取值范围是:-128 - 127。

我们可以参照下面的图:

 我们发现a[i]的变化路径是:-1,-2,-3......-128 127 126......1 0

我们只需要知道0之前的元素个数,根据上图我们知道其个数为:128+127 即255。

2.3.7 示例7

下面代码输出什么?

#include <stdio.h>
unsigned char i = 0;
int main()
{
    for(i = 0;i<=255;i++)
   {
        printf("hello world\n");
   }
    return 0; 
}

上面代码最终会死循环。

题目分析:
     变量i是unsigned char类型的,unsigned char类型的取值范围是0-255,所以i<=255恒成立,所以就会死循环。

三、浮点型在内存中的存储

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

     浮点数在内存中存储与整型在内存中的存储相同吗?答案是不相同。

3.1 一个例子

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

他的运行结果是:

上述代码的结果正说明了浮点数和整型在内存中的存储方式不同,本题在下面讲解原因。

3.2 浮点数存储规则

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

  • (-1) ^ 5 * M * 2 ^ E
  •   (-1)^S表示符号位,当S=0,V为正数;当S=1,V为负数。
  •   M表示有效数字,大于等于1,小于2。
  •   2^E表示指数位。
举例来说:v= 9.5 , 9.5 是十进制表示的,我们要先化为2进制,
v= 9.5
  = 1001.1
  = 1.0011 * 2  ^ 3
  = (-1)^ 0 * 1.0011 * 2 ^ 3
S = 0,M = 1.0011,E = 3。
IEEE 754 规定:
对于32位的浮点数,最高的1位是符号位S,接着的8位是指数E,剩下的23位为有效数字M

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

 

IEEE 754对有效数字M和指数E,还有一些特别规定。

     前面说过, 1≤M<2 ,也就是说,M可以写成 1.xxxxxx 的形式,其中xxxxxx表示小数部分。

IEEE 754 规定,在计算机内部保存 M 时,默认这个数的第一位总是 1 ,因此可以被舍去,只保存后面的 xxxxxx部分。比如保存 1.01 的时 候,只保存01 ,等到读取的时候,再把第一位的 1 加上去。这样做的目的,是节省 1 位有效数字。以 32 位 浮点数为例,留给M 只有 23 位,将第一位的1舍去以后,等于可以保存 24 位有效数字。
至于指数 E ,情况就比较复杂。
首先, E 为一个无符号整数( unsigned int
     这意味着,如果 E 8 位,它的取值范围为 0~255 ;如果 E 11 位,它的取值范围为 0~2047 。但是我们知道,科学计数法中的E 是可以出 现负数的,所以IEEE 754 规定,存入内存时 E 的真实值必须再加上一个中间数,对于 8 位的 E ,这个中间数 是127 ;对于 11 位的 E ,这个中间 数是1023 。比如, 2^10 E 10 ,所以保存成 32 位浮点数时,必须保存成 10+127=137 ,即10001001。
然后,指数 E 从内存中取出还可以再分成三种情况:

3.2.1 E不全为0或不全为1

     这时,浮点数就采用下面的规则表示,即指数E的计算值减去127(或1023),得到真实值,再将 有效数字M前加上第一位的1。
比如:
0.5(1/2)的二进制形式为0.1,由于规定正数部分必须为1,即将小数点右移1位,则为1.0*2^(-1),其阶码为-1+127=126,表示为 01111110,而尾数1.0去掉整数部分为0,补齐0到23位00000000000000000000000,则其二进 制表示形式为:
0 01111110 00000000000000000000000

3.2.2 E全为0

     这时,浮点数的指数E等于1-127(或者1-1023)即为真实值,有效数字M不再加上第一位的1,而是还原为0.xxxxxx的小数。这样做是为了表示±0,以及接近于0的很小的数字。

3.2.3 E全为1

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

3.3 解释之前的题目

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

 

int n = 9;
    
    //00000000000000000000000000001001   9的补码

    float* pFloat = (float*)&n;

    printf("n的值为:%d\n", n);
    //%d打印有符号的整型,他认为补码对应的数是一个有符号整型
    //打印的即为00000000000000000000000000001001对应的十进制数

  printf("*pFloat的值为:%f\n", *pFloat);
    //*pFloat使用浮点数的视角去看这块空间,认为里面放的是浮点数
    //0 00000000 00000000000000000001001
    //(-1)^ 0 * 0.00000000000000000001001 * 2 ^-126
    //即为0.000000

  *pFloat = 9.0;
    //9.0
    //1001.0
    // 1.001 * 2^3
    //(-1)^0 * 1.001 * 2^3
    //0 10000010 0010000000000000000
    
    printf("num的值为:%d\n", n);
    //0 10000010 0010000000000000000
    //这个32位的二进制数,还原成十进制,正是 1091567616 
    printf("*pFloat的值为:%f\n", *pFloat);

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值