【C\C++】基础:数据在内存中的存储
摘要:本篇文章将详细介绍数据在内存中的存储。首先将整体介绍数据类型,然后从两方面进行,分别为整型数据在内存中存储,以及浮点型数据在内存中的存储。
文章目录
一. 数据类型概述
数据类型的差异主要在于:每个数据类型的含义不同,以及开辟大小不同,还有从内存中看待数据的视角不同
1.1 基本内置类型的含义以及最小尺寸
类型 | 含义 | 最小尺寸 |
---|---|---|
bool | 布尔类型 | 未定义 |
char | 字符 | 8位 |
short | 短整型 | 16位 |
int | 整型 | 16位 |
long | 长整型 | 32位 |
long long | 长整型 | 64位 |
float | 单精度浮点数 | 6位有效数字 |
double | 双精度浮点数 | 10位有效数字 |
long double | 扩展精度浮点数 | 10位有效数字 |
在这些基本内置类型中,需要时刻牢记它们的含义以及大小,在类型转换、大小端字节序等后续部分理解中将会起到很重要的作用。
1.2 整型类型
类型 | 含义 | 最小尺寸 |
---|---|---|
unsigned char | 无符号字符数据类型 | 8位 |
signed char | 有符号字符数据类型 | 8位 |
unsigned short | 无符号短整型 | 16位 |
signed short | 有符号短整型 | 16位 |
unsigned int | 无符号整型 | 32位 |
signed int | 有符号整型 | 32位 |
unsigned long | 无符号长整型 | 64位 |
signed long | 有符号长整型 | 64位 |
整型类型细分后,可以发现对是否有无符号进行了划分,在内存中的区别就是在将内存的首位是否看做为符号位,这对整型提升以及原码转换等内容会起到至关重要的影响。
1.3 浮点类型
类型 | 含义 | 最小尺寸 |
---|---|---|
float | 单精度浮点数 | 6位有效数字 |
double | 双精度浮点数 | 10位有效数字 |
浮点类型的区别在内存的体现是,内存从什么视角规则去分析浮点类型的数据,得出所表示的值得大小与精度
1.4 构造类型
数组 int arr[10]
结构体类型 struct
枚举类型 enum
联合类型 union
1.5 指针类型
int *pi;
double *pd;
char *pc;
void *pv;
指针的大小在同一位数机器上是固定的,都表示的是地址。而不同数据类型的指针,区别在于运算时步长有所差异,以及在解引用内存时,对解引用的大小也存在差异。
1.6 空类型
void 表示空类型,通常用于函数返回、函数参数、指针类型。通常有人会戏称其为垃圾桶,原因是它什么都能放,一般用于临时存放,在此举一个例子。
例:在 void qsort(void *base, size_t nitems, size_t size, int (*compar)(const void *, const void*)) 函数中的第四个函数参数就是返回值为int,参数为void*的函数指针,其中void*可以放各种类型的数据,只需要按照相应类型进行转换即可,参考如下:
int cmp_int(const void* e1,const void* e2) {
return *((int*)e1) - (*(int*)e2);
}
int cmp_person_byAge(const void* e1, const void* e2) {
return ((person*)e1)->age - ((person*)e2)->age;
}
int cmp_person_byName(const void* e1, const void* e2) {
return strcmp(((person*)e1)->name, ((person*)e2)->name);
}
二. 整型在内存中的存储
整型数据在内存中的储存,首先要按照数据类型开辟空间,然后再引入原码反码补码来解析在内存中存储的数据,最后需要通过大端小端来了解在内存中的存取顺序。
2.1 空间开辟
在开辟空间的部分我们只需要,按照数据类型的大小完成开辟任务,需要注意的是,如果当占用空间较大的数据类型转换为占用空间较小的数据类型时,需要进行截断,当占用空间空间较小的数据类型转换为占用空间较大的数据类型时,需要进行整形提升。还要注意在运算过程中,需要将占用空间较短的数据类型整型提升为适合编译器运算的相应位数,方便运算器进行运算,最后可能还需要进行截断来再次存储到内存中。
2.2 原码、反码、补码
计算机中的整数有三种2进制表示方法,即原码、反码和补码。三种表示方法均有符号位和数值位两部分,符号位都是用0表示“正”,用1表示“负”,而数值位正数的原、反、补码都相同,负整数的三种表示方法各不相同其中原码直接将数值按照正负数的形式翻译成二进制就可以得到原码,反码将原码的符号位不变,其他位依次按位取反就可以得到反码,补码将反码+1就得到补码。需要注意的是,有种说法为正数不存在反码和补码。
原码与补码具有不同的实际意义,原码即为整型数据翻译成相应的二进制数据,而补码表示的是数据内存中存放的二进制序列,可以简单的理解为原码就是能看到的,补码就是储存在内存中的数据。而为什么在内存需要用补码来储存数据呢?原因是,使用补码可以将符号位和数值域统一处理,加法和减法也可以统一处理(CPU只有加法器),以及补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。
注意:有无符号与原码反码补码的转换无关,只与整形提升的补位方式有关
现在进行几个原码反码补码转换的练习
int a = 20;
//开辟空间 ====> 类型为int,占用 4 byte,即 32 位空间
//二进制表示如下:
原码(表示出来的):0000 0000 0000 0000 0000 0000 0001 0100
//转换为反码补码(正数的补码与反码都是本身)
反码、补码(储存在内存中):0000 0000 0000 0000 0000 0000 0001 0100
int b = -10;
//开辟空间 ====> 类型为int,占用 4 byte,即 32 位空间
原码(表示出来的,符号位为负):1000 0000 0000 0000 0000 0000 0000 1010
反码(符号位不变,其他位取反):1111 1111 1111 1111 1111 1111 1111 0101
补码(反码+1,存在内存中) :1111 1111 1111 1111 1111 1111 1111 0110
综合练习题:
//练习:输出什么?
#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 a = -1;
//原码:1000 0000 0000 0000 0000 0000 0000 0001
//反码:1111 1111 1111 1111 1111 1111 1111 1110
//补码:1111 1111 1111 1111 1111 1111 1111 1111
//存入内存需要发生截断
//内存:1111 1111
//signed char b = -1;
//原码:1000 0000 0000 0000 0000 0000 0000 0001
//反码:1111 1111 1111 1111 1111 1111 1111 1110
//补码:1111 1111 1111 1111 1111 1111 1111 1111
//存入内存需要发生截断
//内存:1111 1111
//unsigned char c = -1;
//原码:1000 0000 0000 0000 0000 0000 0000 0001
//反码:1111 1111 1111 1111 1111 1111 1111 1110
//补码:1111 1111 1111 1111 1111 1111 1111 1111
//存入内存需要发生截断
//内存:1111 1111
//printf("a=%d,b=%d,c=%d", a, b, c);
//通过%d输出(有符号整数输出),需要进行整型提升
//a b:1111 1111
//补码:1111 1111 1111 1111 1111 1111 1111 1111
//反码:1000 0000 0000 0000 0000 0000 0000 0000
//原码:1000 0000 0000 0000 0000 0000 0000 0001
//c:1111 1111(全是正数)
//补码 反码 原码:0000 0000 0000 0000 0000 0000 1111 1111
//最后按照原码输出即可
总结:注意:有无符号与原码反码补码的转换无关,只与整形提升的补位方式有关
2.3 大端小端介绍
产生大端小端的原因:但数据存放到内存中是,当超过1个字节,就会有多个字节需要排列i到内存中,为此就需要规定不同字节的存储顺序,分别为将数据高位存储到高地址部分,以及数据低位存储到高地址部分,对应的就是大端小端的两种存储方式,示意图如下:
如此引入大端小端的定义:
- 大端(存储)模式,是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中;
- 小端(存储)模式,是指数据的低位保存在内存的低地址中,而数据的高位,,保存在内存的高地址中。
例题:请简述大端字节序和小端字节序的概念,设计一个小程序来判断当前机器的字节序。
#include <stdio.h>
//小端为例:通过截断来返回低地址数据,从而判断大小端存储
int check_sys(){
int i = 1;
//int i = 1;
//原码:0000 0000 0000 0000 0000 0000 0000 0001
//补码:0000 0000 0000 0000 0000 0000 0000 0001
//内存:0000 0000 0000 0000 0000 0000 0000 0001
// 高位 <========================= 低位
// 高地址 <========================= 低地址
return (*(char*)&i);
//*(char*)&i:取i的地址,强制类型转换为char*指针,再对其进行解引用操作。
// 转换类型为char后,解引用的内存大小变化,需要截断,
// 截断需要从低地址开始截断,返回的是1字节数据,即0000 0001
}
int main(){
int ret = check_sys();
if (ret == 1){
printf("小端\n");
}
else{
printf("大端\n");
}
return 0;
}
#include <stdio.h>
//大端为例:通过截断来返回低地址数据,从而判断大小端存储
int check_sys(){
int i = 1;
//int i = 1;
//原码:0000 0000 0000 0000 0000 0000 0000 0001
//补码:0000 0000 0000 0000 0000 0000 0000 0001
//内存:0000 0000 0000 0000 0000 0000 0000 0001
// 高位 <========================= 低位
// 低地址 <========================= 高地址
return (*(char*)&i);
//*(char*)&i:取i的地址,强制类型转换为char*指针,再对其进行解引用操作。
// 转换类型为char后,解引用的内存大小变化,需要截断,
// 截断需要从低地址开始截断,返回的是1字节数据,即0000 0000
}
int main(){
int ret = check_sys();
if (ret == 1){
printf("小端\n");
}
else{
printf("大端\n");
}
return 0;
}
2.4 练习
//练习:输出什么?
#include <stdio.h>
int main()
{
char a = -128;
//char a = -128;
//整型提升:1000 0000 0000 0000 0000 0000 1000 0000
//原码:1000 0000 0000 0000 0000 0000 1000 0000
//反码:1111 1111 1111 1111 1111 1111 0111 1111
//补码:1111 1111 1111 1111 1111 1111 1000 0000
//内存:1000 0000
//整型提升:1111 1111 1111 1111 1111 1111 1000 0000
//%u 无符号整型输出只有正数
//原码反码补码一致
//原码:1111 1111 1111 1111 1111 1111 1000 0000
//输出:4,294,967,168
printf("%u\n", a);
return 0;
}
//练习:输出什么?
#include <stdio.h>
int main()
{
int i = -20;
unsigned int j = 10;
//int i = -20;
//原码:1000 0000 0000 0000 0000 0000 0001 0100
//反码:1111 1111 1111 1111 1111 1111 1110 1011
//补码:1111 1111 1111 1111 1111 1111 1110 1100
//unsigned int j = 10;
//原码 补码 反码:0000 0000 0000 0000 0000 0000 0000 1010
//i: 1111 1111 1111 1111 1111 1111 1110 1100
//j:1111 1111 1111 1111 1111 1111 1111 0110
//i+j:1111 1111 1111 1111 1111 1111 1111 0101
//转换为原码:1000 0000 0000 0000 0000 0000 0000 1010
printf("%d\n", i + j);
//按照补码的形式进行运算,最后格式化成为有符号整数
//输出:-10
}
三. 浮点型在内存中的存储
浮点型内存空间的存储在开辟空间方面与整型相似,但在读取内存数据的视角与整型读取内存数据的视角完全不同,需要着重掌握浮点型数据在内存中存储的规则。
3.1 浮点型在内存中存储规则
浮点型的存储规则是按照国际标准IEEE754规定,任意一个二进制浮点数可以表示为:(-1)^S * M * 2^E
- (-1)^S表示符号位,当S=0,V为正数;当S=1,V为负数。
- M表示有效数字,大于等于1,小于2。
- 2^E表示指数位。
对于32位的浮点数,最高的1位是符号位s,接着的8位是指数E,剩下的23位为有效数字M
对于64位的浮点数,最高的1位是符号位S,接着的11位是指数E,剩下的52位为有效数字M
其他规定细节:
M: 由于M表示有效数字,大于等于1,小于2,因此将会表示位1.XXXXXX的形式,.XXXXX表示小数部分,IEEE 754 规定,将会默认将1. 省略,只保留小数部分,即保存时省略1,只保存.XXXXX部分,在表示时将1补齐,表示为1.XXXX。这样可以节省空间,增加一位有效数字位。
E:表示指数部分,有两个需要注意的细节,分别为用无符号整数表示正负指数制定的规则,其次用其表示最大值、最小值与一般值。
首先,E是一个无符号整数,没有正负之分,可是我们知道,在指数部分有时需要小数来表示,因此IEEE 754 规定,**E在存入内存时,需要增加一个中间值,在表示时,需要将中间值减去得到实际值。对于8位的E,这个中间数是127;对于11位的E,这个中间数是1023。**比如,2^10的E是10,所以保存成32位浮点数时,必须保存成10+127=137,即10001001。
其次讨论用E表示最大值、最小值与一般值,在此将E分为全0,全1以及不全0不全1三种情况。针对E为全0来说,其实际表示为 0 - 中间值,当表示为指数时,非常接近与零,同时编译器会将有效数字M不再加上第一位的1,而是还原为0.xxxxxx的小数,因此表示为 ±0;针对E为全1来说,加上中间值,当表示为指数时为一个很大的数,同时如果有效数字M全为0,表示±无穷大(正负取决于符号位s);针对E为不全0以及不全1,采取一般规则即可。
例:
Dex:-2.75
Bin:-10.11
(-1)^(1) * 1.011 * 2^(1)
S(符号位):):1 E(需要加上中间值,此处位127):(1+127 = 128) M(省略1,并需要补齐到23位):011
最终结果:1 10000000 0110000000000000000
3.2 练习
//输出值,并说明原因
#include<string>
#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;
}
//输出值,并说明原因
#include<string>
#include <stdio.h>
int main()
{
int n = 9;
//原码:0000 0000 0000 0000 0000 0000 0000 1001
//反码:0000 0000 0000 0000 0000 0000 0000 1001
//补码:0000 0000 0000 0000 0000 0000 0000 1001
float* pFloat = (float*)&n;
printf("n的值为:%d\n", n);
//%d 有符号整型输出:将补码转换为原码后直接输出
//结果:0000 0000 0000 0000 0000 0000 0000 1001 ======> 9
printf("*pFloat的值为:%f\n", *pFloat);
//%f 单精度浮点型输出:0000 0000 0000 0000 0000 0000 0000 1001
//S:0 E:00000000 M: 000 0000 0000 0000 1001
//结果:(-1)^(0) * 1.000 0000 0000 0000 1001 * 2^(0-127)
//结果将接近于0,在保留六位小数的情况下输出:0.000000
*pFloat = 9.0;
//通过单精度浮点型的指针对存储空间进行赋值操作
//*pFloat = 9.0 需要从浮点数的视角来访问内存
//(-1)^(0) * 1001 * 2^(0)
//(-1)^(0) * 1.001 * 2^(3)
//(-1)^(0) * .001 * 2^(3+127 = 130)
//单精度浮点型在内存中存储:0 10000010 00100000000000000000000
printf("num的值为:%d\n", n);
//%d 有符号整型输出:将补码转换为原码后直接输出
//原码为:01000001000100000000000000000000 ======> 1091567616
printf("*pFloat的值为:%f\n", *pFloat);
//%f 单精度浮点型输出:9.000000
return 0;
}
四. 总结
对于各种数据类型,需要牢记其中的占用长度,精度以及各种意义等内容,便于实现转换等灵活运用;
对于整型数据在内存中的存储,进行如下总结:
- 牢记原码、反码和补码之间的相互转换;
- 原码可以理解为展示出来的数据,补码可以理解为储存在内存中的数据形式;
- 在运算过程中,需要将数据整型提升为ALU所适应的位数,而整型提升的补位,需要关注是否为有符号位;
- 在储存到内存中,需要关注是否发生截断操作,如果占用内存小于运算位数,则需要发生截断,截断是从低地址开始截断;
对于浮点型数据在内存中的存储,进行如下总结:
- 时刻记得标准形式:(-1)^S * M * 2^E ;
- S 为符号位
- M 在存入内存时,需要对 1. 进行省略,在表示0或无穷大时,M 为 0 ;
- E 需要注意三种情况的区分,在一般情况存入内存时需要加上中间值,在表示数据时,需要减去中间值;
补充:
- 代码将会放到: https://gitee.com/liu-hongtao-1/c–c–review.git ,欢迎查看!