【C语言】万字肝爆!建议收藏!深度剖析数据在内存中的存储

本文涉及的产品
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
简介: 数据在内存中到底是怎么存储的呢?相信看完这篇文章你的内功又会增加一点。

前言

数据在内存中到底是怎么存储的呢?相信看完这篇文章你的内功又会增加一点。


本文重点:

1. 数据类型详细介绍

2. 整形在内存中的存储:原码、反码、补码

3. 大小端字节序介绍及判断

4. 浮点型在内存中的存储解析


### 正文开始

---
我们大家最容易接触的内置类型:

*char //字符数据类型*
short //短整型
int //整形
**long //长整型
long long //更长的整形
float //单精度浮点数
double //双精度浮点数**

那么这些类型有什么意义呢?

  1. 确认这些类型在内存开辟的空间大小,比如 int类型 4字节
  2. 看待内存的视角,int类型是整形,float类型是浮点型

类型的基本归类

- 整形家族:

char
unsigned char
signed char
short
unsigned short [int]
signed short [int]
int
unsigned int
signed int
long
unsigned long [int]
signed long [int]

为什么char类型放在整形里面?

其实字符类型在底层存储,存的是ASCII码值,ASCII码值也是整数

unsigned(无符号)

未来你要创建一个变量,变量只会存正数,没有可能有负数的情况,那么你可以创建一个unsigne的类型

signed (有符号)

有符号位,也就是有正负之分

- 浮点类型家族:

float
double

- 构造类型家族(自定义类型):

结构体类型 struct
枚举类型 enum
联合类型 union
数组类型

为什么数组类型也是自定义类型?

定义一个数组 int arr [10] ,我们知道数组去掉它的数组名剩下的就是类型了, int [10] <--- 这个就是我们自定义的类型

---
定义int arr [ 5 ],去掉数组名int [5],可以看见他们都是int类型,但是int [10]和int[5] 不一样,所以说数组类型也属于自定义类型

- 指针类型:

float*p
char*p
void*p
int*p

- 空类型:

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

整形在内存中的存储

我们之前讲过一个变量的创建是要在内存中开辟空间的。空间的大小是根据不同的类型而决定的。

那接下来我们谈谈数据在所开辟内存中到底是如何存储的?
int a = -10;

问题一:看了上面的代码我们知道 a 是int类型占4个字节,那在如何存储的呢?

首先我们要来了解下面的概念:

数据在内存中以二进制的形式存储
计算机中的 有符号数有三种表示方法,即 原码反码补码

三种方法都有符号位数值位

  • 符号位 : 符号位都是用0表示“正”,用1表示“负”
  • 数值位 : 数值位三种表示方法各不相同

对于整数来说:

整数的二进制有3种表示形式:原码,反码,补码

正整数: 原码,反码,补码相同

负整数: 原码,反码,补码要进行计算

  • 原码:按照数值直接写出来的二进制序列就是原码
  • 反码:原码的符号位不变,其他位按位取反,得到的就是反码
  • 补码:反码加1,就是补码

好了,知道以上的一些概念,让我们回到问题一看看我们的int a在内存是怎么存储的

  • 先编译器取A的内存

输入&a 得到 f6 ff ff ff
在这里插入图片描述
为什么是 f6 ff ff ff 呢?让我们来按照上面的步骤来探究一下:

  • 获取a的原码

-10
原码:10000000 00000000 00000000 00001010
在这里插入图片描述

  • 获取a反码

反码:11111111 11111111 11111111 11110101
在这里插入图片描述

  • 获取a的补码

反码: 111111111 111111111 111111111 11110110

上面说到为什么是f6 ff ff ff
对于整形来说:数据存放内存中其实存放的是补码,怎么证明呢?

在这里插入图片描述
可以看见把刚刚计算出来的补码放在计算器里面得到的就是一个16进制的FFFFFFF6 ,只是顺序有点倒过来了,不过的确可以证明对于整形来说:数据存放内存中其实存放的是补码


问题二: 为什么要存补码呢?为什么不存原码呢?

**在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统一处理; 同
时,加法和减法也可以统一处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程是相同的,不需
要额外的硬件电路。**

其实就是说补码补码相加才可以得到一个准确的数值,看不懂上面的?没事

来个代码:1-1 因为CPU只有加法器 , 所以就是1+(-1)

1-1

1+(-1)  (CPU只有加法器)

我们试一下 错误的原码相加会等到正确的值吗?

1 的原反补相同:00000000 000000000 00000000 000000001
-1的原码 : 10000000 000000000 00000000 000000001

二进制原码相加:
00000000 00000000 00000000 00000001
10000000 00000000 00000000 00000001

得到下面这个数:
10000000 00000000 00000000 00000010
认为这个是正确的数,那我们读出来就是 -2 了,1 - 1 = 0,最后是-2肯定是错误的。

我们试一下 正确的补码相加会等到正确的值吗?

1的原反补相同:00000000 000000000 00000000 000000001

来算一下负1的补码:

-1的原码:10000000 00000000 00000000 00000001

-1的反码:1111 1111 1111 1111 1111 1111 1111 1110

-1的补码:1111 1111 1111 1111 1111 1111 1111 1111

二个数的补码相加:
在这里插入图片描述

得到 :10000000 00000000 00000000 000000000
32个0,一个1 ,我们实际上只可以放32个二进制数,那个进1的那位要丢掉,所以最后得到全0,而 1-1 的 正确结果也是 0.
在这里插入图片描述

这就是为什么要补码相加,而不是原码,或者反码相加的原因。


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

这句话怎么理解呢?看下面的案列:

在这里插入图片描述

-1的补码要是想得到原码,可以反过来计算,补码-1,然后按位取反就可以得到。

那其实还有一种方法可以验证我们上面引用的话:

我们把补码当原码,在过一次计算一次依然可以得到我们的原码.

在这里插入图片描述
是不是和上面的原码一样了,这就解释了上面所说的话了。


大端小端

为什么要有大端小端:

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

啊这,太官方话了。。。。 简单说就是你的数值大于一个字节,就会有字节存储顺序,所以要安排怎么把这些字节按照什么顺序放进去


什么是大端小端:

大端(存储)模式: 是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中;
小端(存储)模式: 是指数据的低位保存在内存的低地址中,而数据的高位,,保存在内存的高地址中。

来个代码案列:

定义了一个变量:

在这里插入图片描述

来看一下这个a在本机是什么存储模式,是大端存储模式,还是小端存储模式

我们画个图,模拟一下这个变量在内存中是怎么样存储的。

在这里插入图片描述

为什么说上面的是大端存储模式,下面是小端存储模式呢?

可以看见0x11223344 最后面的44是低位,然后在大端存储模式下44是放在内存的高地址位,根据上面的概念可以得出,大端模式,低位在内存的高地址存储就是大端模式

为什么下面是小端存储模式:
0x11223344 44是放在内存的低地址处,根据上面的概念可以得出,小端模式,低位在内存的低地址存储就是小端模式


那么我们当前的电脑上使用的是什么存储模式呢?来让我们一起看看
在这里插入图片描述
诶!!!大家可以看见原来在本机上使用的是小端存储!

在这里插入图片描述
还记得这个FFFF FFF6吗,也是小端存储模式,所以说为什么有时候看内存是倒着的,就是因为这个大小端原因。

来做一道百度2015年的笔试题:

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

答案:简述的概念我们就不讲了,来看一下小程序吧

思路:我们只要判断第一个字节是小端应该出现的字节就是小端,是大端应该出现的字节就是大端,

我们先看一下1在小端内存是怎么表示的:
在这里插入图片描述
可以看见第一个字节是01 也就是1 所以我们代码看一下写成这样判断

int main()
{
    //写代码判断当前字节序
    int a = 1;     //在小端应该是这样的存储 01 00 00 00
    char* b =(char*) &a;  //取出第一个字节的内容 
    if (*b==1) {
        printf("小端");

    }
    else {
        printf("大端");
    }
    return 0;
}

也可以写成函数的那样:

int check_sys()
{
    int a = 1; 
    return (*(char*)&a);  //因为返回的是1 或者0 也是&a的内容 所以可以直接rerunt
}
int main()
{
    int ret  =    check_sys();
    if (ret == 1 )
    {
        printf("小端");

    }
    else
    {
        printf("大端");

    }
    return 0;
}

做题目巩固

第一题:

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

答案:-1 , -1 ,255

解析:(a)-1的由来

-1在内存中存的是补码
-1的原码:10000000000000000000000000000001
-1的反码:11111111111111111111111111111110
-1的补码:11111111111111111111111111111111
因为a是char类型只可以放8个bit为所以a里面应该存的是 8个1
再因为输入打印是以%d类型打印的所以要发生整形提升,有符号位发生提升前面全补1
11111111变成11111111111111111111111111111111 ---这个是补码
还要变成原码的方式输出出去:

a的补码11111111111111111111111111111
a的反码11111111111111111111111111110
a的原码10000000000000000000000000000001
最后打印的是原码:-1

b其实也是一样的有符号的-1

解析(c)255的由来
因为c是unsigned char类型只可以放8个bit为所以a里面应该存的是 8个1。
再因为输入打印是以%d类型打印的所以要发生整形提升,无符号位发生提升前面全补0。

11111111变成00000000000000000000000011111111 ---这个是补码
看符号位是0,判断它是一个正数,所以正数的原反补相同,最后已原码打印出来的是8个1 也就是255
在这里插入图片描述
补充:char 到底是signed char 还是unsigned char ?
答:C语言标准并没有规定,取决于编译器
int 是 signed int

第二题:

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

答案是:4294967168

解析:

a的原码:10000000000000000000000010000000
a的反码:11111111111111111111111101111111
a的补码:11111111111111111111111110000000

因为a是char类型,所以a里面存放的应该是8个bit位: 10000000
以%u形式输出出来,要发生整形提升,看见a是char类型是有符号的,高位补1,得到:11111111111111111111111110000000,最后发现是以%u的类型打印出来,可%u是无符号类型,就意味着,这是个正数,正数的原反补相同,所以是168结尾的大数字
在这里插入图片描述

第三题:

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

答案:4294967168

解析:和上面一样
注意:char a =128, 其实a不可能放一个128,127后就是-128了

补充(char的取值范围)

char取值范围是:-128 ~ 127

为什么?

画图解释一下:

char是8个bit位,所以在内存应该是这样的:
在这里插入图片描述
因为01111111已经是最大的正数了,在加1最高位变成1的话,那就变成负数了,01111111 == 127。

在这里插入图片描述

如果01111111加1那么就变成了10000000,当然这个数并不是0,而是-128,遇见一个1,七个0都是-128,因为这里的二进制数不可以计算原反补码了,但是其他的就不一样。(负数是要进行原反补的)
在这里插入图片描述
补码:11111110通过原反补的计算得到 -2
反码:11111101 补码减1得反码
原码:1000010 == -2 反码取反得原码

第四题:

int i= -20;
unsigned int j = 10;
printf("%d\n", i+j);

答案:-10
解析:求出-20 和 10的补码相加 然后%d打印认为是有符号,还要计算出原码打印出来。

i的原码:10000000000000000000000000010100
i的反码:11111111111111111111111111101011
i的补码:11111111111111111111111111101100
j的补码:00000000000000000000000000001010

在这里插入图片描述

因为%d打印的所以还要取出原码
在这里插入图片描述

最终是-10

第五题

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

答案:死循环

解析: 因为i是一个无符号数字,对于一个无符号数字 最小是0,判断条件刚好是大于等于0,所以恒成立“,所以死循环

第六题

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

答案 :255

解析:strlen找到 \0 停止, 从-1 到 127 因为char的范围是-128~127 所以128+127 = 255
在这里插入图片描述
第七题

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

答案: 死循环
解析:无符号的char 取值符号是255 没有正反区分,条件刚好是大于等于255 ,所以死循环
在这里插入图片描述

本文结束,感谢大家的观看!!!!感觉对你有用的话,不妨点个大拇指

相关文章
|
3天前
|
存储 程序员 编译器
C 语言中的数据类型转换:连接不同数据世界的桥梁
C语言中的数据类型转换是程序设计中不可或缺的一部分,它如同连接不同数据世界的桥梁,使得不同类型的变量之间能够互相传递和转换,确保了程序的灵活性与兼容性。通过强制类型转换或自动类型转换,C语言允许开发者在保证数据完整性的前提下,实现复杂的数据处理逻辑。
|
6天前
|
传感器 人工智能 物联网
C 语言在计算机科学中尤其在硬件交互方面占据重要地位。本文探讨了 C 语言与硬件交互的主要方法,包括直接访问硬件寄存器、中断处理、I/O 端口操作、内存映射 I/O 和设备驱动程序开发
C 语言在计算机科学中尤其在硬件交互方面占据重要地位。本文探讨了 C 语言与硬件交互的主要方法,包括直接访问硬件寄存器、中断处理、I/O 端口操作、内存映射 I/O 和设备驱动程序开发,以及面临的挑战和未来趋势,旨在帮助读者深入了解并掌握这些关键技术。
27 6
|
7天前
|
存储 数据建模 程序员
C 语言结构体 —— 数据封装的利器
C语言结构体是一种用户自定义的数据类型,用于将不同类型的数据组合在一起,形成一个整体。它支持数据封装,便于管理和传递复杂数据,是程序设计中的重要工具。
|
13天前
|
存储 编译器 数据处理
C 语言结构体与位域:高效数据组织与内存优化
C语言中的结构体与位域是实现高效数据组织和内存优化的重要工具。结构体允许将不同类型的数据组合成一个整体,而位域则进一步允许对结构体成员的位进行精细控制,以节省内存空间。两者结合使用,可在嵌入式系统等资源受限环境中发挥巨大作用。
36 11
|
2月前
|
监控 算法 应用服务中间件
“四两拨千斤” —— 1.2MB 数据如何吃掉 10GB 内存
一个特殊请求引发服务器内存用量暴涨进而导致进程 OOM 的惨案。
|
1月前
|
C语言
【c语言】动态内存管理
本文介绍了C语言中的动态内存管理,包括其必要性及相关的四个函数:`malloc`、``calloc``、`realloc`和`free`。`malloc`用于申请内存,`calloc`申请并初始化内存,`realloc`调整内存大小,`free`释放内存。文章还列举了常见的动态内存管理错误,如空指针解引用、越界访问、错误释放等,并提供了示例代码帮助理解。
45 3
|
2月前
|
存储 C语言
数据在内存中的存储方式
本文介绍了计算机中整数和浮点数的存储方式,包括整数的原码、反码、补码,以及浮点数的IEEE754标准存储格式。同时,探讨了大小端字节序的概念及其判断方法,通过实例代码展示了这些概念的实际应用。
65 1
|
4月前
|
存储 编译器 C语言
【C语言篇】数据在内存中的存储(超详细)
浮点数就采⽤下⾯的规则表⽰,即指数E的真实值加上127(或1023),再将有效数字M去掉整数部分的1。
395 0
|
2月前
|
存储
共用体在内存中如何存储数据
共用体(Union)在内存中为所有成员分配同一段内存空间,大小等于最大成员所需的空间。这意味着所有成员共享同一块内存,但同一时间只能存储其中一个成员的数据,无法同时保存多个成员的值。
|
2月前
|
存储 弹性计算 算法
前端大模型应用笔记(四):如何在资源受限例如1核和1G内存的端侧或ECS上运行一个合适的向量存储库及如何优化
本文探讨了在资源受限的嵌入式设备(如1核处理器和1GB内存)上实现高效向量存储和检索的方法,旨在支持端侧大模型应用。文章分析了Annoy、HNSWLib、NMSLib、FLANN、VP-Trees和Lshbox等向量存储库的特点与适用场景,推荐Annoy作为多数情况下的首选方案,并提出了数据预处理、索引优化、查询优化等策略以提升性能。通过这些方法,即使在资源受限的环境中也能实现高效的向量检索。