C 进阶 — 数据在内存中的存储
主要内容
1、数据类型详细介绍
2、整形在内存中的存储:原码、反码、补码
3、大小端字节序介绍及判断
4、浮点型在内存中的存储解析
一 数据类型介绍
基本内置类型
char //字符数据类型 1
short //短整型 2
int //整形 4
long //长整型 4
long long //更长的整形 8
float //单精度浮点数 4
double //双精度浮点数 8// 规定
sizeof(char) == 1
sizeof(char) <= sizeof(short)
sizeof(short) <= sizeof(int)
sizeof(int) <= sizeof(long)
sizeof(long) <= sizeof(long long)
C 语言没有字符串类型,字符串类型是用字符数组实现的
类型:决定了开辟内存空间的大小和如何描述该块内存空间
1.1 类型的基本归类
整形
charunsigned charsigned charshortunsigned shortsigned short intunsigned intsigned intlongunsigned long signed long
浮点型
float double
构造类型
> 数组类型
> 结构体类型 struct
> 枚举类型 enum
> 联合类型 union
指针类型
int *pi;
char *pc;
float* pf;
void* pv;
空类型
void 表示空类型(无类型)
通常应用于函数的返回类型、函数参数、指针类型
二 整型在内存中的存储
2.1 原码、反码、补码
计算机中的整数有三种二进制表示方法,即原码、反码和补码
三种表示方法均有 符号位 和 数值位 两部分,符号位都是用 0 表示 正 ,用 1 表示 负。数值位正数的原、反、补码都相同,而负整数的三种表示方法各不相同
- 原码 将数值按照正负数的形式翻译成二进制就可以得到原码
- 反码 原码的符号位不变,其他位依次按位取反就可以得到反码
- 补码 反码 +1 得到补码
2.2 原反补的意义
对于整形而言:数据存放内存中的是补码
因为可以将符号位和数值域统一处理,同时加减法也统一处理( CPU 只有加法器 )此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路
以下为原因探讨,参考
1、原码(Sign-Magnitude Representation)
原码是最简单的表示法,它用一位二进制数表示符号,0 表示正数,1 表示负数,其余位数表示数字的绝对值。例如,+5和 -5 的二进制原码分别是:
+5 的原码:00000101
-5 的原码:10000101
问题:原码虽然直观,但在进行二进制运算时,正负数的处理很麻烦,尤其是减法时,还需要额外判断符号位,增加了硬件电路的复杂性
2、反码(Ones’ Complement)
反码通过将负数的每一位进行取反( 1 变 0 ,0 变 1 )来表示负数。例如,+5 和 -5 的反码表示如下:
+5的反码:00000101(与原码相同)
-5的反码:11111010
优点:反码改进了原码的加减法处理,因为减法可以转化为加法,只需要加上一个负数的反码即可
问题:反码有一个严重的问题,即零的表示不唯一,存在 +0(00000000)和 -0(11111111)两种表示形式,这导致逻辑处理变得复杂
3、补码(Two’s Complement)
补码是最常用的表示法,它通过在反码的基础上再加 1 来表示负数。比如,+5 和 -5 的补码如下:
+5 的补码:00000101
-5 的补码:11111011(反码是 11111010,再加 1)
优点:① 唯一的零表示:补码只用一个零(00000000),解决了反码的双零问题 ② 加减法统一:补码的设计让加法和减法能够统一处理,所有数值都可以通过加法来计算,简化了硬件设计 ③ 符号处理自动化:补码的符号位(最高位)在计算过程中自动处理,无需额外逻辑
2.3 大小端
示例数据在内存中的存储
#include<stdio.h>/*
短除法20/2 = 10 010/2 = 5 05/2 = 2 12/2 = 1 01/2 = 0 1 10100
*/
int main()
{ int a = 20; // 00000000 00000000 0000000 00010100// 14 00 00 00 十六进制 —> 二进制 (将每个十六进制数转换为 4 个二进制数字)// 00010100 00000000 00000000int b = -10; //10000000 00000000 00001010//取反 = 11111111 11111111 11110101//+1 = 11111111 11111111 11110110//转十六进制 = ff ff ff f6return 0;
}
可以看到对于 a 和 b 分别存储的是补码。但是发现顺序不一致
大小端
大端(存储)模式,是指数据低位保存在内存高地址中,而数据高位,保存在内存低地址
小端(存储)模式,是指数据低位保存在内存低地址中,而数据高位,保存在内存高地址
例如一个 16bit 的 short 型 x ,在内存中的地址为 0x0010 , x 的值为 0x1122 ,那么 0x11 为高字节, 0x22 为低字节。对于大端模式,就将 0x11 放在低地址中,即 0x0010 中, 0x22 放在高地址中,即 0x0011 中。小端模式,刚好相反
为什么有大小端之分 因为在计算机中,数据是以字节为单位的,一个字节为 8 bit。对于 32 位处理器,由于寄存器宽度大于一个字节,必然存在如何将多个字节安排的问题
2.4 练习
1、设计一个小程序判断当前机器的字节序
#include<stdio.h>
//方式一, 利用取地址 + 指针解引用
int check_sys()
{int i = 1;return *(char*) & i;
}// 方式二, 利用联合体特性
int check_sys()
{union {int i;char c;} un;un.i = 1;return un.c;
}int main()
{if (check_sys())printf("大端");elseprintf("小端");return 0;
}
2、下面程序分别都输出什么
/*① char 类型变量中存储的值的本质就是数字(存入字符时,字符会转化为 ASCLL 表中对应的数字再存入),因此char 类型的变量是可以直接存储数字的,只不过由于字节数的限制,存储范围较小② -1 的补码是 11111111111111111111111111111111,存入 char、signed char、unsigned char 类型变量中时,会发生截断,只将补码的八个低位端的 1 放入,即 11111111③ 打印时,由于打印格式是 %d,那么会发生整形提升。提升规则如下:有符号位则左端补充符号位,无符号位则左端补充 0。于是 a 补充为 11111111111111111111111111111111(char 一般默认为有符号型),b 补充为11111111111111111111111111111111,c 补充为 00000000000000000000000011111111。转化成原码输出,它们的结果分别是-1、-1、255
*/
#include <stdio.h>
int main()
{char a = -1;//-1 的补码 11111111111111111111111111111111 存入 char 发生截断, 即 11111111signed char b = -1;unsigned char c = -1;printf("a = %d, b = %d, c = %d",a,b,c); //打印时 %d 整形提升, 有符号位则左端补充符号位, 无符号位则左端补充0, 11111111111111111111111111111111 -> 转原码 -1return 0;
}
/*-128 = 10000000 00000000 00000000 10000000 原码= 11111111 11111111 11111111 01111111 反码= 11111111 11111111 11111111 10000000 补码存入 char 发生截断a = 10000000 (二进制)%u 按无符号整型输出, 发生提升 10000000 -> 00000000 00000000 00000000 10000000 补码-> 11111111 11111111 11111111 10000000 原码(补码取反 + 1, 这里要注意 %u 没有符号位, 所以原码不需要考虑符号位)
*/#include <stdio.h>
int main()
{char a = -128; //128 = 2^7 = 1000 0000printf("%u\n",a); //4294967168return 0;
}
/*128 = 00000000 00000000 00000000 10000000 原(补)码存入 char 发生截断a = 10000000 (二进制)%u 按无符号整型输出, 发生提升 10000000 -> 00000000 00000000 00000000 10000000 补码-> 11111111 11111111 11111111 10000000 原码(补码取反 + 1)
*/#include <stdio.h>
int main()
{char a = 128;printf("%u\n",a); //4294967168return 0;
}
/*-20 = 10000000 00000000 00000000 00010100 (原码)= 11111111 11111111 11111111 11101011 (反码)= 11111111 11111111 11111111 11101100 (补码)10 = 00000000 00000000 00000000 00001010 (原补码)+ = 11111111 11111111 11111111 11110110 (补码)-> = 10000000 00000000 00000000 00001010 (原码)这里要注意 %d 有符号位, 所以 11111111 11111111 11111111 11110110 也要被当成有符号数, 符号位为 1
*/
int i = -20;
// i + j, unsigned + signed, 就要转为 unsigned
unsigned int j = 10;
printf("%d\n", i + j); //-10 //最后一步需要注意 %d
//按照补码的形式进行运算,最后格式化成为有符号整数
/*9 = 00000000 00000000 00000000 00001001当 i == 0 时, i-- 则 i 变为 11111111 11111111 11111111 11111111 = 2^32 - 1死循环输出
*/
unsigned int i;
for(i = 9; i >= 0; i--)
{printf("%u\n",i);
}
/*数值 -1 - i = -1, -2, -3, -4-> 对应的二进制10000000 00000000 00000000 00010100char 1 个字节 8 bit-1 - i = 10000000 00000000 00000001 00000000 时存入 a[i] 中因发生截断, 存入数值 0 2^8 = 256, 此时 i = 256strlen 不包含 '\0', 因此 strlen 结果为 255阿拉伯数字 0 的ASCII码为 48,‘\0’ 转义字符的 ASCII码值为 0,它表示的是 ASCII 控制字符中空字符
*/int main()
{char a[1000];int i; //整型for(i=0; i<1000; i++){a[i] = -1-i;}printf("%d",strlen(a)); //255return 0;
}
/*i 是 unsigned char, 对应范围 [0, 255]当 i == 255 时, i++ 则 i = 0死循环打印
*/#include <stdio.h>
unsigned char i = 0;
int main()
{for(i = 0;i<=255;i++){printf("hello world\n");}return 0;
}
三 浮点数在内存中的存储
参考链接 浮点型在内存中的存储 ,浮点型在内存中的存储
浮点数在计算机内部的表示方式,根据国际标准IEEE754,任意一个二进制浮点数可表示成下面的形式:
(-1)^ S * M * 2 ^ E
其中(-1)^ S
表示符号位,当 S = 0
为正,当 S = 1
为负,M 表示有效数字,1 <= M < 2
, 2 ^ E
表示指数位
//如下所示符号位 有效数字 指数
(-1) ^ S * M * 2 ^ E
举例,十进制的 7.0,二进制表示为 111.0,相当于1.11 * 2 ^ 2。那么,按照上面的格式,7 大于 0,S = 0;有效数字为 1.11,M=1.11;指数为 2,E = 2
再举个例子,对于十进制数 5.5,把它化成二进制数,为 101.1。5.5大于0,S=0;有效数字为 1.011,M = 1.011;指数为 2 E = 2
IEEE 754 规定
对于 32 位浮点数,最高的 1 位是符号位 S,接着的 8 位是指数 E,剩下的 23 位为有效数字 M
对于 64 位的浮点数,最高的 1 位是符号位 S,接着的 11位是指数 E,剩下的 52 位为有效数字 M
对于有效数字 M 和指数 E,还有一些特别规定
对于 M
由于 M 是范围在 1 和 2 之间,计算机在保存 M 时,默认这个数的第一位总是 1,因此可以舍去,只保存后面的小数部分。而在读取M的时候,会自动加上这个 1,这样的话可以节省一位有效数字,扩大保存数字的范围
例 M 为 1.01 时,假如没有舍去 1,可以保存23位有效数字;将 1 舍去,只保存后面的 01,等于可以保存 24 位有效数字
对于 E
由于 E 为一个无符号整数(unsigned int)没有负数,但浮点型数据的指数是可以为负的。因此存储 E 的真实值必须加上一个中间数 127(32 位平台下),这样若本身 E 为正,使用时减去 127 即可;若 E 本身为负,加上 127 为正数可以存储,使用时减去 127 即可
当 E 不全为 0 或不全为 1
指数 E 计算值减去 127(或 1023)得到真实值, 再将有效数字 M 前加上第一位的 1
当 E 全为 0
浮点数的指数 E 等于 1 - 127(或 1 - 1023)即为真实值,有效数字 M 不再加上第一位的 1,而是还原为 0.xxxxxx
的小数。这样做是为了表示 ±0,以及接近于 0 的很小的数字
注 : 有效数字 M 不再加上第一位的 1,因为当 E 全为 0 时,该小数的范围在 (-1,1)之间
当 E 全为 1
如果有效数字 M 全为 0,表示 ± 无穷大(正负取决于符号位 S)
例存储十进制的 5.5
补充知识:一个十进制的浮点数据怎么表示成二进制形式
核心要点:整数部分除 2 取余,小数部分乘 2 取整
举一个简单的例子:
178.125整数部分除 2 取余,直至得到 0
178 除 2 得 89,余 0
89 除 2 得 44,余 1
44 除 2 得 22,余 0
22 除 2 得 11,余 0
11 除 2 得 5, 余 1
5 除 2 得 2, 余 1
2 除 2 得 1, 余 0
1 除 2 得 0, 余 1
得到的余数从下往上排列得 10110010,即为 178 的二进制表示小数部分乘 2 取整
0.125 乘 2 得 0.25,整 0
0.25 乘 2 得 0.5,整 0
0.5 乘 2 得 1.0,整 1
得到的整数从上往下排列得 001,即 0.001 为 0.125 的二进制表示
合并上面的两点 178.125 的二进制表达方式为:10110010.011
练习题
/*
9 = 00000000 00000000 00000000 00001001
-> 转成 float 形式
-> 0(S) 00000000(E) 00000000000000000001001(M)
-> (-1)^S * M (0.00000000000000000001001) * 2^E(-126)
-> %f 输出小数点后六位 0.000000把对应的 9 的内存空间转为 9.0 表示
-> 1001.0 = 1.0010 * 2^3
-> 转成 float 形式
-> (-1)^S(0) * M (00100000000000000000000) * 2^E(130)
-> 0 10000010 00100000000000000000000
-> 二进制转十进制 1091567616
*/
int main()
{int n = 9;float* pFloat = (float*)&n;printf("n的值为 %d\n",n); //9printf("*pFloat的值为 %f\n", *pFloat); //0.000000*pFloat = 9.0;printf("n的值为 %d\n", n); //1091567616printf("*pFloat的值为 %f\n", *pFloat); //9.0
}