数据在内存中的存储

简介:

整数在内存中的存储

整数的2进制表示方法有三种,原码、反码和补码

原码、反码和补码是用于表示有符号整数的三种方式。


原码:有符号整数的原始表示形式。它的最高位是符号位(0代表正数,1代表负数),其余位表示数值的绝对值。


反码:对于正数,反码就是原码本身;对于负数,反码是对原码除了符号位以外的所有位取反(0变为1,1变为0)。


补码:对于正数,补码就是原码本身;对于负数,补码是对该数的反码加1。


下面我们来举一个例子:


假设我们使用8位表示有符号整数:


对于 +3(原码:00000011),+3的原码、反码和补码都是00000011。

对于 -3,原码为10000011,反码为11111100,补码为11111101(负数的反码由原码的符号位不变,其余位置取反得到,补码为反码加1得到)

反码和补码的存在是为了解决原码的加减法运算问题。使用补码,可以通过简单的加法运算来实现有符号整数的加减法,而不需要单独处理符号位。补码的另一个重要特性是,一个数的补码加上它的补码应该等于零。


在计算机中,通常使用补码来表示和存储有符号整数,因为它可以简化算术运算。


部分类型数据的存储

在内存中,整数的存储通常是以二进制形式表示的。整数占用的存储空间取决于其数据类型的位数。在大多数系统中,整数通常以补码形式存储。


例如,在C语言中,常见的整数类型如下:


char:通常占用1个字节(8位),可以表示-127到127之间的整数(带符号)或0到255之间的整数(无符号)。

short:通常占用2个字节(16位),可以表示-32767到32767之间的整数(带符号)或0到65535之间的整数(无符号)。

int:通常占用4个字节(32位),可以表示-2147483647到2147483647之间的整数(带符号)或0到4294967295之间的整数(无符号)。

long long:通常占用8个字节(64位),可以表示更大范围的整数。

整数在内存中的存储是直接以其二进制表示形式存储的。例如,十进制数19在内存中的存储形式可能是00010011(假设使用8位的存储空间)。整数的存储形式还取决于计算机的字节序,即大端序(高位字节存储在低地址)或小端序(高位字节存储在高地址)。


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

我们以一个数据为开始,来观察它在内存中的存储


#include 
int main()
{
 int a = 0x11223344;
 return 0;
}

我们会发现,在内存中,它是倒着存储的。由此,引出大小端:


在大端字节序中,整数的高位字节存储在内存的低地址处,而低位字节存储在内存的高地址处。换句话说,整数的最高有效位存储在最低的地址,最低有效位存储在最高的地址。这种方式符合我们阅读整数的习惯,也使得多字节整数在内存中的表示更加直观。

而在小端字节序中,整数的低位字节存储在内存的低地址处,高位字节存储在内存的高地址处。整数的最高有效位存储在最高的地址,最低有效位存储在最低的地址。相比大端字节序,小端字节序在内存中的表示可能会更加符合硬件架构的特点,但是在习惯方面可能会有些令人困惑。


上述例子中(0x11223344):其中0x11是最高有效的字节,0x44是最低有效的字节。

当表示为大端字节序时,0x11223344会被存储为:


0x11 0x22 0x33 0x44


而在小端字节序时,0x11223344会被存储为:


0x44 0x33 0x22 0x11


有排序之分,是因为内存的存储以字节为单位,每个字节占八个比特位,而像整形为四个字节,在存储中必然会有排序问题,


那么,如何判断当前编译器环境下的大小端顺序呢?


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


这段代码的原理是,在内存中使用一个整型变量n,然后通过将n的地址强制转换为指向char类型的指针,接着对这个char类型指针所指向内存的内容进行判断。如果这个地址的第一个字节存储的是1,那么说明这个系统是小端序;如果第一个字节存储的是0,那么说明这个系统是大端序。


有关整形提升与无符号整形的存储等问题

我们以一道题为例,来展开我们的内容:


#include 
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占一个字节,而打印的为整数为四个字节,这里就要引入整形提升的知识点


整形提升

整形提升是指将较小的整数类型转换为较大的整数类型的过程,在c语言中,当对较小的整数类型进行算朑运算时,这些值会被自动提升为较大的整数类型再进行运算。这是为了避免精度丢失和提升计算的精度。

举个例子,如果有一个char类型的变量和一个int类型的变量,进行加法运算的时候,char类型的值会被提升为int类型再进行运算。这是因为int类型通常比char类型更大,所以把char类型提升为int类型可以避免精度丢失。

另外,如果有一个int类型的变量和一个unsigned int类型的变量进行运算,int类型的值会被提升为unsigned int类型再进行运算,这是为了避免带符号数和无符号数混合运算时的问题。

那么整形提升的规则是什么呢?

1. 有符号整数提升是按照变量的数据类型的符号位来提升的

2. 无符号整数提升,高位补0

eg:


//负数的整形提升
char c1 = -1;
变量c1的⼆进制位(补码)中只有8个⽐特位:
1111111
因为 char 为有符号的 char
所以整形提升的时候,⾼位补充符号位,即为1
提升之后的结果是:
11111111111111111111111111111111
//正数的整形提升
char c2 = 1;
变量c2的⼆进制位(补码)中只有8个⽐特位:
00000001
因为 char 为有符号的 char
所以整形提升的时候,⾼位补充符号位,即为0
提升之后的结果是:
00000000000000000000000000000001
//⽆符号整形提升,⾼位补0


例题1

所以,回到上面的那道题

1. #include 
2. int main()
3. {
4.  char a= -1;
5.  signed char b=-1;
6.  unsigned char c=-1;
7.  printf("a=%d,b=%d,c=%d",a,b,c);
8.  return 0;
9. }


a的原码如下:


10000001


得到反码补码:


1111110
1111111


打印时,转换为整形,发生整形提升:


111111111111111111111111


得到原码为:


100000000000000000000001


所以a打印值为-1;

同理,b打印值也为-1;

而对于c:

无符号字符的范围是 0 到 255。当你将 -1 赋值给无符号字符时,它会被转换为无符号数,即 255(内存中的表示为 11111111),其转换如下:

-1 是一个整数字面值,它通常由编译器当作 int 类型处理,因此它在内存中的表示(假设 int 是32位)按照补码将是 11111111 11111111 11111111 11111111,它代表十进制中的 -1。

** 当这个 -1 被赋值给一个 unsigned char 变量时,它需要转换成一个无符号的8位值。 unsigned char 类型仅使用值的低8位,进行了截断,所以 -1 的低8位是 11111111。

这8位被直接截断并复制到 unsigned char 类型的变量 c 中。

由于 c 是一个 unsigned char 类型,这8位 11111111 就被解释为无符号整数值,即 255。在无符号数中,11111111 的二进制表示就是十进制中的 255。


所以对c进行整形提升后,用0补位


00000000000000000000000011111111


符号位为0;

则原返补码相同,则打印值为255;


例题2

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


这里出现了一个类型不匹配的问题。%u 是用来打印无符号整数的格式说明符,而 a 是有符号的 char 类型。在这种情况下,会发生隐式的整形提升。


整形提升规则表明当 char 类型(假设它是有符号的)参与到表达式中时,它会提升为 int 类型以执行运算。所以,-128 会被提升为 int 类型的 -128。


在32位系统上,-128 的 int 表示为:


11111111 11111111 11111111 10000000

(补码表示)


然而,由于使用了 %u 这个无符号整数的格式说明符,printf 将会把 a 的值当作无符号数来解释和打印。

所以打印结果如下


4294967168


例题3

1. #include 
2. int main()
3. {
4.  char a = 128;
5.  printf("%u\n",a);
6.  return 0;
7. }

首先,需要注意的是,char 类型的范围通常是 -128 到 127(假设 char 是有符号的且占用1个字节)。当你尝试将 128 赋值给 char 类型的变量时,会发生溢出。在这种情况下,128 实际上会被解释为有符号 char 的 -128(补码形式是 10000000)


进行整形提升后,-128 会被提升为 int 类型的 -128。在32位系统上,-128 的 int 表示为:


11111111 11111111 11111111 10000000


我们会发现,结果与例题二相同:


4294967168


关于char,unsigned char数据的周期与规律

在C语言中,char 和 unsigned char 类型的数据大小由其位数定义,通常是 8 位或者 1 字节。这意味着这些类型可以有固定数量的可能值,它们的表示范围是有界的,因此当它们的值超出这个范围时会出现周期性的回绕行为——这通常称为溢出。这里是 char 和 unsigned char 溢出的行为规律:


1。char

当我们讨论 char 的周期和规律时,假设 char 类型是有符号的,并且我们使用的机器是 8 位字符的系统。


可能的值范围:-128 到 127。

溢出规律:当 char 增加超过 127 时,它会回绕到 -128,进一步增加则继续从 -127 向上增加;当 char 减少低于 -128 时,它会回绕到 127,进一步减少则继续从 126 向下减少。

2. unsigned char

unsigned char 类型总是无符号的,也通常是 8 位。


可能的值范围:0 到 255。

溢出规律:当 unsigned char 增加超过 255 时,它会回绕到 0,进一步增加则继续从 1 向上增加;当 unsigned char 减少低于 0 时(在C中通过操作导致负数赋值给无符号类型),它会回绕到 255,进一步减少则继续从 254 向下减少。

步骤说明

考虑以下情况,我们对 char 和 unsigned char 类型的变量递增或递减操作:


对 char 类型递增:

初始化 char 变量,例如 char c = 120;。

递增变量 c,一直到它接近边界值 127。

当 c 达到 127 并且再次递增时,它变成 -128(回绕)。

继续递增将会得到 -127,-126,…,直到回到 127 再次开始一个周期。

对 unsigned char 类型递增:

初始化 unsigned char 变量,例如 unsigned char uc = 250;。

递增变量 uc,一直到它接近边界值 255。

当 uc 达到 255 并且再次递增时,它变成 0(回绕)。

继续递增将会得到 1,2,…,直到回到 255 再次开始一个周期。

这种周期性行为是底层数据类型和算术操作直接的结果。这也说明了为什么在实际编程中很重要的一点,那就是确保不会意外地造成数据类型溢出,因为这会导致不可预期的行为。


在此基础上,我们看下面的例题:


例题4

1. #include 
2. int main()
3. {
4.  char a[1000];
5.  int i;
6.  for(i=0; i<1000; i++)
7.  {
8.  a[i] = -1-i;
9.  }
10.  printf("%d",strlen(a));
11.  return 0;
12. }

我们了解到,char类型的数据在从-1一直减,知道-128,再减一得到127,继续减,得到0,形成循环

而strlen在遇到’\0’结束,所以上述的结果则为,128+127=255;


例题5

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


这道题,首先i为无符号char类型的数据,在i从零加到255后,再加一发生溢出,得到0,所以i恒小于255,程序陷入死循环。


例题6

#include 
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取出整个数组的地址,加一跳过整个数组。指针由其实位置指向末尾

以16进制的结果打印,假设为小端存储,则上述图形可转化如下:

ptr【-1】;即为*(ptr-1),

此时ptr减一指向04起始位置,解引用,打印的结果即为


4


而对于ptr2

int* ptr2 = (int*)((int)a + 1); 这行代码使 ptr2 指向数组 a[0](即数字 1)的首地址向前移动了 1 个字节。因此,ptr2 实际上指向的是 a[0] 的第二个字节。由于 ptr2 是 int* 类型的指针,当你对其进行解引用操作(*ptr2)时,它将尝试读取四个字节作为一个整数。

所以,*ptr2 解引用后的值应该是 0x02000000


浮点数在内存中的存储

同样以一道例题为开始


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


你认为结果是什么呢?

我们运行结果如下

1. n的值为:9
2. *pFloat的值为:0.000000
3. num的值为:1091567616
4. *pFloat的值为:9.000000


所以为什么第三行第四行的结果可能与预想不同呢?

这正是因为浮点数在内存中存储的特殊性


浮点数在内存中的存储遵循IEEE 754标准,是目前最广泛使用的浮点数表示方法。


任意⼀个⼆进制浮点数V可以表⽰成下⾯的形式:

V=(-1)S×M×2E.

(-1)S表示符号位,当S=0,V为正数;当S=1,V为负数

M 表示有效数字,M是大于等于1,小于2的

2E表示指数位

单精度浮点数(32位):包括1位符号位,8位指数位,和23位尾数位。


双精度浮点数(64位):包括1位符号位,11位指数位,和52位尾数位。

举例来说:

⼗进制的5.0,写成⼆进制是 101.0 ,相当于 1.01×2^2 。

那么可以得出S=0,M=1.01,E=2。

在计算机内部保存M时,默认这个数的第⼀位总是1,因此可以被舍去,只保存后⾯的

xxxxxx部分。⽐如保存1.01的时候,只保存01,等到读取的时候,再把第⼀位的1加上去。

我们只需要存储后面的部分,即 (01)。我们将这个二进制数扩展到23位,剩下的位用0填充。所以尾数位为 (01000000000000000000000)。

符号位首位则为0;

至于E,E为无符号整数,如果E为8位,它的取值范围为0-255;如果E为11位,它的取值范围为0~2047,但是指数位可能出现负数,所以IEEE 754规定,存⼊内存时E的真实值必须再加上

⼀个中间数,对于8位的E,这个中间数是127;对于11位的E,这个中间数是1023。

所以实际存储的指数值为 (2 + 127 = 129)。129的二进制形式为 (10000001)

将这些部分组合起来,得到5.0的IEEE 754单精度浮点数表示为:


0 10000001 01000000000000000000000


浮点数取的过程

当从内存中取出IEEE 754标准浮点数的指数部分时,可以将其分为以下三种情况

E不全为0或不全为1

这意味着这些指数值代表了有效的浮点数。在解析指数时,需要从其值中减去偏移量以得到实际的指数值。

对于32位的单精度浮点数,偏移量是127。如果指数的8位不全是0或1,例如10000001,则实际指数 (E = 129 - 127 = 2)。

对于64位的双精度浮点数,偏移量是1023。如果指数的11位不全是0或1,则类似地减去1023得到实际的指数值。

再将有效数字M前加上第⼀位的1

E全为0

这种情况下,浮点数表示接近于0的非常小的数

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

特殊值

如果指数部分全部为1,则表示该数为特殊值。在这种情况下,尾数的不同值表示不同的特殊情况:


无穷大:如果尾数全为0,那么该值表示无穷大。符号位决定了是正无穷还是负无穷。

非数:如果尾数不全为0,那么该值表示非数.


最后,回到开始的那道题


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


9以整型的形式存储在内存中,得到如下⼆进制序列:


0000 0000 0000 0000 0000 0000 0000 1001


将 9 的⼆进制序列按照浮点数的形式拆分,得到第⼀位符号位s=0,后面8位的指数

E=00000000 ,

最后23位的有效数字M=000 0000 0000 0000 0000 1001。

由于指数E全为0,所以符合E为全0的情况。

因此,V是⼀个很小的接近于0的正数,所以用十进制小数表示就是0.000000。

浮点数9.0 等于⼆进制的1001.0,即换算成科学计数法是:1.001×2^3

9.0 = (−1)0×(1.001) × 23 第⼀位的符号位S=0,有效数字M等于001后⾯再加20个0,凑满23位,指数E等于3+127=130,即10000010


0 10000010 001 0000 0000 0000 0000 0000


这个浮点数在被当做整形的时候,得到的值即为1091567616


感谢观看!求点赞和关注!


相关文章
|
22天前
|
监控 算法 应用服务中间件
“四两拨千斤” —— 1.2MB 数据如何吃掉 10GB 内存
一个特殊请求引发服务器内存用量暴涨进而导致进程 OOM 的惨案。
|
21天前
|
存储 C语言
数据在内存中的存储方式
本文介绍了计算机中整数和浮点数的存储方式,包括整数的原码、反码、补码,以及浮点数的IEEE754标准存储格式。同时,探讨了大小端字节序的概念及其判断方法,通过实例代码展示了这些概念的实际应用。
44 1
|
26天前
|
存储
共用体在内存中如何存储数据
共用体(Union)在内存中为所有成员分配同一段内存空间,大小等于最大成员所需的空间。这意味着所有成员共享同一块内存,但同一时间只能存储其中一个成员的数据,无法同时保存多个成员的值。
|
28天前
|
监控 Java easyexcel
面试官:POI大量数据读取内存溢出?如何解决?
【10月更文挑战第14天】 在处理大量数据时,使用Apache POI库读取Excel文件可能会导致内存溢出的问题。这是因为POI在读取Excel文件时,会将整个文档加载到内存中,如果文件过大,就会消耗大量内存。以下是一些解决这一问题的策略:
67 1
|
30天前
|
存储 弹性计算 算法
前端大模型应用笔记(四):如何在资源受限例如1核和1G内存的端侧或ECS上运行一个合适的向量存储库及如何优化
本文探讨了在资源受限的嵌入式设备(如1核处理器和1GB内存)上实现高效向量存储和检索的方法,旨在支持端侧大模型应用。文章分析了Annoy、HNSWLib、NMSLib、FLANN、VP-Trees和Lshbox等向量存储库的特点与适用场景,推荐Annoy作为多数情况下的首选方案,并提出了数据预处理、索引优化、查询优化等策略以提升性能。通过这些方法,即使在资源受限的环境中也能实现高效的向量检索。
|
1月前
|
缓存 安全 Java
使用 Java 内存模型解决多线程中的数据竞争问题
【10月更文挑战第11天】在 Java 多线程编程中,数据竞争是一个常见问题。通过使用 `synchronized` 关键字、`volatile` 关键字、原子类、显式锁、避免共享可变数据、合理设计数据结构、遵循线程安全原则和使用线程池等方法,可以有效解决数据竞争问题,确保程序的正确性和稳定性。
36 2
|
1月前
|
存储 编译器
数据在内存中的存储
数据在内存中的存储
41 4
|
1月前
|
存储 Java
JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配
这篇文章详细地介绍了Java对象的创建过程、内存布局、对象头的MarkWord、对象的定位方式以及对象的分配策略,并深入探讨了happens-before原则以确保多线程环境下的正确同步。
53 0
JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配
|
4月前
|
存储 分布式计算 Hadoop
HadoopCPU、内存、存储限制
【7月更文挑战第13天】
279 14
|
3月前
|
存储 编译器 C语言
【C语言篇】数据在内存中的存储(超详细)
浮点数就采⽤下⾯的规则表⽰,即指数E的真实值加上127(或1023),再将有效数字M去掉整数部分的1。
366 0