引言
在C语言中,数据在内存中是如何存储的?为什么-1用%u打印出来是4294967295?为什么char类型的128用%d打印出来是-128?为什么浮点数9.0在内存中是00 00 10 41?
理解数据在内存中的存储方式,是掌握C语言底层原理的关键。今天,我将从二进制角度,深入讲解整数、浮点数在内存中的存储规则,以及原码、反码、补码的转换关系。
第一部分:原码、反码、补码
一、为什么计算机使用补码存储?
计算机只能做加法运算,减法需要转换为加法。补码的出现使得减法可以用加法实现,简化了硬件设计。
原码(Sign-Magnitude)
最高位表示符号(0正1负),其余位表示数值
核心规则:计算机只保存数值的补码
| 十进制 | 原码 |
|---|---|
| +5 | 0101 |
| -5 | 1101 |
| +0 | 0000 |
| -0 | 1000 |
问题:
有两个零(+0 和 -0),浪费一个编码
减法不能用加法直接实现
反码(Ones' Complement)
正数不变,负数 = 原码符号位不变,其余位取反
| 十进制 | 原码 | 反码 |
|---|---|---|
| +5 | 0101 |
0101 |
| -5 | 1101 |
1010 |
| +0 | 0000 |
0000 |
| -0 | 1000 |
1111 |
问题:
仍然有两个零
加法需要"循环进位"(端回进位),硬件复杂
**补码(Two's Complement)**现代计算机使用
正数不变,负数 = 反码 + 1
| 十进制 | 原码 | 反码 | 补码 |
|---|---|---|---|
| +5 | 0101 |
0101 |
0101 |
| -5 | 1101 |
1010 |
1011 |
| +0 | 0000 |
0000 |
0000 |
| -0 | 1000 |
1111 |
0000 ← 和 +0 相同 |
优点:
只有一个零
减法 = 加法,直接运算
硬件简单
记忆口诀
正数三码都一样,负数反码原码取反,补码反码再加一
或者:
原码看符号,反码按位取反,补码反码加一,减法秒变加法
二、转换示例
cpp
#include <stdio.h>
// 以 int a = -10 为例(32位)
// 原码:10000000 00000000 00000000 00001010
// 反码:11111111 11111111 11111111 11110101(符号位不变,其余取反)
// 补码:11111111 11111111 11111111 11110110(反码+1)
// 内存中存储:F6 FF FF FF(小端序)
int main() {
int a = -10;
// 查看内存中的十六进制表示
unsigned char* p = (unsigned char*)&a;
for (int i = 0; i < sizeof(a); i++) {
printf("%02X ", p[i]);
}
printf("\n"); // 输出:F6 FF FF FF(小端:低位在低地址)
return 0;
}
三、各种整型的取值范围
| 类型 | 有符号范围 | 无符号范围 |
|---|---|---|
char |
-128 ~ 127 | 0 ~ 255 |
short |
-32768 ~ 32767 | 0 ~ 65535 |
int |
-2147483648 ~ 2147483647 | 0 ~ 4294967295 |
long long |
-2^63 ~ 2^63-1 | 0 ~ 2^64-1 |
特殊值注意:
-
char类型:-128的补码是10000000 -
short类型:-32768的补码是10000000 00000000 -
int类型:-2147483648的补码是10000000 00000000 00000000 00000000
第二部分:整型在内存中的存储
一、有符号整型的打印规则
cpp
#include <stdio.h>
int main() {
int a = -10;
// %d:有符号十进制(按补码解释,输出原码对应的值)
printf("%d\n", a); // -10
// %u:无符号十进制(将补码直接当作无符号数解释)
printf("%u\n", a); // 4294967286(2^32 - 10)
// %x:十六进制(直接输出补码的十六进制)
printf("%x\n", a); // fffffff6
// %p:指针格式(十六进制,通常带前导0)
printf("%p\n", a); // 0xfffffff6
return 0;
}
二、不同格式化输出的影响
cpp
int main() {
int a = -1;
// 补码:11111111 11111111 11111111 11111111
printf("%d\n", a); // -1(有符号解释)
printf("%u\n", a); // 4294967295(2^32 - 1)
printf("%hd\n", a); // -1(截取低16位:11111111 11111111 → -1)
printf("%hu\n", a); // 65535(低16位作为无符号)
return 0;
}
三、char类型的特殊问题
cpp
int main() {
char a = 128;
// 128的二进制:10000000(8位)
// 作为char(有符号),最高位是符号位,表示负数
// 补码10000000代表-128
printf("%d\n", a); // -128
printf("%u\n", a); // 4294967168(32位:11111111 11111111 11111111 10000000)
char b = -128;
printf("%d\n", b); // -128
printf("%u\n", b); // 4294967168
return 0;
}
四、unsigned char的特殊性
cpp
int main() {
unsigned char c = -1;
// -1的补码(8位):11111111
// 作为unsigned char,直接解释为255
printf("%d\n", c); // 255(%d会进行整型提升,高位补0)
printf("%u\n", c); // 255
signed char d = -1;
printf("%d\n", d); // -1(符号位扩展)
printf("%u\n", d); // 4294967295(符号位扩展后全1)
return 0;
}
第三部分:大小端存储
一、什么是大小端?
| 模式 | 规则 | 示例(int 0x12345678) |
|---|---|---|
| 小端 | 低地址存低位 | 78 56 34 12 |
| 大端 | 低地址存高位 | 12 34 56 78 |
cpp
#include <stdio.h>
int main() {
int a = 0x12345678;
unsigned char* p = (unsigned char*)&a;
printf("内存存储(低地址→高地址):");
for (int i = 0; i < 4; i++) {
printf("%02X ", p[i]);
}
printf("\n");
// 小端输出:78 56 34 12
// 大端输出:12 34 56 78
return 0;
}
二、指针类型对内存访问的影响
cpp
int main() {
int a = -596;
// 补码:11111111 11111111 11111101 10101100(FD FF FF FF?需验证)
char* p1 = (char*)&a; // 指向第一个字节
short* p2 = (short*)&a; // 指向short
unsigned char* p3 = (unsigned char*)&a;
unsigned short* p4 = (unsigned short*)&a;
// 指针+1:移动sizeof(指针类型)个字节
printf("%d\n", *(p1 + 1)); // 第二个字节作为有符号char解释
printf("%d\n", *(p2 + 1)); // 第3-4字节作为short解释
printf("%u\n", *(p3 + 2)); // 第三个字节作为无符号解释
printf("%u\n", *(p4 + 1)); // 第3-4字节作为unsigned short解释
return 0;
}
第四部分:浮点数的存储(IEEE 754)
一、浮点数格式
S:符号位 (Sign)
作用:决定数字是正数还是负数
取值:0 或 1
M:尾数 / 有效数字 (Mantissa / Significand)
作用 :存储数字的小数部分(精度)
取值范围 :0 ≤ M < 1
E:指数 (Exponent)
作用:控制数字的大小范围(小数点移动多少位)
存储形式 :移码存储(不是补码)
偏移量 (Bias)
作用 :让指数 E 可以存储负数,而不需要使用补码(方便比较大小)
计算方法 :2^(k-1) - 1,其中 k 是指数字段的位数
| 类型 | 总位数 | 符号位(S) | 指数位(E) | 尾数位(M) | 指数偏移量 |
|---|---|---|---|---|---|
| float | 32 | 1 | 8 | 23 | 127 |
| double | 64 | 1 | 11 | 52 | 1023 |
公式: 值 = (-1)^S × (1.M) × 2^(E - 偏移量)
记忆口诀
S 决定正负,M 存小数,E 控制大小,减去 Bias 得真实指数
二、十进制转浮点数步骤
以 9.75 为例:
步骤1:转换为二进制
9 = 1001
0.75 = 0.11(0.5 + 0.25)
9.75 = 1001.11
步骤2:规格化为科学计数法
1001.11 = 1.00111 × 2^3
步骤3:确定各字段
S = 0(正数)
E = 3 + 127 = 130 = 10000010
M = 00111(去掉整数部分的1,后面补0到23位)
步骤4:拼接二进制
0 10000010 00111000000000000000000
步骤5:分组为字节(小端存储)
01000001 00011100 00000000 00000000
41 1C 00 00
内存中:00 00 1C 41
cpp
#include <stdio.h>
int main() {
float f = 9.75f;
unsigned char* p = (unsigned char*)&f;
printf("9.75的内存存储:");
for (int i = 0; i < 4; i++) {
printf("%02X ", p[i]);
}
printf("\n"); // 输出:00 00 1C 41
return 0;
}
三、更多浮点数示例
9.0的存储:
9.0 = 1001.0 = 1.001 × 2^3
S=0, E=3+127=130=10000010, M=001
二进制:0 10000010 00100000000000000000000
内存:00 00 10 41
0.5625的存储:
0.5625 = 0.1001 = 1.001 × 2^(-1)
S=0, E=-1+127=126=01111110, M=001
二进制:0 01111110 00100000000000000000000
内存:00 00 90 3F
795.0625的存储:
795 = 1100011011
0.0625 = 0.0001
795.0625 = 1100011011.0001 = 1.1000110110001 × 2^9
S=0, E=9+127=136=10001000, M=1000110110001
内存:00 C4 46 C4
四、浮点数转整型(截断)
cpp
int main() {
float f = 9.75;
int i = (int)f;
printf("%d\n", i); // 9(直接舍弃小数部分,不是四舍五入)
return 0;
}
第五部分:整型提升与算术转换
一、整型提升规则
当char、short等小于int的类型参与运算时,会先提升为int。
cpp
int main() {
char a = -1;
char b = 1;
// a和b被提升为int后运算
char c = a + b; // -1 + 1 = 0
unsigned char d = -1; // 255
char e = d; // 255作为有符号char解释 → -1
return 0;
}
二、算术转换
当两个不同类型运算时,会向精度更高的类型转换。
cpp
int main() {
// strlen返回size_t(unsigned int)
if (strlen("abc") - strlen("abcdef") > 0) {
printf(">\n");
} else {
printf("<\n");
}
// 输出:>(因为3-6 = -3,作为无符号数是4294967293)
printf("%d\n", strlen("abc") - strlen("abcdef")); // -3
printf("%u\n", strlen("abc") - strlen("abcdef")); // 4294967293
return 0;
}
第六部分:常见陷阱与经典题目
陷阱1:无符号循环死循环
cpp
int main() {
unsigned int i;
for (i = 9; i >= 0; i--) {
printf("%u\n", i);
}
// 当i=0时,i--变成4294967295,永远不小于0,死循环
return 0;
}
陷阱2:char数组越界与strlen
cpp
int main() {
char a[1000];
int i;
for (i = 0; i < 1000; i++) {
a[i] = -1 - i;
}
// 当-1-i = -128时,补码10000000,再减1变成127
// 所以数组内容会循环:-1,-2,...,-128,127,126,...,0,-1,...
// strlen在遇到第一个0时停止
printf("%zu\n", strlen(a)); // 255
return 0;
}
陷阱3:char类型范围溢出
cpp
int main() {
unsigned char i;
for (i = 0; i <= 255; i++) {
printf("Hello World!\n");
}
// 当i=255时,i++变成256,但unsigned char只能存0-255
// 256会溢出变成0,导致无限循环
return 0;
}
总结
一、整型存储核心规则
| 规则 | 说明 |
|---|---|
| 计算机存储补码 | 正数三码合一,负数需要转换 |
| 有符号/无符号 | 同一补码解释不同,结果不同 |
| 大小端 | 影响多字节数据的内存布局 |
| 整型提升 | 小于int的类型运算时先提升为int |
二、浮点数存储核心规则
| 规则 | float | double |
|---|---|---|
| 公式 | (-1)^S × (1.M) × 2^(E-127) |
(-1)^S × (1.M) × 2^(E-1023) |
| 偏移量 | 127 | 1023 |
| 精度 | 约7位十进制 | 约15位十进制 |
三、格式化输出对照表
| 格式 | 解释方式 | 示例(int a=-1) |
|---|---|---|
%d |
有符号十进制 | -1 |
%u |
无符号十进制 | 4294967295 |
%x |
十六进制 | ffffffff |
%p |
指针格式 | 0xffffffff |
%hd |
short有符号 | -1 |
%hu |
short无符号 | 65535 |
%hhd |
char有符号 | -1 |
%hhu |
char无符号 | 255 |
理解数据在内存中的存储方式,是深入掌握C语言的必经之路。原码、反码、补码的转换规则,大小端存储的影响,浮点数的IEEE 754标准,这些都是面试和实际开发中经常遇到的问题。
学习建议:
-
多动手打印变量的内存十六进制
-
理解有符号/无符号的解释差异
-
注意类型转换时的截断和提升规则
-
警惕unsigned类型的循环边界