您现在的位置是:首页 >技术交流 >【C语言】深度剖析数据在内存中的存储网站首页技术交流

【C语言】深度剖析数据在内存中的存储

李昕泽的小仓库 2023-06-26 08:00:02
简介【C语言】深度剖析数据在内存中的存储

简单不先于复杂,而是在复杂之后。

89efcc89ac61428db4d5b6639b2bd948.jpeg

目录

 1. 数据类型介绍

1.1 类型的基本归类 

2.整型在内存中的存储 

2.1 原码、反码、补码 

2.2 大小端介绍 

2.3 练习 

2.3.1 练习1 

2.3.2 练习2 

3.2.3 练习3 

2.3.4 练习4 

2.3.5 练习5 

2.3.6  练习6

2.3.7 练习7 

2.3.8 对 strlen 类型 size_t 的补充 

 3. 浮点型在内存中的存储

3.1 一个例子 

3.2 浮点数存储规则 


 

 1. 数据类型介绍

 基本的内置类型

 

类型的意义:

1. 使用这个类型开辟内存空间的大小(大小决定了使用范围)

2. 如何看待内存空间的视角

1.1 类型的基本归类 

 整型家族:

long long 
  unsigned long long [int]
    signed long long [int]
    
int a; -----> signed int a;(short 、long 、long long 同理 )
    
char 到底是 signed char 还是 unsigned char 是标准未定义的,取决于编译器的实现。

char -  字符的本质是ASCLL码值,是整型。

生活中有些数据是没有负数的:身高、体重、长度等 
unsigned int high;

有正有负的情况下使用 signed int 或 int
符号位代表正负 不是有效位    符号位是0代表正数
                           符号位是1代表负数                            

浮点数家族:

浮点型家族:只要是表示小数就可以使用浮点型
float 的精度低,存储的数值范围较小,double精度高,存储的数据范围更大

构造类型:

自定义类型:我们可以创造出新的类型

//数组类型
int arr[5]; - 类型为int [5]
int arr2[8]; - 类型为int [8]
char arr2[5] - 类型为char [5]

指针类型:

空类型:

void 表示空类型(无类型)

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

2.整型在内存中的存储 

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

数据在开辟内存中究竟是如何存储的?

2.1 原码、反码、补码 

数值有不同的表示形式
2进制
8进制
10进制
16进制

十进制的21
0b10101
025
21
0x15

整数的2进制表示也有三种表示形式:
1.正的整数原码、反码、补码相同
2.负的整数原码、反码、补码是计算得来的
原码:直接通过正负的形式写出二进制序列
反码:符号位不变,其他位按位取反得到的就是反码
补码:反码+1就是补码

int main()
{
int a = 20;
20
000000000000000000000000000010100
ox00 00 00 14
000000000000000000000000000010100
000000000000000000000000000010100
int b = -10;
100000000000000000000000000001010 - 原码
ox80 00 00 0a
111111111111111111111111111110101 - 反码
0xff ff ff f5
111111111111111111111111111110110 - 补码
0xff ff ff f6

return 0;
}

 

内存中本质存放的是二进制,展示的形式是十六进制。

 对于整型来说数据存放内存中其实存放的是补码的二进制序列。

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

原因在于,使用补码,可以将符号位数值域统一处理

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

此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。

2.2 大小端介绍 

什么是大端小端:

大端【字节序】存储:

是指数据的低位【字节序】的内容保存在内存的高地址中,而数据的高位【字节序】的内容,保存在内存的低地址中。

小端【字节序】存储:

是指数据的低位【字节序】的内容保存在内存的低地址中,而数据的高位【字节序】的内容,保存在内存的高地址中。

注:在计算机中,【字节序】指的是在内存中多字节数据(如整数、浮点数等)的存储顺序。

对于多字节数据(如整数、浮点数等),存储顺序通常被划分为高位字节和低位字节。

高位字节是指数据中最高有效位所在的字节,而低位字节则是指数据中最低有效位所在的字节。

例如,对于16位的整数0xABCD,其中高位字节是0xAB,低位字节是0xCD。

地址是指计算机内存中的一个标识符,它用来表示内存中某个字节的位置。

高地址通常指内存中的较大位置,而低地址则是指内存中的较小位置。

为什么会有大端和小端?

为什么会有大小端模式之分呢?

这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为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处理器还可以 由硬件来选择是大端模式还是小端模式。

 百度2015年系统工程师笔试题:

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

(10分)

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>

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


	return 0;
}

首先定义了一个整型变量a并赋值为1,然后使用强制类型转换将a的地址转换为字符型指针。

接着通过解引用字符型指针来访问a的第一个字节,判断系统的字节序。

在小端系统中,最低有效字节存储在最低的内存地址上,因此当程序解引用字符型指针时,会读取到值为1的字节。

而在大端系统中,最高有效字节存储在最低的内存地址上,因此当程序解引用字符型指针时,会读取到值为0的字节。

在C语言中,内存单元的最小单位是字节(byte)。一个整型变量在内存中占用多个字节,具体占用多少字节取决于编译器和系统架构。而字符型变量只占用一个字节。

当将一个整型变量的地址转换为字符型指针时,实际上是将整型变量在内存中的第一个字节的地址转换为字符型指针。

这样做可以直接访问内存中的单个字节,而不是整个整型变量。

写成函数形式:

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>

int check_sys()
{
	int a = 1;
	if (*(char*)&a == 1)
		return 1;
	else
		return 0;
}

int main()
{
	int ret = check_sys();
	if (ret == 1)
		printf("小端
");
	else
		printf("大端
");

	return 0;
}

这是优化后的结果:

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>

int check_sys()
{
	int a = 1;
	return (*(char*)&a == 1);
}

int main()
{
	int ret = check_sys();
	if (ret == 1)
		printf("小端
");
	else
		printf("大端
");

	return 0;
}

2.3 练习 

2.3.1 练习1 

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

这个代码输出什么?

先看一下 signed char 在内存中如何存储:

 

 

 

2.3.2 练习2 

 

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>

int main()
{
	char a = -128;
	printf("%u", a);

	return 0;
}

3.2.3 练习3 

 

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>  
int main()
{
    //-128~127
    char a = 128;
    //00000000000000000000000010000000 - 原码 - 补码
    //截断 - 10000000 - a
    //11111111111111111111111110000000 - 提升
    //因为打印无符号整数,原码补码相同,直接打印
    printf("%u
", a);
    printf("%d
", a);
    //11111111111111111111111110000000 - 提升
        //10000000000000000000000001111111 - 反码
    //10000000000000000000000010000000 - 原码 - -128
    return 0;
}

2.3.4 练习4 

 

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>

int main()
{
	int i = -20;
	//100000000000000000000000000010100
	//111111111111111111111111111101011
	//111111111111111111111111111101100 - -20的补码
	//
	unsigned int j = 10;
	//000000000000000000000000000001010 - 10的补码

	printf("%d
", i + j);

	//按照补码的形式进行运算,最后格式化成为有符号整数
	//111111111111111111111111111101100 - -20的补码
	//000000000000000000000000000001010 - 10的补码
	//111111111111111111111111111110110 - 补码
	//100000000000000000000000000001001
	//100000000000000000000000000001010 -> -10

	return 0;
}

2.3.5 练习5 

 

 

unsigned int类型占用4个字节的内存空间,它的取值范围是0到4294967295,当循环变量i递减到0时,它的值已经达到了unsigned int类型的最小值0,再次递减时会发生整数下溢,即数值会从0变成4294967295(2的32次方-1)。

这是因为当整数类型下溢时,C语言规定它们将“回绕”到该类型的最大值减去1,然后继续减少。

因此,当循环变量i变为0时,下一次循环i--操作会将i从0变为4294967295,然后依次递减1。

可以用以下代码验证这一点:

unsigned int i = 0;
i--;  // i变为4294967295
printf("%u
", i);  // 输出4294967295

因此,当循环变量i递减到0后,它会继续递减,变成4294967295,然后依次递减1,直到程序异常结束。

2.3.6  练习6

 

 

 

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<string.h>

int main()
{
	char a[1000];
	int i;

	for (i = 0; i < 1000; i++)
	{
		a[i] = -1 - i;
	}
	//
	//arr[i] --> char  -128~127
	//-1 -2 -3 -4 ····· -128 127 126 ······3 2 1 0 -1····

	printf("%d", strlen(a));
	//strlen 是求字符串长度,关注的是字符串中''(数字0)之前出现多少字符

	return 0;
}

2.3.7 练习7 

 

 unsigned char 的取值范围是0~255,所以 i <= 255 的条件恒成立,就构成了死循环。

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>

unsigned char i = 0;

int main()
{
	for (i = 0; i <= 255; i++)
	{
		printf("hello,world
");
	}
	return 0;
}

2.3.8 对 strlen 类型 size_t 的补充 

 

 

 

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<assert.h>

int my_strlen(const char* str)
{
	assert(str);
	int count = 0;
	while (*str)
	{
		str++;
		count++;
	}
	return count;
}

int main()
{
	if (my_strlen("abc") - my_strlen("abcdef") > 0)
	{
		printf(">");
	}
	else
	{
		printf("<");
	}

	return 0;
}

 3. 浮点型在内存中的存储

常用的浮点数 :

 

 

3.1 一个例子 

 

3.2 浮点数存储规则 

 

 

 

 

 

指数 E 的存储分析完,我们还会由把 E 从内存中取出的需求。

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

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,则其二进制表示形式为:

 

2. E为全0

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

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

3. E为全1

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

会了以上这些就可以对刚开始的那段代码做出解释

 

 

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>

int main()
{
	int n = 9;
	//00000000000000000000000000001001 - 补码
	//0 00000000 00000000000000000001001
	//E=-126
	//M = 0.00000000000000000001001
	//+0.00000000000000000001001*2^-126
	//
	float* pFloat = (float*)&n;

	printf("n的值为:%d
", n);//9
	printf("*pFloat的值为:%f
", *pFloat);//0.000000

	*pFloat = 9.0;
	//1001.0
	//1.001*2^3
	//S=0  E=3  M=1.001
	//01000001000100000000000000000000
	printf("num的值为:%d
", n);//1091567616
	printf("*pFloat的值为:%f
", *pFloat);  //9.0

	return 0;
}

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。