C语言--数据在内存中的存储


再写代码和项目的时候,我们时时刻刻都要少不了与数据打交道,那么数据在内存中是如何存储的呢?今天我们来学习一下。
在这里插入图片描述

一、整数在内存中的存储

首先我们来探讨一下我们最熟悉的整数在内存中是如何存储的。
首先,在整数在内存中是以二进制的形式存放的,整数的二进制的表示方法有三种:原码、反码、补码

整数又分为有符号整数无符号整数
有符号整数的二进制的三种表示形式均有符号位和数值位之分,在二进制序列中,最高的一位就是符号位,剩余的都是数值位,符号位只表示正负,不参与计算,数值位才参与计算。
在最高的符号位中,1表示“负”’,0表示“正”
无符号整数则没有不分符号位和数值位,只表示正数,没有负数。

在C语言中规定:
正整数的原码、反码和补码都相同
负数的三种表示形式各不相同

0 存在 +0-0
对于 +0
原码:00000000
反码:00000000
补码:00000000
对于 -0
原码:10000000
反码:11111111
补码:0000000

我们首先来了解一下什么是原码、反码和补码:

原码:直接将数值按照正负数的形式翻译成⼆进制得到的就是原码
反码:将原码的符号位不变,其他位依次按位取反就可以得到反码
补码:反码+1就得到补码
举个例子:

#include <stdio.h>
int main()
{
	int a = 10;
	int b = -10;
	return 0;
}

a是int类型整数10,int类型占内存大小是4个字节,首先将10翻译成二进制就是1010,一个字节八个byte位,这才4个byte位,int类型整数内存大小是4个字节,所以在前面补上0,为:

a的原码: 00000000 00000000 00000000 00001010(中间空格方便观察)
a的反码: 011111111 111111111 111111111 11110101 (符号位不变,其他位按位取反)
a的补码: 011111111 111111111 111111111 11110110 (反码+1)

b是int类型整数-10,因为-10是负数,所以b的符号位就是1
b的原码:10000000 00000000 00000000 00001010
b的反码:11111111 11111111 11111111 11110101
b的补码:11111111 11111111 11111111 11110110

了解了原码,反码,补码后我们知道从原码到补码可以通过先取反得到反码再加1得到补码,那么如何从补码得到原码呢?
有两种方法:
方法一:
直接用逆过程,先减1,在取反。
方法二:
与从原码得到补码方法相同,直接先取反再加1。
我们来验证一下:

-10的原码:10000000 00000000 00000000 00001010
-10的反码:11111111 11111111 11111111 11110101
-10的补码:11111111 11111111 11111111 11110110

对-10的补码取反:10000000 00000000 00000000 00001001
然后加1:10000000 00000000 00000000 00001010
与原码相同

所以,可以得出结论,从原码得到补码和从补码得到原码都可以通过取反再加1的方法

其实对于整数来说,数据在内存中存放的是补码,那么为什么呢?
在计算机中,数值的计算和使用均使用补码,原因在于:使用补码方便将数值位和符号位统一处理,也可以将加法和减法统一处理,在计算机CPU中只有加法器(计算减法就等价于加一个负数)。而且由原码到补码,和由补码到原码的转换计算过程是相同的,不需要在加其他的硬件电路。

二、大小端字节序和字节序判断

了解了整数在内存中的存储后,我们通过一个例子,来具体看看是什么情况:

定义一个int类型变量n,把一个十六进制的数字赋值给n,十六进制一位数字可以转化为二进制四位数字,所以11,22,33,44分别是一个字节,总共4个字节。

#include <stdio.h>
int main()
{
	int n = 0x11223344;
	return 0;
}

我们调试,观察n的内存,以二进制观察内存地址太长了,所以为了方便展示,调试观察内存地址的时候以十六进制的形式观察,但存储还是以二进制存储的。
在这里插入图片描述
在这里插入图片描述

我们发现,它在内存中竟然是倒着存储的,那么为啥呢?

于是我们有了大端字节序存储小端字节序存储这两个概念:(概念很重要哦,面试笔试题考过的!)
大端字节序存储:是指数据的低位字节内容保存在内存的⾼地址处,⽽数据的⾼位字节内容,保存在内存的低地址处。
小端字节序存储:是指数据的低位字节内容保存在内存的低地址处,⽽数据的⾼位字节内容,保存在内存的高地址处。
知道这两个概念后,我们可能对高位和低位字节是什么不太清楚,就拿刚才的例子的n来说吧,n的值是0x11223344,按照每一位的权重来说,44 33 22 11所代表的数字大小依次变大,44所代表的数值最小,为低位,11代表的数值最大,为高位
在这里插入图片描述
在这里插入图片描述

那么为什么会有大小端字节序存储之分呢?
这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着⼀个字节,⼀个字节为8bit 位,但是在C语⾔中除了8 bit 的 char 之外,还有16 bit 的 short 型,32 bit 的 long 型(要看具体的编译器),另外,对于位数⼤于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度⼤于⼀个字节,那么必然存在着⼀个如何将多个字节安排的问题。因此就导致⼤端存储模式和⼩端存储模式。
我们常⽤的 X86 结构是⼩端模式,⽽KEIL C51 则为⼤端模式。很多的ARM,DSP都为⼩端模式。有些ARM处理器还可以由硬件来选择是
⼤端模式还是⼩端模式。

那我们来设计一个小程序来判断当前计算机的字节序存储方式

#include <stdio.h>
int check_sys()
{
	int n = 1;
	return *(char*)&n;
}
int main()
{
	int ret = check_sys();
	if (ret == 1)
		printf("小端\n");
	else
		printf("大端\n");
	return 0;
}

数字1的十六进制就是00 00 00 01,如果小端存储,n是1,是整型占4个字节,再内存中原码反码补码相同,把n的地址强制转换成char*类型,再解引用就是从n的地址向后访问一个字节,如果是小端存储,那访问的就是01,就是1,如果是大端存储,访问点就是00,就是0。
再VS中结果:
在这里插入图片描述
所以在VS中是以小端字节序存储。

练习:

学习了整数在内存中的存储和大小端字节序存储后,是不是觉得自己强的可怕!所以我们来做一些练习来巩固一下所学的知识。
在这里插入图片描述

在练习之前,首先来探讨一下signed char(有符号字符)和unsigned char(无符号字符)的存储:
首先char类型只能存储一个字节也就是8个比特位大小的数据
对于无符号char,没有符号位,8个比特位全部参与计算
在这里插入图片描述
所以,unsigned char的取值范围是0 ~ 255

那么对于signed char来说是有符号的数,也就是说有符号位,不参与计算
在这里插入图片描述
对于-128,如果表示-128,二进制是110000000,取反加1得到补码是110000000,总共9位,但是char只能存下8位,截断就是10000000,所以规定10000000为-128。
所以signed char的取值范围是-128 ~ 127

练习一:

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

-1的原码:10000000 00000000 00000000 00000001
-1的反码:11111111 11111111 11111111 11111110
-1的补码:11111111 11111111 11111111 11111111

-1是整型,有32个bit位,char只能存放8个bit位,所以就会发生截断,从小到大依次8个bit位放在char类型的a里面,所以a里面放的是-1的补码的后8bit位:11111111。

但是这是补码,要转化成原码打印,所以要判断a是否是有符号数,a是char类型的,char是有符号的char还是无符号的char要取决于编译器,在VS中char就等价于signed char,所以a是有符号的,是1

题目中是以%d打印,就是以有符号整数打印,是4个字节,但是a是个字符,补码是11111111就一个字节,所以要发生整型提升,在前面补上符号位直到满足位数,即11111111 11111111 11111111 11111111,这是补码,取反再加1得到原码是:
10000000 00000000 00000000 00000001,就是-1
b和a一样是有符号char,所以结果一样。

对于无符号char类型的c,没有符号位,发生整型提升前面补上0,即
00000000 00000000 00000000 11111111
正数原码反码补码相同,所以的值是255。
结果:
在这里插入图片描述

练习二:

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

-128是整数,a是char类型,赋值给char会发生截断

-128原码:10000000 00000000 00000000 10000000
-128反码:11111111 11111111 11111111 01111111
-128补码:11111111 11111111 11111111 10000000
所以-128补码的后面8位10000000被截断作为a的补码

a是char类型,是有符号数,要进行整型提升,补符号位,即
11111111 11111111 11111111 10000000
%u是以无符号整数打印,无符号位,正数原码反码补码相同,所以二进制转化为十进制直接打印,是4294967168。
结果:
在这里插入图片描述

练习三:

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

128是整数,a是char类型,发生截断。

128原码:00000000 00000000 00000000 10000000
128补码:01111111 11111111 11111111 01111111
128补码:01111111 11111111 11111111 10000000
所以128补码的后面8位10000000被截断作为a的补码

a是char类型,是有符号数,要进行整型提升,补符号位,即
11111111 11111111 11111111 10000000
%u是以无符号整数打印,无符号位,正数原码反码补码相同,所以二进制转化为十进制直接打印,是4294967168。
结果:
在这里插入图片描述

练习四:

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

从题中我们可以知道,数组元素是char类型的,有符号。
a[0] = -1,a[1] = -2,……一直到-128,-128的补码是10000000,再减1,就是01111111,正数原码补码反码相同,是127,然后126……一直到0,再到-1,……-128。
所以会一直循环从-1到-128,127到-1……
在这里插入图片描述

我们可以在调试窗口看:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
strlen计算的是从开始到’\0’的字符个数,再ASCII码中,'\0’的ASCII码是就是0,所以从-1到-128,在从127到1,总共255个数。
结果:
在这里插入图片描述

练习五:

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

我们上面再练习的开头讲了,unsigned char的取值范围是0 ~ 255,所以循环恒成立,会陷入死循环。

练习六:

#include <stdio.h>
int main()
{
 unsigned int i;
 for(i = 9; i >= 0; i--)
 {
 printf("%u\n",i);
 }
 return 0;
}

因为是unsigned char类型,所以一定是正数,i一定>=0,所以会一直打印从4294967295到0之间的数字。

练习七:

#include <stdio.h>
//X86环境 ⼩端字节序
int main()
{
 int a[4] = { 1, 2, 3, 4 };
 int *ptr1 = (int *)(&a + 1);
 int *ptr2 = (int *)((int)a + 1);
 printf("%x,%x", ptr1[-1], *ptr2);
 return 0;
}

a是int [4]类型的数组,&a就是取出整个数组的地址,加1,跳过一个数组的大小,然后强制类型转换为(int * )类型的指针赋给 ptr1,ptr[-1]就是 * (ptr - 1),因为ptr是int类型的,所以解引用向前访问一个整型,指向数组元素4的地址,以%x也就是十六进制打印,就是00 00 00 04,前面的0可以省略,就是4。

再看后一个,a是数组名,代表首元素地址,也就是1的地址,强制转化为(int)类型,在加1,就是地址加1,等价于向后走了一个字节。强制类型转化为(int*)类型的指针,放在ptr2指针变量里。ptr2就指向从数组首元素地址向后一个字节的地址
因为整数在内存中是小端字节序存储,所以如图所示:
在这里插入图片描述
1 2 3 4是int类型,都占4个字节,ptr2指向第二个字节00的地址,向后ptr2是int类型,所以解引用向后访问一个整型4个字节
在这里插入图片描述
所以
ptr2在内存中的存储就是00 00 00 02,因为整数是小端字节序存储,所以取出来时,数据的值就是02 00 00 00,在转化为十六进制打印还是02 00 00 00,前面的0可以省略,就是2000000。
结果:

在这里插入图片描述

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

数据不仅包括整数,也包括浮点数,也就是小数,例如,3.1415,1E10(科学计数法,表示1*10^10)。浮点数家族包括float,double,long double等类型。那么浮点数存储是否和整数存储方法一样呢?我们通过一个例子来看一下:

#include <stdio.h>
int main()
{
	int n = 9;
	float* pFloat = (float*)&n;

	printf("%d\n", n);
	printf("%f\n", *pFloat);

	*pFloat = 9.0;
	printf("%d\n", n);
	printf("%f\n", *pFloat);
	return 0;
}

结果:
在这里插入图片描述
从中可以发现,赋值整数以浮点数打印和赋值浮点数以整数打印结果均是错误的,所以可以得出结论:浮点数与整数在内存中的存储不同

所以浮点数在内存中是怎么存储的呢?
根据国际标准IEEE(电⽓和电⼦⼯程协会) 754,任意⼀个⼆进制浮点数V可以表⽰成下⾯的形式:

V = (−1) S ∗ M ∗ 2E
• (−1)S 表⽰符号位,当S=0,V为正数;当S=1,V为负数
• M 表⽰有效数字,M是⼤于等于1,⼩于2的
• 2E 表⽰指数位

举例来说:小数点后面的每一位权重依次为2-1,2-2……,所以十进制的5.5,翻译成二进制是101.1。
类比十进制的科学计数法,123.45 可以表示为1.2345 *102,二进制的101.1可以表示为1.011 * 22,则对于5.5这个浮点数来说,S就是0,M就是1.011,E就是2。
所以如果在内存中存储浮点数,仅仅需要存储S、M、E就行。但是对于不同精度的浮点数,存储也不相同。
IEEE 754规定:
对于32位的浮点数,最⾼的1位存储符号位S,接着的8位存储指数E,剩下的23位存储有效数字M
对于64位的浮点数,最⾼的1位存储符号位S,接着的11位存储指数E,剩下的52位存储有效数字M
在这里插入图片描述
在这里插入图片描述

3.1浮点数存的过程

我们知道存储一个浮点数只需要存储它的S,M,E就行,那么具体是如何存储的呢?

首先S只占一位,表示正负,1为负,0为正,就不多说了;

对于M,在前面我们提到 1≤M<2,也就是说,M可以写成1.xxxxxx的形式,其中xxxxxx表示小数部分。
IEEE 754 规定,在计算机内部保存M时,默认这个数的第⼀位总是1,也就是说任何小数的M都可以写成1.xxxxxx的形式,例如0.5可以写成1.0*2-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。⽐如,210的E是10,所以保存成32位浮点数时,必须保存成10+127=137,即10001001。

我们以5.5这个浮点数为例,来具体看一下它的存储。
5.5可以表示为 (−1) 0 ∗ 1.011 ∗ 22 ,
S是0,M是1.011,E是2。

#include <stdio.h>
int main()
{
	float f = 5.5;
	return 0;
}

将5.5赋值给float类型(32位)的 f,所以第一位存储S,是正数,所以是0,然后后面8位存储E,E原本是2,但是要加上中间值127,就是129,转化为二进制是10000001,后面23位存储M,我们之前提到M可以表示为1.xxxxxx的形式,1要省略舍去,也就是说要存储的是小数部分011,但是总共有23位,不够那就在后面补0,是01100000000000000000000。

所以5.5在内存中的存储就是
0(S)10000001(E) 01100000000000000000000(M)
即:01000000101100000000000000000000

以十六进制展示:
01000000101100000000000000000000
0100 0000 1011 0000 0000 0000 0000 0000 //4位变1位
40 b0 00 00

我们调试瞅瞅:
在这里插入图片描述
我们看到确实是这样存储的,但是顺序好像反了,因为float类型也是4个字节,那么就会有存储顺序的问题,浮点数数据存储也是小端字节序存储

3.2浮点数取的过程

知道了浮点数是如何存的,那么它是如何取出的呢?
有三种情况:

  1. E不为全0,也不为全1
    当E不为全0,也不为全1时,也就是既有1也有0时,第一位就是S,表示正负,后面的E,先要减去中间值127(或1023),得到真实的E,最后的有效数字M是计算的是小数部分,前面还要加上小数点之前的1,通俗点就是怎么存的怎么取。
0 01111110 01000000000000000000000
  1. E为全0
    如果E为全0时,那就说明真实的E加上中间值才等于0,此时的E很小,2E 所代表的数值非常接近与0。所以规定,当在内存中存储的E是全0时,就认为E=1-127(或1-1023)位真实的E值,有效数字M也不加上1,而是还原为0.xxxxxx的小数,表示±0,以及接近于0的很⼩的数字。
0 00000000 00100000000000000000000
  1. E为全1
    如果E为全1时,E此时的值为255(或2045),那么此时就代表±⽆穷⼤(正负取决于符号位s)。
0 11111111 00010000000000000000000

OK,到这里数据在内存中的存储就结束了,本文中有错误和由待改进的地方望大家批评和指正,再次感谢您的阅读!拜拜!
在这里插入图片描述

  • 25
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值