数据存储
大小端和字节内部存储
大小端
大小端是指在计算机内存中存储多字节数据类型(如整数、浮点数)时,字节的排列顺序。主要有两种类型:
-
大端序:高位字节存储在内存的低地址,低位字节存储在内存的高地址。
-
小端序:低位字节存储在内存的低地址,高位字节存储在内存的高地址。
不同编译系统采用的存储方式不同。
假设我们有一个16位的整数 0x1234,在内存中的存储方式如下:
大端序:
内存地址: 低地址 -> 高地址
存储内容: 0x12 0x34
小端序:
内存地址: 低地址 -> 高地址
存储内容: 0x34 0x12
判断当前系统的大小端
可以通过以下代码判断当前系统的大小端:
#include <stdio.h> int main() { int num = 1; char *ptr = (char *)# if (*ptr == 1) { printf("Little-Endian\n"); } else { printf("Big-Endian\n"); } return 0; } //int num = 1;:定义一个整数 num,并赋值为1。 //char *ptr = (char *)#:将 num 的地址强制转换为 char* 类型,这样 ptr 指向 num 的最低地址字节。 //if (*ptr == 1):检查 ptr 指向的字节是否为1。如果是1,说明最低地址字节存储的是低位字节,系统为小端序;否则为大端序。字节内部存储
字节内部存储通常指的是单个字节(8位)的二进制表示。每个字节可以表示0到255之间的整数。
假设我们有一个字节 0x5A,其二进制表示为 01011010。在内存中,这个字节的每一位存储如下:
位: 7 6 5 4 3 2 1 0
值: 0 1 0 1 1 0 1 0
整数存储
在计算机中,整数通常以二进制形式存储。不同类型的整数(如 char、short、int 、long 等)有不同的位数,因此它们的取值范围也不同。
截断与提升
截断
截断是指将一个较大类型的整数赋值给一个较小类型的整数时,高位部分被丢弃,只保留低位部分。这可能会导致数据丢失或不准确。
#include <stdio.h>
int main() {
int a = 0x12345678; // 定义一个32位的整数 a,值为0x12345678。
char b = a; // 将32位的整数 a 赋值给8位的整数 b。
printf("a = 0x%x\n", a); // 输出 a的值,结果为 0x12345678
printf("b = 0x%x\n", b); // 输出 b 的值,结果为 0x78。
return 0;
}
可以看到当我把一个十六进制的数a赋给一个字符b时,由于 char只有8位,高位部分 0x123456被丢弃,只保留低位的8位二进制 0x78。
所以我们把不同类型数据进行转换时,丢失一部分数据叫做截断
提升
提升是指将一个较小类型的整数赋值给一个较大类型的整数时,较小类型的整数会被扩展到较大类型的整数。扩展的方式取决于整数的符号:
无符号整数:高位部分用0填充。
有符号整数:高位部分用符号位填充(符号扩展)。
#include <stdio.h>
int main() {
char a = -1; // 定义一个8位的有符号整数 a,值为 -1。
int b = a; // 将8位的有符号整数a赋值给32位的整数 b。由于 a 是有符号的,高位部分用符号位 1填充,结果为 -1
printf("a = %d\n", a); // 输出 a 的值——(-1)
printf("b = %d\n", b); // 输出 b 的值——(-1)
unsigned char c = 255; // 定义一个8位的无符号整数 c,值为 255。
unsigned int d = c; // 将8位的无符号整数 c 赋值给32位的无符号整数 d。由于 c 是无符号的,高位部分用 0 填充,结果为 255。
printf("c = %u\n", c); // 输出 c 的值,结果为 255。
printf("d = %u\n", d); // 输出 d的值,结果为 255。
return 0;
}
截断:将较大类型的整数赋值给较小类型的整数时,高位部分被丢弃。
提升:将较小类型的整数赋值给较大类型的整数时,较小类型的整数会被扩展到较大类型的整数。扩展的方式取决于整数的符号。
字符存储
单纯的字符
在之前的学习中我们知道——字符在内存中的存储是以ASCII码值存在的 ,每一个字符占据1个字节的存储空间。
它使用 7 位二进制数(从 0 到 127)来表示 128 个不同的字符,包括常见的英文字母(大写和小写)、数字、标点符号以及一些控制字符。
字符类型的符号性
但是我们会发现,为什么字符会有(signed/unsigned)之分呢?它难道不仅仅只是表示单个字符那么简单?
在了解这些之前我们必须清楚,没有一种类型是可以完美解决任何问题的。每种类型都存在着它的优势以及局限性。所以用有无符号的字符类型同样是为了解决不同的事件,避免不同的程序缺陷。
在C语言中,char 类型确实可以用来表示单个字符,但它本质上是一个整数类型,可以表示从 -128 到 127(有符号)或从 0 到 255(无符号)的整数值。字符的值可以是有符号的或无符号的,这会影响字符的表示和处理。字符类型的符号性主要影响以下几个方面:
1. 整数运算
char 类型可以参与整数运算。有符号和无符号的字符类型在运算时会有不同的行为。例如:
char a = -1;
unsigned char b = 255;
int result = a + b;//如果 a 是有符号的a的值为 -1。
如果 b 是无符号的,b 的值为 255。
运算结果取决于符号性:
有符号 char 的 -1 会被提升为-1(32位整数)。
无符号 char 的 255会被提升为 255(32位整数)。
结果为 -1 + 255 = 254。
而且存储时,有无符号会影响值的变化范围
2. 符号扩展
当 char 类型被提升为更大的整数类型时,符号性会影响扩展的方式:
有符号 char:扩展时会进行符号扩展,高位部分用符号位填充。
无符号 char:扩展时会用0填充高位部分。
例如:
char a = -1;
int b = a;
unsigned char c = 255;
unsigned int d = c;
a 是有符号的 char,值为 -1,二进制表示为 11111111。
b 是 int 类型,a 被提升为int 时,进行符号扩展,结果为 -1。
c是无符号的 char,值为 255,二进制表示为 11111111。
d 是 unsigned int 类型,c 被提升为 unsigned int 时,用0填充高位部分,结果为 255。
3. 字符编码
在字符编码中,字符的值可以是有符号的或无符号的。例如,ASCII码表中的字符值范围是 0 到 127,但在某些系统中,字符值可能会超出这个范围。
4. 标准库函数
一些标准库函数(如printf)在处理字符时会根据符号性进行不同的处理。例如:
char a = -1;
unsigned char b = 255;
printf("%d\n", a); // 输出 -1
printf("%d\n", b); // 输出 255
浮点数存储
二进制转换
在Double与Float四舍五入中我们曾提到浮点数在存储时存在精度问题,而这与二进制的转换有息息相关。如果我们了解二进制的话我们可以知道———浮点数小数点后的数字是可能需要用非常多为二进制位才能表达的。
举一个简单的例子:12.63
它转换为二进制时,整数部分是很好处理的(0 * 2 ^ 0) + (0 * 2 ^ 1)+(1 * 2 ^ 2)+(1 * 2 ^ 3).表示为1100
而小数部分由于只能是2的指数被,需要不断地向下寻值,直到刚好凑齐0.63
1* 0.5+0 * 0.25+1 * 0.125 +0 *0.725 +0 *0.3825 +0 *0.19125+0 *0.095625+1 *0.0478125.......
最后得到的二进制为1100.1010000101000111101011100001010001111010111000010101
额.....显而易见当我们遇到一个不友好的浮点数时,不仅是计算非常麻烦,把它存储起来更是需要极多的位数。虽然我们的数据是来之不易的,但是我们的内存同样也是十分的宝贵滴,不可能用这么多的内存存储浮点数据,挤压其他数据的存储空间。
一般规定 float浮点型数据占 4字节 32个bit double浮点型数据占 8字节 64个bit
大多时候,我们得到一个近似的值其实也够用了,而为了使表达的浮点数范围足够的大,所以在计算机中,浮点数通常使用 IEEE 754 标准来存储。IEEE 754 标准定义了两种常见的浮点数格式:单精度(32位)和双精度(64位)。浮点数的存储格式包括三个部分:符号位S、指数部分E和尾数部分M。
单精度浮点数(32位)
单精度浮点数的存储格式如下:
符号位(1位):表示浮点数的正负,0表示正数,1表示负数。
指数部分(8位):表示浮点数的指数,使用偏移量(表示为小数点移动的位数,参照十进制123.456=1.23456 * 10 ^ 2,2就是十进制中的偏移量,二进制用2^n表示,n就是偏移量)来表示,单精度浮点数的偏移量为127。
尾数部分(23位):表示浮点数的有效数字,尾数部分隐含了一个前导1,因此尾数部分的实际精度为24位。
假设我们要存储浮点数 -123.456,我们可以将其转换为 IEEE 754 格式。
- 符号位:负数,符号位为1。
- 指数部分:将 123.456 转换为二进制表示,得到 1111011.011101001011110001101010011111101111100111011011。
- 规范化:将二进制数规范化,得到 1.111011011101001011110001101010011111101111100111011011 * 2^6。
- 指数部分:指数为6,加上偏移量127,得到133,二进制表示为 10000101。
- 尾数部分:取规范化后的尾数部分,得到 11101101110100101111000。
最终的单精度浮点数存储格式为:
符号位:1 指数部分:10000101 尾数部分:11101101110100101111000
双精度浮点数(64位)
双精度浮点数的存储格式如下:
符号位(1位):表示浮点数的正负,0表示正数,1表示负数。
指数部分(11位):表示浮点数的指数,使用偏移量来表示,双精度浮点数的偏移量为1023。
尾数部分(52位):表示浮点数的有效数字,尾数部分隐含了一个前导1,因此尾数部分的实际精度为53位。
同样,若想将123.456以double浮点数类型存储起来,也是通过转换为IEEE754格式实现
- 符号位:负数,符号位为1。
- 指数部分:将 123.456 转换为二进制表示,得到 1111011.011101001011110001101010011111101111100111011011。
- 规范化:将二进制数规范化,得到 1.111011011101001011110001101010011111101111100111011011 * 2^6。
- 指数部分:指数为6,加上偏移量1023,得到1029,二进制表示为 10000000101。
- 尾数部分:取规范化后的尾数部分,得到 111011011101001011110001101010011111101111100111011011。
最终的双精度浮点数存储格式为:
符号位:1 指数部分:10000000101 尾数部分:111011011101001011110001101010011111101111100111011011
浮点数存储中的真实值和中间值
实际上,在浮点数存储中,真实值(即实际的浮点数值)和中间值(即存储在计算机中的二进制表示)之间存在一定的转换关系。IEEE 754 标准定义了如何将浮点数转换为二进制表示,并存储在内存中。
真实值到中间值的转换
假设我们要将浮点数 -123.456 转换为单精度浮点数的中间值。
- 符号位:负数,符号位为1。
- 指数部分:将
123.456转换为二进制表示,得到 1111011.011101001011110001101010011111101111100111011011。 - 规范化:将二进制数规范化,得到 1.111011011101001011110001101010011111101111100111011011 * 2^6。
- 指数部分:指数为6,加上偏移量127,得到133,二进制表示为 1000010。
- 尾数部分:取规范化后的尾数部分,得到 11101101110100101111000。
最终的单精度浮点数存储格式为:
符号位:1
指数部分:10000101
尾数部分:11101101110100101111000
中间值到真实值的转换
假设我们有一个单精度浮点数的中间值 0xC2F6E979,我们将其转换为真实值。
- 符号位:最高位为1,表示负数。
- 指数部分:接下来的8位为 10000101,转换为十进制为133。减去偏移量127,得到指数为6。
- 尾数部分:剩下的23位为 11101101110100101111000,加上隐含的前导1,得到 1.11101101110100101111000。
将尾数部分乘以 2^6,得到 1111011.011101001011110001101010011111101111100111011011,转换为十进制为 -123.456。