1. 整数在内存中的存储
整数的2进制表⽰⽅法有三种,即原码、反码和补码
有符号的整数,三种表⽰⽅法均有符号位和数值位两部分,符号位都是⽤0表⽰"正",⽤1表
⽰"负",最⾼位的⼀位是被当做符号位,剩余的都是数值位。
正整数的原、反、补码都相同。
负整数的三种表⽰⽅法各不相同。
原码:直接将数值按照正负数的形式翻译成⼆进制得到的就是原码。
反码:将原码的符号位不变,其他位依次按位取反就可以得到反码。
为了更直观地对比原码、反码和补码的区别,下面以 -5 为例,列出三种编码的详细对比:
| 编码类型 | 定义 | 计算方法 | 示例(-5,假设8位有符号整数) |
|---|---|---|---|
| 原码 | 直接将数值按正负形式翻译成二进制,最高位为符号位(0正1负),其余为数值位 | 符号位写1,数值部分写5的二进制 0000101 |
1000 0101 |
| 反码 | 原码的符号位不变,其他位按位取反 | 原码 1000 0101 → 符号位1不变,数值位 0000101 取反得 1111010 |
1111 1010 |
| 补码 | 反码 + 1 | 反码 1111 1010 + 1 = 1111 1011 |
1111 1011 |
说明:
- 正整数的原码、反码、补码完全相同,例如 +5 的三种编码都是
0000 0101。 - 负整数的三种编码各不相同,如上表所示,-5 的原码、反码、补码依次为
1000 0101、1111 1010、1111 1011。 - 计算机中实际存储的是补码,因为补码可以将减法统一为加法运算,简化硬件设计。
补码:反码+1就得到补码。
2. ⼤⼩端字节序和字节序判断
其实超过⼀个字节的数据在内存中存储的时候,就有存储顺序的问题,按照不同的存储顺序,我们分为⼤端字节序存储和⼩端字节序存储,下⾯是具体的概念:
⼤端(存储)模式:
是指数据的低位字节内容保存在内存的⾼地址处,⽽数据的⾼位字节内容,保存在内存的低地址处。
⼩端(存储)模式:
是指数据的低位字节内容保存在内存的低地址处,⽽数据的⾼位字节内容,保存在内存的⾼地址处。
下面是一个C语言程序,用于判断当前机器的字节序是大端还是小端:
c
#include <stdio.h>
// 方法1:通过联合体(union)判断字节序
// 联合体的特点是所有成员共享同一块内存空间
int check_endian_union() {
union {
int i; // 4字节整数
char c[4]; // 4字节字符数组
} u;
u.i = 0x12345678; // 将一个已知值写入整数成员
// 如果小端存储:低地址存放低位字节,即 c[0] = 0x78
// 如果大端存储:低地址存放高位字节,即 c[0] = 0x12
if (u.c[0] == 0x78) {
return 0; // 小端
} else if (u.c[0] == 0x12) {
return 1; // 大端
} else {
return -1; // 未知
}
}
// 方法2:通过指针强制类型转换判断字节序
int check_endian_pointer() {
int i = 0x12345678; // 定义一个整数
unsigned char* p = (unsigned char*)&i; // 将int指针转为char指针,指向最低地址
// 读取最低地址处的第一个字节
// 小端:最低地址存放的是低位字节 0x78
// 大端:最低地址存放的是高位字节 0x12
if (*p == 0x78) {
return 0; // 小端
} else if (*p == 0x12) {
return 1; // 大端
} else {
return -1; // 未知
}
}
int main() {
printf("===== 字节序判断程序 =====\n\n");
// 使用联合体方法判断
int result1 = check_endian_union();
printf("【方法1:联合体判断】\n");
printf("测试值:0x12345678\n");
printf("内存布局(从低地址到高地址):\n");
union {
int i;
unsigned char c[4];
} u;
u.i = 0x12345678;
for (int j = 0; j < 4; j++) {
printf(" 地址 +%d:0x%02X\n", j, u.c[j]);
}
if (result1 == 0) {
printf("结论:当前机器为 **小端字节序**\n");
printf("解释:低地址(地址+0)存放的是低位字节 0x78\n");
} else if (result1 == 1) {
printf("结论:当前机器为 **大端字节序**\n");
printf("解释:低地址(地址+0)存放的是高位字节 0x12\n");
}
printf("\n");
// 使用指针方法判断
int result2 = check_endian_pointer();
printf("【方法2:指针强制转换判断】\n");
int test_val = 0x12345678;
unsigned char* p = (unsigned char*)&test_val;
printf("测试值:0x12345678\n");
printf("最低地址处的字节内容:0x%02X\n", *p);
if (result2 == 0) {
printf("结论:当前机器为 **小端字节序**\n");
printf("解释:将int指针转为char指针后,读取最低地址得到 0x%02X,这是低位字节\n", *p);
} else if (result2 == 1) {
printf("结论:当前机器为 **大端字节序**\n");
printf("解释:将int指针转为char指针后,读取最低地址得到 0x%02X,这是高位字节\n", *p);
}
printf("\n===== 原理说明 =====\n");
printf("1. 我们定义一个整数 0x12345678,它在内存中占用4个字节\n");
printf("2. 这4个字节分别是:0x12(最高位)、0x34、0x56、0x78(最低位)\n");
printf("3. 通过联合体或指针,我们可以访问整数在内存中的第一个字节(最低地址)\n");
printf("4. 如果第一个字节是 0x78(低位字节),说明是小端模式\n");
printf("5. 如果第一个字节是 0x12(高位字节),说明是大端模式\n");
printf("6. 常见的x86/x64架构(如Intel、AMD)都是小端模式\n");
printf("7. 网络协议通常使用大端模式(网络字节序)\n");
return 0;
}
代码逐步解释:
-
联合体方法(
check_endian_union):- 联合体
union中的所有成员共享同一块内存空间 - 我们定义了一个包含
int和char[4]的联合体,两者都占用4个字节 - 给
u.i赋值为0x12345678后,通过u.c[0]读取第一个字节 - 小端模式下,
u.c[0]是0x78(低位字节在低地址) - 大端模式下,
u.c[0]是0x12(高位字节在低地址)
- 联合体
-
指针方法(
check_endian_pointer):- 将
int*强制转换为unsigned char*,这样指针就指向整数的第一个字节 - 解引用
*p得到最低地址处的字节内容 - 根据该字节的值判断字节序
- 将
-
内存布局输出:
- 程序会打印出整数
0x12345678在内存中从低地址到高地址的4个字节 - 小端:
78 56 34 12(低地址→高地址) - 大端:
12 34 56 78(低地址→高地址)
- 程序会打印出整数
-
实际应用:
- 在网络编程中,需要将主机字节序转换为网络字节序(大端),使用
htonl()、htons()等函数 - 在读写二进制文件或跨平台数据传输时,必须考虑字节序问题
- 在网络编程中,需要将主机字节序转换为网络字节序(大端),使用
3. 浮点数在内存中的存储
常⻅的浮点数:3.14159、1E10等,浮点数家族包括:float、double、long double 类型。
浮点数表⽰的范围:float.h 中定义。
3.1 练习
c
int main()
{
int n = 9;
//00000000000000000000000000001001
//
float* pFloat = (float*)&n;
printf("n的值为:%d\n", n);
printf("*pFloat的值为:%f\n", *pFloat);//0.000000
//0 00000000 00000000000000000001001
//0.00000000000000000001001 * 2^-126
//
*pFloat = 9.0;
//1001.0
//(-1)^0 * 1.001 * 2^3
//S = 0
//E = 3 +127 = 130
//M = 1.001
//0 10000010 00100000000000000000000
//
//
printf("n的值为:%d\n", n);
//01000001000100000000000000000000
printf("*pFloat的值为:%f\n", *pFloat);
//n的值为:9
//*pFloat的值为:0.000000
//n的值为:1091567616
//*pFloat的值为:9.000000
return 0;
}
3.2 浮点数的存储
例如,数值 9.0 的二进制表示为 0 10000010 00100000000000000000000,其中 S=0(正数),E=130(真实指数 = 130 - 127 = 3),M=001...(有效数字为 1.001,即十进制 1.125),最终值为 (-1)^0 × 1.125 × 2^3 = 9.0。
3.2.1 浮点数取的过程
E不全为0或不全为1(常规情况)
这时,浮点数就采⽤下⾯的规则表⽰,即指数E的计算值减去127(或1023),得到真实值,再将有效数字M前加上第⼀位的1。
c
0 01111110 00000000000000000000000
E全为0
这时,浮点数的指数E等于1-127(或者1-1023)即为真实值,有效数字M不再加上第⼀位的1,⽽是还原为0.xxxxxx的⼩数。这样做是为了表⽰±0,以及接近于0的很⼩的数字。
c
0 00000000 00100000000000000000000
E全为1
这时,如果有效数字M全为0,表⽰±⽆穷⼤(正负取决于符号位s);如果M不全为0,表⽰NaN(Not a Number,⾮数值)。
详细解释:
当指数E的所有位都为1时(即E=255,对于float类型),这是一个特殊的标记值,用于表示无穷大或非数值。
- M全为0 → 无穷大 :当有效数字M的所有位都是0时,这个浮点数表示的是无穷大。符号位s决定正负:s=0表示+∞,s=1表示-∞。例如,
0 11111111 00000000000000000000000表示 +∞,而1 11111111 00000000000000000000000表示 -∞。 - M不全为0 → NaN:当M中至少有一位是1时,这个浮点数表示NaN(Not a Number),用于表示无效的运算结果,比如0/0、∞-∞等。
为什么这样设计?
IEEE 754标准之所以用E全为1来编码无穷大和NaN,是因为在科学计算和工程应用中,经常需要处理溢出、除零等异常情况。通过保留专门的位模式来表示这些特殊值,程序可以在不崩溃的情况下继续执行,并检测到异常。
C语言代码示例与逐步解释:
c
#include <stdio.h>
#include <math.h> // 用于 isinf() 和 isnan() 函数
int main() {
// 示例1:通过除法得到正无穷大
double pos_inf = 1.0 / 0.0;
printf("1.0 / 0.0 = %f\n", pos_inf); // 输出:inf
printf("isinf(pos_inf) = %d\n", isinf(pos_inf)); // 输出:1(是无穷大)
// 示例2:通过除法得到负无穷大
double neg_inf = -1.0 / 0.0;
printf("-1.0 / 0.0 = %f\n", neg_inf); // 输出:-inf
printf("isinf(neg_inf) = %d\n", isinf(neg_inf)); // 输出:1
// 示例3:通过溢出得到无穷大
double overflow = 1e308 * 10.0; // double最大值约1.8e308,乘以10会溢出
printf("1e308 * 10.0 = %f\n", overflow); // 输出:inf
printf("isinf(overflow) = %d\n", isinf(overflow)); // 输出:1
// 示例4:0/0 得到 NaN
double nan_val = 0.0 / 0.0;
printf("0.0 / 0.0 = %f\n", nan_val); // 输出:nan
printf("isnan(nan_val) = %d\n", isnan(nan_val)); // 输出:1(是NaN)
// 示例5:无穷大参与运算
printf("pos_inf + 100 = %f\n", pos_inf + 100); // 输出:inf
printf("pos_inf + neg_inf = %f\n", pos_inf + neg_inf); // 输出:nan(∞-∞无意义)
// 示例6:手动构造一个float类型的正无穷大
// 二进制:0 11111111 00000000000000000000000
// 即:符号位0,指数全1,有效数字全0
unsigned int inf_bits = 0x7F800000; // 0 11111111 000...0
float* p_inf = (float*)&inf_bits;
printf("手动构造的float正无穷大 = %f\n", *p_inf); // 输出:inf
printf("isinf(*p_inf) = %d\n", isinf(*p_inf)); // 输出:1
return 0;
}
代码逐步解释:
-
double pos_inf = 1.0 / 0.0;:在C语言中,浮点数除以0不会导致程序崩溃,而是返回无穷大。1.0/0.0的结果是正无穷大,其内部二进制表示为0 11111111 000...0(double类型指数11位全1,有效数字全0)。 -
double neg_inf = -1.0 / 0.0;:负数的除法得到负无穷大,符号位为1。 -
double overflow = 1e308 * 10.0;:当计算结果超出double能表示的最大值(约1.8×10³⁰⁸)时,发生上溢,结果被表示为无穷大。 -
double nan_val = 0.0 / 0.0;:0/0是一个未定义的数学运算,结果被表示为NaN。NaN的二进制特征是:指数全1,有效数字不全为0。 -
无穷大参与运算:无穷大加上任何有限数仍然是无穷大;但正无穷大加负无穷大没有意义,结果为NaN。
-
手动构造无穷大 :通过将内存中的位模式
0x7F800000解释为float,我们直接构造了一个正无穷大。这个32位二进制数正好对应0 11111111 00000000000000000000000。
关键点总结:
- E全为1且M全为0 → ±∞(无穷大),用于表示溢出或除零的结果
- E全为1且M不全为0 → NaN(非数值),用于表示无效运算结果
- 使用
isinf()和isnan()函数可以检测这些特殊值 - 无穷大和NaN可以参与运算,遵循IEEE 754的特殊规则