目录
数据存储
整数在内存中的存储
存储规则
机器码
大小端字节序以及其判断
浮点数在内存中的存储
数据存储
整数在内存中的存储
存储规则
计算机内部使用二进制编码表示整数,带符号的整数称为机器数(包含符号位和数值位),实际数值是真值。
| 符号位 | 数值位 |
| 0为正,1为负 | 数的绝对值 |
机器码
整数的二进制表示方法有三种,即原码、反码、补码。
正整数的原码、反码、补码相同。
负整数:
- 原码为将数值按照正负数的形式翻译为二进制形式得到的数值码。
- 反码为原码的符号位不变,数值位按位取反。
- 补码为反码加一。
在计算机中,统一采用补码的存储及运算。
计算机硬件中只有加法器,补码可以将符号位和数值域统一处理,减法运算转换为加法运算,简化了运算器的设计,不需要额外的硬件电路。
大小端字节序以及其判断
概念
如下图所示,在vs调试当中,我们取地址a,可以发现它是按照字节为单位倒着存储的。
那下面我们一起来探讨一下数据在内存中存储顺序的问题。
在计算机中,内存的编制是由字节组成的,一个多字节的数据会占用连续多个字节。
按照不同的存储顺序,我们分为大端模式和小端模式。
| 大端存储 | 数据的低位字节内容保存在内存的⾼地址处,⽽数据的⾼位字节内容,保存 在内存的低地址处。 |
| 小端存储 | 数据的低位字节内容保存在内存的低地址处,⽽数据的⾼位字节内容,保存 在内存的⾼地址处。 |
- 大端字节序常用于:网络协议、嵌入式、单片机。
- 小端字节序常用于:x86 架构 PC、Windows、Linux(日常电脑都是小端)。
练习1
利用大小端字节序的概念,设计一个简单的程序判断当前机器的字节序。
int check_order() { int i = 1; return (*(char*)&i); } int main() { int ret = check_order(); if (ret == 1) { printf("小端\n"); } else { printf("大端\n"); } return 0; }代码解析:
- 定义 int i=1,整形为4个字节,在二进制编码当中为
00 00 00 01高字节为: 00 00 00
低字节为: 01
- 取变量 i 的低地址,将其强制转换为char类型,只读取1个字节,并解引用,取出第一个字节的值。
主函数判断:
内存低地址 → 高地址 01 00 00 00返回值为1,为小端。
高字节 → 低地址 00 00 00 01返回值为0,为大端。
练习2
int main() { char a = -1; signed char b = -1; unsigned char c = -1; printf("%d,%d,%d\n", a, b, c); return 0; }运行结果如下:
代码解析:
由于打印格式为%d需要整形提升
提升规则:
- 有符号数:按符号位扩展(高位补符号位)
- 无符号数:高位补0
- a的原码: 10000000 00000000 00000000 00000001
反码: 111111111 111111111 111111111 111111110
补码: 111111111 111111111 111111111111111111
内存补码为11111111
提升后补码为 111111111 111111111 111111111 111111111 ,真值为-1。
- 同理,b打印也为-1。
- 同理,c的内存存储为11111111
提升后补码为00000000 00000000 00000000 111111111,真值为255。
练习3
int main() { char a = -128; printf("%u\n", a); return 0; }- %u为无符号数打印形式
- a为char类型需要整型提升
-128 原码 -> 10000000 00000000 00000000 10000000 反码 -> 11111111 11111111 11111111 01111111 补码 -> 11111111 11111111 11111111 10000000 内存存储 -> 10000000 整型提升 -> 11111111 11111111 11111111 10000000结果如下图所示:
练习4
int main() { char a = 128; printf("%u\n", a); return 0; }结果如下图所示:
练习5
#include <string.h> int main() { char a[1000]; int i; for ( i = 0; i < 1000; i++) { a[i] = -1 - i; } printf("%zd", strlen(a)); return 0; }- 求数组a的字符串长度-> 在‘\0’(0)前的字符串长度
- -1-i为依次-1的循环模式,范围为-1 -> -128 -> 127-> 0
结果如下图所示:
练习6
unsigned char i = 0; int main() { for(i = 0; i <=255; i++) { printf("hello world\n"); } return 0; }- 代码结果为无限循环。
- 无符号数的取值范围为0~255。
- 如果数值大于255(二进制为11111111),增一为100000000,取八位为00000000,即返回0。
练习7
#include <Windows.h> int main() { unsigned int i; for (i = 9; i >= 0; i--) { printf("%u\n",i); Sleep(1000); } return 0; }- 代码结果为无限循环。
- 无符号整数的取值范围很大,为0-32位的1组成的数。
- 所以 i>=0 的判断恒成立,程序会无限循环。
结果如下图所示:
练习8
int main() { int a[4] = { 1,2,3,4 }; int* ptr1 = (int*)(&a + 1); int* ptr2 = (int*)((int)a + 1); printf("%x,%x\n", ptr1[-1], *ptr2); return 0; }如图所示:
地址偏移(字节): 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ... 对应元素: ───── a[0]=1 ────┼───── a[1]=2 ────┼───── a[2]=3 ────┼───── a[3]=4 ────┼ 数组外未知内存 字节归属: 0~3号字节 4~7号字节 8~11号字节 12~15号字节 16号及以后ptr1: a 表示数组首元素的地址 &a 表示整个数组的地址 &a+1 表示跳过整个数组 (int*)(&a+1)即ptr1 解引用表示一个数组的单个元素 ptr1[-1] 即为4偏移: 0 ... 15 16 ↑ ↑ &a ptr1 (&a+1 的位置) 偏移: 12 13 14 15 | 16 ↑ ↑ a[3]=4 ptr1 (ptr1 - 1)ptr2: (int)a 表示将a强制转换为普通整数 (int)a+1 纯数值 +1,只偏移 1 个字节 再强制转回 int*,得到 ptr2偏移: 0 1 2 3 ↑ ↑ a ptr2 a[0]首字节 a[0]第2字节结果如下图所示:
浮点数在内存中的存储
基础划分
| 类型 | 总位数 | 符号位 (S) | 阶码 (E) | 尾数 (M) |
|---|---|---|---|---|
| float(单精度) | 32 bit | 1 bit | 8 bit | 23 bit |
| double(双精度) | 64 bit | 1 bit | 11 bit | 52 bit |
内存布局(高位 → 低位):符号位 S + 阶码 E + 尾数 M
存储规则
浮点数在计算机内部的表示方法:
- S表示符号位,0为正数,1为负数。
- M表示有效数字,区间为1<=M<2。
- E为指数位。
存储过程
浮点数存放
有效数字M
- 约定:二进制浮点数整数部分固定为 1,所以可以被舍去,只存小数部分,写作
1.M。 - 这一步叫隐藏整数 1,可以节省 1 位空间。
指数E
- 规定:E是一个无符号整数。但在科学计数法中,E可以有负数的形式出现。
- 所以当存入内存时,指数E需要将原有的真实值加上一个中间数。
- 对于8位的E,取值范围为0-255,中间数位127。
- 对于11位的E,取值范围为0-2047,中间数位1023。
浮点数取值
规格化浮点数
- E不为全0或不为全1
非规格化浮点数
- E为全0
将隐藏的整数 1 取消,小数变为 0.M。表⽰±0,以及接近于0的很小的数字。
- E为全1