C语言数据存储深度解析:从原码反码补码到浮点数存储

引言

在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;
}

第五部分:整型提升与算术转换

一、整型提升规则

charshort等小于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标准,这些都是面试和实际开发中经常遇到的问题。

学习建议:

  1. 多动手打印变量的内存十六进制

  2. 理解有符号/无符号的解释差异

  3. 注意类型转换时的截断和提升规则

  4. 警惕unsigned类型的循环边界

相关推荐
hipolymers2 小时前
C语言怎么样?难学吗?
c语言·数据结构·学习·算法·编程
2501_933329554 小时前
企业级舆情监测系统技术解析:Infoseek数字公关AI中台架构与实践
开发语言·人工智能·自然语言处理·架构
Wave8454 小时前
C++继承详解
开发语言·c++·算法
Tairitsu_H4 小时前
C++类基础概念:定义、实例化和this指针
开发语言·c++
.柒宇.4 小时前
Java八股之反射
java·开发语言
环流_5 小时前
多线程1(面试题--常见的线程创建方式)
java·开发语言·面试
努力努力再努力wz5 小时前
【Linux网络系列】深入理解 I/O 多路复用:从 select 痛点到 poll 高并发服务器落地,基于 Poll、智能指针与非阻塞 I/O与线程池手写一个高性能 HTTP 服务器!(附源码)
java·linux·运维·服务器·c语言·c++·python
Han_han9195 小时前
常用API:
java·开发语言
minji...5 小时前
Linux 线程同步与互斥(四) POSIX信号量,基于环形队列的生产者消费者模型
linux·运维·服务器·c语言·开发语言·c++