0基础C语言积跬步之数据在内存中的存储

目录

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

1.1大小端字节序存储模式

1.2练习(含大小端字节序判断)

(1)练习一

(2)练习二

(3)练习三

(4)练习四

(5)练习五

(6)练习六

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

2.1浮点数的存储

2.2浮点数存储的过程

2.3浮点数取数的过程

(1)E不全为0或不全为1

(2)E为全0

(3)E为全1

2.4题目解析


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

首先我们知道,整数的二进制表现方法有三种:原码、反码、补码

在内存中整数是以二进制补码的形式进行存储的

我们来看一串代码:

cpp 复制代码
#include <stdio.h>
int main()
{
	int a = 0x11223344;
	return 0;
}

a中存的是十六进制的11223344,因为是正数,所以他的原反补码相同,虽然在内存中会把这串数字换算为二进制的形势存储,但是当我们调试时,会以十六进制形势展示出来,所以我们看到的它的原反补码都是11223344,让我们在调试中看看:

可以看到,在内存中我们存储的时候,是倒着存储的,这是为什么呢?这就要讲到一个知识点:大小端字节序,上面这个是小端存储模式

1.1大小端字节序存储模式

为什么会有大小端存储模式呢?

因为当超过一个字节的数据在内存中存储时,我们就肯定会有存储顺序的问题,当一个字节的时候,我们就不用考虑顺序,因为就一个字节,直接放着就好了,但超过一个字节时,例如上述代码的0x11223344,我们就需要考虑,是直接存储11223344,还是要倒过来存储44332211,这个在不同的CPU架构有不同的设计,是在硬件生产的时候就已经固定好的,我们分为大端字节序存储和小端字节序存储,字节序意思就是按单个字节来存储,按【单个字节】为单位排顺序

  • x86/x64 架构 日常电脑、笔记本的 Intel/AMD CPU 都是这个架构,默认小端序 ,也是你现在电脑的模式。 示例数据 0x11223344,内存低地址开始存:44 33 22 11

  • ARM 架构 手机、单片机、部分嵌入式设备用的架构。 早期多为大端序 ,现在很多新款 ARM 支持切换,但嵌入式场景仍常用大端。 大端规则:0x11223344,内存低地址开始存:11 22 33 44

然后我们要了解一个知识点,一个整数例如0x11223344,我们从左到右看分别是从高位到低位,我们可以这样理解,比如123,右边的3是个位,中间的2是十位,左边的1是百位,百位肯定比个位大,所以左边是高位,右边是低位,了解这个过后,我们再来看看大小端的存储模式

大端存储模式:是指数据的低位字节内容保存在内存的高地址处,而数据的高位字节内容,保存在内存的低地址处

小端存储模式:是指数据的低位字节内容保存在内存的低地址处,而数据的高位字节内容,保存在内存的高地址处

1.2练习(含大小端字节序判断)

(1)练习一

设计一个小程序来判断当前机器的字节序。(10分)--百度笔试题

方法一:

cpp 复制代码
#include <stdio.h>
int main()
{
	int i = 1;
	if (*((char*)&i) == 1)
		printf("小端\n");
	else
		printf("大端\n");
	return 0;
}

我们找到i的地址,并且强制类型转换为char*类型,接着解引用,访问其第一个字节的内容,我们知道i=1,在内存中存储用十六进制表示时是00 00 00 01,如果是大端存储,那依然是00 00 00 01,我们解引用访问第一个字节得到的结果就是0;但如果是小端存储,得到的就是01 00 00 00

方法二:

cpp 复制代码
#include <stdio.h>
//判断大小端函数
int check_sys()
{
	union
	{
		int i;
		char c;
	}un;
	un.i = 1;
	return un.c;
}
//主函数
int main()
{
	if (check_sys() == 1)
		printf("小端\n");
	else
		printf("大端\n");
	return 0;
}

我们创建了个匿名(联合体/共用体),里面有两个成员分别是int i和char c,然后创建了个联合体变量un,总体思路是,因为这个联合体总共大小为4个字节,我们给int i赋值为1,char c不赋值,再以un.c作为返回值返回,un.c访问的就是这个联合体占用内存的第一个字节,int i=1用十六进制表示出来00 00 00 01,如果un.c等于00,那就是大端存储,如果un.c等于01,那就是小端存储

(2)练习二

代码输出的结果是啥?

cpp 复制代码
#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是signed类型时,我们可以把char的取值想象成一个圆,里面存的是补码,右半圆从上往下顺时针方向开始,分别是从0~127,接着左半圆从下往上顺时针起,分别是 -128 ~ -1,接着又回到0,1,2......,所以128就等于-128,129就等于-127,所以假如当我们存入signed char i=135时,其实就是(135-128)+(-128),i的值其实就是-121,因为赋值 135超过了最大值 127 ,会自动溢出回负数

这个就是unsigned char类型的图,和上述规则很像,但这里的补码是无符号位的,所以从顺时针开始,一直从0~255,255是char类型的最大值,当char i=256时,其实i就是0了,会自动换算成0,char i=257时,i就是2,所以假如当我们存入unsigned char i=268时,其实就是268-255=13,i的值其实就是13,因为赋值268,超过了最大值 255 ,会自动溢出回正数数

总结:

signed char类型取值(-128~127)

unsigned char类型取值(0~255)

所以我们现在再去看上述的代码 char a= -1和signed char b=-1,因为有符号char类型的取值是**(-128~127),所以**它们的值就是-1,但unsigned char c=-1的取值是255,根据上述的图就能看出来,-1就是255,-2就是254

运行截图:

(3)练习三

代码输出的结果是啥?

代码一:

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

因为a是char类型,占一个字节,八个bit位,所以-128的补码是10000000,当char类型的值进行传参或运算时,首先要进行整型提升,有符号数高位补符号位,变成11111111 11111111 11111111 10000000,再以%u的形式打印,也就是以无符号整型打印,那么全部都是有效位,无符号位,11111111 11111111 11111111 10000000的值就是4294967168

运行截图:

代码二:

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

因为此时的char在VS中是有符号类型的char,所以可以看我们练习二中的signed char类型的图,里面存128会溢出变成-128,就等于我们存的是char a=-128,所以就和上述一样了,打印的是4294967168

运行截图:

(4)练习四

代码输出的结果是啥?

cpp 复制代码
#include <stdio.h>
#include <string.h>   // 必须加,否则 strlen 用不了
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,从-1开始往下递减,什么时候递减到ai的值为'\0',然后统计'\0'之前的字符个数,这是个有符号的char类型数据,我们结合练习二signed的图可以知道,-1一直递减到-128后,-129就是127,然后从127一直递减到0,-1到-128,一共有128个数,127到1,一共有127个数,所以'\0'之前就一共有128+127=255个数,会打印255

运行截图:

(5)练习五

代码输出的结果是啥?

代码一:

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

因为i是unsigned char类型,范围是0~255,所以当for (i = 0; i <= 255; i++),当i=256时,char类型装不下了,会溢出重新变为0,到511.....时又会变成0,所以这个for循环是个死循环,会一直打印"hello world\n"

运行截图:

代码二:

cpp 复制代码
#include <stdio.h>
#include <windows.h>
int main()
{
	unsigned int i;
	for (i = 9; i >= 0; i--)
	{
		printf("%u\n", i);
		Sleep(300);
	}
	return 0;
}

因为i是unsigned int类型,我们已知了unsigned char类型的规律,unsigned int和unsigned char的规律一样,就是范围不一样,int的范围更大,但我们for (i = 9; i >= 0; i--),i减到-1时,会溢出变为4294967295(unsigned int类型的最大值),i减到-100时,溢出变为巨大的正数.....,所以unsigned int 永远 ≥ 0,所以会死循环打印

运行截图:

(6)练习六

代码输出的结果是啥?

cpp 复制代码
#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\n", ptr1[-1], *ptr2);
	return 0;
}

int* ptr1 = (int*)(&a + 1),这串代码是将整个数组a的地址取了出来,+1跳过了整个数组,指向a4,然后我们强制类型转化为int*类型, 当ptr1-1时,也就是*(ptr1-1),因为ptr1是int*类型,减1减一个整型,到a3的位置,用%x打印出的结果就是4

int* ptr2 = (int*)((int)a + 1),a是数组首元素的地址,转化为int类型加1,也就是整型+1,跳过一个字节,我们再强制类型转化为int*类型,不转系统也会帮我们主动转,将(int*)((int)a + 1)的结果存入指针变量ptr2中,首字符在内存中存储的是01 00 00 00,接着a1存储的是02 00 00 00,它们在内存中是连续存储的,也就是10 00 00 00 02 00 00 00,当我们(int*)((int)a + 1)时,我们说了跳一个字节,然后我们再*ptr2,解引用访问四个字节,此时访问的就是00 00 00 02,因为是小端存储,所以真实的补码是02 00 00 00,用%x打印出的结果就是2000000

运行截图:

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

我们首先来看一串代码:这串代码的结果是什么?

cpp 复制代码
#include <stdio.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;
}

运行截图:

上面的代码中,num 和 *pFloat 在内存中明明是同一个数,为什么浮点数和整数的解读结果会差别这么大?这时候我们就要了解一下浮点数在内存中存储的知识了

2.1浮点数的存储

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

  • 代表的是该浮点数的符号位,当s等于0时,V为正数;当s等于1时,V为负数
  • 代表的是有效数字,大于1并且小于2
  • 代表指数位

举个例子:十进制的-5.125,二进制为-101.001,以上面这种形式来呈现,相当于(-1)¹ × 1.01001 × 2²,得到的S就等于1,M等于1.01001,E等于2

IEEE 754 规定:

对于 32 位的浮点数,最高的 1 位存储符号位 S,接着的 8 位存储指数 E,剩下的 23 位存储有效数字 M

对于 64 位的浮点数,最高的 1 位存储符号位 S,接着的 11 位存储指数 E,剩下的 52 位存储有效数字 M

2.2浮点数存储的过程

那浮点数,具体在内存中是怎么存储的呢?

S:S只占一个符号位,正数存0,负数存1

但IEEE 754标准对有效数字M指数E,还有一些特别规定

我们就以十进制的-5.125为例子,在float类型中,看看具体存在内存中是怎么存储的

M的规定:我们知道M表示的是有效数字,在计算机内部存储时,默认这个数的第一位总是1,是以1.xxxxxxx的形式储存的,所以我们会将前面的1.舍去,这样做的好处是,我们可以存储多一位有效数字,本来如果将1放进来,我们M的23个空间就只能存储23个有效数字,现在将1舍去,我们就可以存多一个有效数字,最后在计算时会主动把舍去的1.加上,不够有效数字时会往后补0,这样其实我们就能存的是24位有效数字;同样道理,那么如果是在64位下,存的就是53位有效数字

例如-5.125,它的M是1.01001,在内存上就会存0100 1000 0000 0000 0000 000,最后拿出来时会自动添1. 也就是1.01001000 0000 0000 0000 000

E的规定:E的规定就比较复杂,首先E是一个unsigned类型,是一个无符号数,它占8个bit位,所以可以表示的数的范围是0~255;如果E占11位,它的取值范围就是0~2047,但我们知道,在浮点数用科学计数法表示时,指数是可以出现负数的,例如存0.25,它的二进制表示是0.01,我们用科学计数法表示就是1.0 × 2⁻²,所以IEEE 754规定,存入内存时E的真实值必须再加上一个中间数,对于8位的E,这个中间数是127;对于11位的E,这个中间数是1023,比如当我们在8位的E中,存E=-2时,我们是先将-2+127=125,将这个125换算成二进制存储在这8位中,E就等于01111101

2.3浮点数取数的过程

指数E从内存中取出还可以再分成三种情况:

(1)E不全为0或不全为1

此时指数E的计算值减去127(或1023),就可以得到真实指数的值,再看符号位S和将M前面加上1.就能取出我们原本的数

例如0.5f在内存中是这样存储的:0 01111110 00000000000000000000000

01111110是它的8位指数位,换算成十进制为126,126-127等于-1,所以E就等于-1,符号位为0,所以S就等于0,后面的M加上1.就是1.00000000000000000000000,简写为1.0,所以最终这个数的科学计数法表示就是(-1)⁰ × 1.0 × 2⁻¹

(2)E为全0

浮点数的指数E就等于1-127(或者1-1023),这是IEEE 754 人为规定的,所以当E为全0时,8位的E真实值其实就是-126,11位的E真实值其实就是-1022;然后有效数字M不再加上1.而是还原为0.xxxxxx这样的小数。这样做是为了表示±0,以及接近于0的很小的数字

比如0 00000000 00100000000000000000000,此时E为00000000,是全0,E的真实值是-126,S为0,M为00100000000000000000000,前面添上0.为0.00100000000000000000000,简写为0.001,所以此数的科学计数法表示就是(-1)⁰ × 0.001 × 2⁻¹²⁶,这是一个非常小的数,无限接近于0的数,所以当我们E为全0时,我们的目的就是为了表示±0,或者无限接近于0的很小的数字

(3)E为全1

比如0 11111111 00010000000000000000000,此时E为11111111,是全1,E为全1时不再计算指数与M数值,只判断 2 种特殊情况

  • M 全 0 → 无穷大
  • M不全为 0 → NaN(非数值)

此处M等于00010000000000000000000,不为全0,所以此数表示NaN,全拼叫Not a Number,意思是非数值, 专门标记非法、无意义的运算结果,此处S无意义

如果M为全0,表示无穷大时: S 有效:S=0 是正无穷 ,S=1 是负无穷

2.4题目解析

接下来让我们再回看这个代码:

cpp 复制代码
#include <stdio.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;
}

**printf("n的值为:%d\n", n):**会以整型的视角去打印数字9,正常打印

**printf("*pFloat的值为:%f\n", *pFloat):**int n=9,我们取出n的地址,并强制类型转化为float*类型,存入字符指针变量pFloat中,n在内存中存储的二进制补码是00000000 00000000 00000000 00001001,此时我们访问*pFloat,就是以浮点数的视角来看待这串补码,那就是0 00000000 00000000000000000001001,S=0,M=00000000000000000001001,最重要的E=00000000,E为全0,真实值为-126,所以这串补码会被翻译成一个无限接近于0的数字:0.00000000000000000001001 × 2⁻¹²⁶,当我们将*pFloat用%f打印时,只显示前六位小数,所以printf("*pFloat的值为:%f\n", *pFloat)会打印出0.000000

**printf("*pFloat的值为:%f\n", *pFloat):**当我们将*pFloat = 9.0,将n的值改成9.0时,其实是以浮点数的方式,将9.0存入了n的内存中,9.0的二进制补码是1001.0,用科学计数法表示是1.001 × 2³,S就等于0,E等于3+127=130,存储的是130的补码,为10000010,最后M等于0010 0000 0000 0000 0000 000,所以我们此浮点数9.0在内存中以二进制形式存储的是0 10000010 0010 0000 0000 0000 0000 000,当我们以%d形式打印时,其实就是把它重新以整型的视角去看待为 01000001 00010000 00000000 00000000,这是一个很大的数,最高位为符号位0,为正数,剩下的1000001 00010000 00000000 00000000转化为十进制是1091567616,所以我们printf("num的值为:%d\n", n)会打印出1091567616

**printf("*pFloat的值为:%f\n", *pFloat):**这串代码就是将*pFloat在内存中以二进制形式存储的0 10000010 0010 0000 0000 0000 0000 000,以浮点数的规则取出并打印,取出后的数就是9.0,%f要打印六位小数,所以是9.000000

下面我们来再看一遍运行截图,来验证我们分析的结果:

感谢大家的观看,新人求互三,关注我必回关!下章见

相关推荐
2401_868534781 小时前
论企业网络设计
数据结构
wabs6662 小时前
关于贪心算法的一些自我总结【力扣45.跳跃游戏II】【灵感来源:代码随想录】
算法·贪心算法·复盘
2401_876964132 小时前
【湖北专升本】2026湖北专升本真题PDF+备考资料汇总
数据结构·人工智能·经验分享·深度学习·算法·计算机视觉
qq3862461963 小时前
更新补发第6天:7天学会C语言,每天5分钟,不需要基础
c语言·for循环·循环语句·while循环·do-while循环
嗝o゚3 小时前
CANN GE 算子融合——融合算法与调度策略
算法·昇腾·cann·ge
小江的记录本3 小时前
【JVM虚拟机】垃圾回收GC:垃圾回收算法:标记-清除、标记-复制、标记-整理、分代收集(附《思维导图》+《面试高频考点清单》)
java·jvm·后端·python·算法·安全·面试
Ulyanov4 小时前
用声明式语法重新定义Python桌面UI:QML+PySide6现代开发入门(一)
开发语言·python·算法·ui·系统仿真·雷达电子对抗仿真
数据科学小丫4 小时前
特征工程处理
人工智能·算法·机器学习
z落落5 小时前
C#参数区别
java·算法·c#