深度剖析数据在内存中的存储
本章重点
- 数据类型详细介绍
- 整形在内存中的存储:原码、反码、补码
- 大小端字节序介绍及判断
- 浮点型在内存中的存储解析
正文开始
1. 数据类型介绍
前面我们已经学习了基本的内置类型(C语言本身自带的类型):
char //字符数据类型
short //短整型
int //整形
long //长整型
long long //更长的整形
float //单精度浮点数
double //双精度浮点数
//C语言有没有字符串类型?
以及他们所占存储空间的大小。
1 2 4 4/8 8
4 8
类型的意义:
- 使用这个类型开辟内存空间的大小(大小决定了使用范围)。
- 如何看待内存空间的视角
由于数据大小不同,数据类型的范围不同,有了丰富的类型,让我们选择合适的数据类型时更加方便,在一定程度上可以避免内存的浪费。
其次,数据类型的不同,决定了我们看待数据的角度。
当是int short我们认为这是一个整数,当看到double float我们认为这是一个小数。
1.1 类型的基本归类
整形家族:
char
unsigned char
signed char
short
unsigned short [int]
signed short [int]
int
unsigned int
signed int
long
unsigned long [int]
signed long [int]
unsigned 无符号的
int是整形,short是短整型,而short int 是全称,int可以省略。
每一个字符都有对应的ASCII码值,所以char归类到整形。
unsigned 和signed
生活中数值有正数有负数。
然后有些数值,只有正数,没有负数(身高)
有些数值,有正数也有负数(温度)
在C语言中为了把数值描述更加准确,因此把只有正数没有负数的类型,叫做无符号数字。
有些数值,有正数也有负数,为了区分正负,叫做有符号数字。
在C语言中并没有明确规定char类型是有符号还是无符号,所以更准确的可以把char类型分为char,signed char,unsigned char。其中char是有符号还是无符号是不能明确的,有些编译器认为char是有符号的,有些编译器认为char是无符号的。
然而short int long 是有规定给的,如果只写short int long,编译器默认为是有符号的。
short 等价于signed short
int 等价于signed int
long 等价于signed long
浮点数家族:
float
double
浮点数家族我们暂时不讲。
构造类型:
构造类型也叫做自定义类型
> 数组类型
> 结构体类型 struct
> 枚举类型 enum
> 联合类型 union
为什么数组类型也叫自定义类型?
char arr[6] //数组元素类型和数组元素个数由自己决定
char arr[5]
int arr[5]
数组元素类型和数组元素个数由自己决定。所以数组类型我们归为构造类型(自定义类型)。
结构体类型我们比较了解。
而枚举类型和联合体类型不太了解。
这三块内容我们放到第四节,自定义类型详解中进行讲解。
指针类型
int *pi;
char *pc;
float* pf;
void* pv;
空类型:
void 表示空类型(无类型)
通常应用于函数的返回类型、函数的参数、指针类型。
返回类型
void test()
{
//不需要返回值
}
int main()
{
return 0;
}
函数的参数
void test(void)
{
//不需要函数的参数
}
int main()
{
return 0;
}
指针类型
int main()
{
void* p = NULL;
int a = 10;
void* p1 = &a; //原本是用int*类型进行存储
return 0;
}
void* 就像一个垃圾桶,不管什么类型的指针类型,甚至结构体类型的指针都能往里面放。
虽然什么类型的指针都能往void*里面进行存储。但是却不能进行操作。
void* = &p;
p++; //error
*p; //error
void*一般用于临时存放地址,可以通过强制类型转换进行使用,至于它的应用,我们以后再说。
2. 整形在内存中的存储
我们之前讲过一个变量的创建是要在内存中开辟空间的。空间的大小是根据不同的类型而决定的。
那接下来我们谈谈数据在所开辟内存中到底是如何存储的?
比如:
int a = 20;
int b = -10;
我们知道为 a 分配四个字节的空间。
那如何存储?
下来了解下面的概念:
2.1 原码、反码、补码
计算机中的整数有三种2进制表示方法,即原码、反码和补码。
三种表示方法均有符号位和数值位两部分,最高位为符号位,符号位都是用0表示“正”,用1表示“负”,而数值位
正数的原、反、补码都相同。
负整数的三种表示方法各不相同。
原码
直接将数值按照正负数的形式翻译成二进制就可以得到原码。
反码
将原码的符号位不变,其他位依次按位取反就可以得到反码。
补码
反码+1就得到补码。
举个例子
int main()
{ int a = 20;
//00000000 00000000 00000000 00010100 原码
//00000000 00000000 00000000 00010100 反码
//00000000 00000000 00000000 00010100 补码
int b = -10;
//10000000 00000000 00000000 00001010 原码
//11111111 11111111 11111111 11110101 反码
//11111111 11111111 11111111 11110110 补码
return 0;
}
对于有符号的数字来说,最高位是符号位。
对于整形来说:数据存放内存中其实存放的是补码。
为了验证数据在内存中是以补码的形式存在。
打开调试-窗口-内存
查看b的数据
f6 ff ff ff 十六进制
经过计算正好就是对应的二进制补码,不过是倒着进行存储。
但是为什么以补码的形式存放?
在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统一处理;
同时,加法和减法也可以统一处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。
cpu只有加法器,如何计算减法?
int main()
{
int c = 1-1;
//1 + (-1)在计算减法的时候,把减号和后面的数字看成一个整体进行相加。
//补码相加
// 00000000 00000000 00000000 00000001
// 11111111 11111111 11111111 11111111
//相加为
//100000000 00000000 00000000 00000000
由于是int类型,最高位无法存储,所以保留的就是0。
return 0;
}
原码与补码的转换
原码到补码,有一种方式,原码取反、加一得到补码
补码到原码有两种方式
1 补码减一、取反得到原码
2 补码取反、加一得到原码
方式二举例
-1的
补码11111111 11111111 11111111 11111111
10000000 00000000 00000000 00000000
10000000 00000000 00000000 00000001
这也就是为什么上面说补码与原码相互转换,其运算过程是相同的。
int main()
{
int a = 0x12345678;
return 0;
}
//在内存窗口显示为
//78 56 34 12
一个十六进制位是四个二进制位,两个十六进制位是八个二进制位。一个字节,八个比特位。所以两个十六进制位是一个字节。所以八个十六进制位刚好四个字节。
所以是以字节为单位进行存储。
至于刚刚提到的是什么,下面我们介绍*大小端介绍**
2.2 大小端介绍
2.2.1什么大端小端:
大端(存储)模式,是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中;
小端(存储)模式,是指数据的低位保存在内存的低地址中,而数据的高位,,保存在内存的高地
址中。
2.2.2 为什么有大端和小端:
个人理解
内存中的存储单元是一个字节,所以我们以字节讨论存储顺序问题。
当一个数值超过一个字节了,要存储到内存中,就有顺序的问题。
0x11223344
低地址---------->高地址
11 22 33 44/22 11 33 44 /44 33 22 11
理论上随意存储,只要最后能有办法还原成0x11223344就可以。
但是如果随意存储的话,存储的顺序很难记忆,并且不符合常理。
所以为了简单,方便。
数据的存储留下两种方法要么
11 22 33 44 要么44 33 22 11
这就是所谓的大小端。
对于一个十进制数字123,3是个位,1是百位,也就是3在低位,1在高位。
对于0x11223344 44是低字节的数据,11是高字节的数据。
小端字节序存储就相当于倒着进行存储。
大端字节序就相当于正着进行存储。
vs编译器环境下就是小端字节序存储。
我们常见的x86是小端模式。
补充:
我们是以字节为单元讨论数据存储顺序。
数值有不同的表现形式,10进制,2进制,8进制。
其中16进制是1-9- a-f组成
其中a-f代表10-15
其中f(15)的二进制是1111
所以对一个二进制数字例如
1111 0000 1111 1011
每四位转换为一个十六进制数字。
所以一个十六进制位代表四个二进制位。两个十六进制是八个二进制位,也就是一个字节。
补充:
1内存中存放的是补码
2整形表达式计算式用的内存中补码计算的。
3打印和我们看到的都是原码
百度2015年系统工程师笔试题:
请简述大端字节序和小端字节序的概念,设计一个小程序来判断当前机器的字节序。(10分)
小端字节序存储:
把一个数值的低位字节的内容存在低地址处,高位字节的内容,存放在高地址处。
大端字节序存储:
把一个数值的低位字节的内容存在高地址处,高位字节的内容,存放在低地址处。
程序:
对于int a= 1;
十六进制为0x 00 00 00 01
如果是小端存储为 01 00 00 00
如果是大端存储为 00 00 00 01
判断首元素地址指向一个字节的元素是0或者1就好了
int main()
{
int a = 1;
char* p = (char*)&a;
if(*p==1)
{
printf("小端\n");
}
else
printf("大端\n");
return 0;
}
如果将代码封装成函数
int check_sys()
{
int a = 1;
char* p = (char*)&a;
if(*p==1)
{
return 1;
}
else
{
return 0;
}
}
int main()
{
if(check_sys()==1)
printf("小端\n");
else
printf("大端\n");
return 0;
}
对代码进行简化
int check_sys()
{
int a = 1;
if(*(char*)&a==1)
{
return 1;
}
else
{
return 0;
}
}
int main()
{
if(check_sys()==1)
printf("小端\n");
else
printf("大端\n");
return 0;
}
再次优化
int check_sys()
{
int a = 1;
return *(char*)&a; //返回1表示小端,返回0表示大端
}
int main()
{
if(check_sys()==1)
printf("小端\n");
else
printf("大端\n");
return 0;
}
2.3 练习
代码一
//输出什么?
#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;
}
//-------------------
//编译器运行结果为
//a=-1,b=-1,c=225
先看char类型的-1存储
先写出来-1的原码反码补码
10000000 00000000 00000000 00000001 -1的原码
11111111 11111111 11111111 11111110 -1的反码
11111111 11111111 11111111 11111111 -1的补码
但是由于存储到char类型中,只有8个比特位,所以会发生截断。只把最右边八个1进行存储
11111111
再看signed char 类型-1的存储。
11111111 11111111 11111111 11111111 -1的补码
但是由于存储到char类型中,只有8个比特位,所以会发生截断。只把最右边八个1进行存储
11111111
再看unsigned char 类型-1的存储。
11111111 11111111 11111111 11111111 -1的补码
但是由于存储到char类型中,只有8个比特位,所以会发生截断。只把最右边八个1进行存储
11111111
虽然存储的一样,但是char 和signed char 把最高位当成符号位,unsigned char的八个比特位都是数值位。
打印的时候是%d ,对应的是整形,而我们是char类型,需要发生整形提升。
char a存储的是11111111进行整形提升。
整形提升按照符号位补充。
整形提升完为
11111111 11111111 11111111 11111111这是补码
11111111 11111111 11111111 11111110这是反码
10000000 00000000 00000000 00000001这是原码
所以打印出来是-1
因为char 在vs中等价于signed char
所以signed char 打印出来也是-1
对于unsigned中存储的11111111
先进行整形提升,由于是无符号的,所以直接补充0
00000000 00000000 00000000 11111111 这是补码
00000000 00000000 00000000 11111111 这是原码
所以打印出来是225
补充
16进制打印 %x
8进制打印 %o
有符号数和无符号数的取值范围如何定?
有符号位
一个字节,八个比特位,所能存储的所有数字
00000000 0
00000001 1
00000010 2
00000011 3
00000100 4
···
01111111 127
10000000 -128
10000001 -127
···
11111110 -2
11111111 -1
2^8是256
其中对于10000000直接规定为-128
因为-123的二进制数字为
原码110000000
反码101111111
补码110000000
存储到8个比特位中,就是10000000
所以char 和signed char的取值为-128~127
无符号
8个比特位都是数值位
11111111就是255
所以对于一个unsigned char来说,范围是从0~255
由此类推
signed short
0000000000000000 0
···
0111111111111111 32767
1000000000000000 -32768
···
1111111111111111 -1
所以signed short 的取值范围是-32768~32767
unsigned short 的取值范围是 0~65535
代码二
2.
#include <stdio.h>
int main()
{
char a = -128;
printf("%u\n",a);
return 0;
}
//-----------------
//编译器运行结果为
//4294967168
%u是打印无符号整形,认为内存中存放的补码对应的是一个无符号数。
%d 是打印有符号整形,认为内存中存放的补码对应的是一个有符号的数。
-128对应的32位比特位
10000000 00000000 00000000 10000000原码
11111111 11111111 11111111 01111111反码
11111111 11111111 11111111 10000000补码
存放在char a 中的数据为
10000000
将%u打印则进行整形提升,至于整形提升补0还是1看数据存放的数据类型,而不是要打印的格式
整形提升
因为有符号,就补充最高位
11111111 11111111 11111111 10000000
所以这个就是整形提升后的补码
再回归原码
因为打印的为无符号的%u,所以原码就是补码
11111111 11111111 11111111 10000000打印的十进制数字为4294967168
这个就是我们今天讲的关于整形存储还有大小端的问题,不过我们的浮点数存储还没有讲解,留到下次再分享。
转载:https://blog.csdn.net/m0_71545436/article/details/128098322