欢迎光临
我们一直在努力

面试必问!!!:补码、大小端、类型转换

面试必问:补码、大小端、类型转换,一篇打通

1. 测试环境说明

以下数据基于 Visual Studio x86(32位) 环境实测。不同平台下部分类型大小可能不同,后文会详细说明。

项目配置
开发环境 Visual Studio
目标平台 x86(32位)
字节序 小端存储

2. 本文目标

初学C语言时只知道 int 是 4 字节、char 是 1 字节,但不清楚为什么这样设计、负数怎么存、类型之间怎么转换。

本文目标:画完整数家族的全貌,搞懂补码、大小端、类型转换这三个面试高频考点。


3. 有符号整数家族

有符号整数可以表示正数、负数和零。存储结构是 1位符号位 + N位数值位。

类型格式控制符大小(Byte)存储结构范围
char %hhd 1 1符号+7数值 -128 ~ 127
short %hd 2 1符号+15数值 -32768 ~ 32767
int %d 4 1符号+31数值 -2^31 ~ 2^31-1
long %ld 4 1符号+31数值 -2^31 ~ 2^31-1
long long %lld 8 1符号+63数值 -2^63 ~ 2^63-1

注意:long 在 32 位 Windows 上是 4 字节,在 64 位 Linux 上是 8 字节。本文环境是 32 位 VS,所以 long = int = 4 字节。


4. 无符号整数家族

无符号整数所有位都表示数值,没有符号位。

类型格式控制符大小(Byte)范围
unsigned char %hhu 1 0 ~ 255
unsigned short %hu 2 0 ~ 65535
unsigned int %u 4 0 ~ 2^32-1
unsigned long %lu 4 0 ~ 2^32-1
unsigned long long %llu 8 0 ~ 2^64-1

适用场景:身高、体重、数组下标、size_t 等没有负数的数据。


5. 补码:负数在内存中的存储方式

计算机中所有整数都以补码形式存储。这是面试最高频的考点之一。

5.1 正数:原码 = 反码 = 补码

正数三码合一,直接存其二进制形式。

int a = 10;
// 原码:(0) 00000000 00000000 00000000 00001010
// 补码:(0) 00000000 00000000 00000000 00001010
// 内存(小端):0A 00 00 00

5.2 负数:原码 → 取反 → +1

负数需要三步才能存入内存。以 -10 为例:

// 第一步:写原码(符号位=1,数值位=绝对值)
// 原码:(1) 00000000 00000000 00000000 00001010
// ↑符号位为1表示负数,后面是10的二进制

// 第二步:求反码(符号位不变,其余位取反)
// 反码:(1) 11111111 11111111 11111111 11110101
// ↑符号位保持1,后面是原码数值位的按位取反

// 第三步:得补码(反码最低位加1)
// 补码:(1) 11111111 11111111 11111111 11110110
// ↑这就是最终存储在内存中的二进制形式

// 内存查看(小端存储):
// 地址: 0x00 0x01 0x02 0x03
// 数据: 0xF6 0xFF 0xFF 0xFF
// 解释: 低地址存低字节,所以看到的是 F6 FF FF FF

计算过程总结:

  • 原码:符号位1 + 绝对值的二进制
  • 反码:符号位不变,数值位按位取反
  • 补码:反码加1(得到最终存储形式)
  • 5.3 从内存读出负数:减 1 → 取反

    如果看到内存中最高位是 1,就知道它是负数,需要反向操作:

    补码:(1) 11111111 11111111 11111111 11110110
    减一:(1) 11111111 11111111 11111111 11110101
    取反:(1) 00000000 00000000 00000000 00001010 → 原码 = -10

    5.4 为什么用补码?

    统一加减法,省去减法电路。 A – B 就是 A + (-B),CPU 只需要加法器就能完成所有整数运算。

    补码运算示例:

    int a = 10; // 补码:00001010
    int b = 6; // 补码:11111010
    int c = a + b; // 00001010 + 11111010 = 00000100 = 4
    // 10 + (-6) = 4,验证正确

    6. 大小端:字节的存储顺序

    同一个整数 0x12345678 在不同 CPU 上的存储顺序不同。这是面试中考察“是否真的写过底层代码”的标志性问题。

    模式低地址 → 高地址常见平台
    小端 78 56 34 12 x86/x64(VS 32位也是小端)
    大端 12 34 56 78 部分嵌入式、网络字节序

    验证方法:

    • 用 VS 调试器查看内存窗口
    • 或 GDB 的 x/4xb &a,第一个字节是 0x78 就是小端

    内存布局对比:

    小端存储(x86/x64):
    地址:0x00 0x01 0x02 0x03
    数据:0x78 0x56 0x34 0x12

    大端存储(网络字节序):
    地址:0x00 0x01 0x02 0x03
    数据:0x12 0x34 0x56 0x78

    7. 类型转换的底层规则

    7.1 大字节 → 小字节:截取低字节

    int a = 0x12345678;
    char c = a; // c = 0x78,高 3 字节被丢弃
    // 内存视角:
    // a: 78 56 34 12 (小端存储)
    // c: 78 (只保留最低字节)

    7.2 小字节 → 大字节:符号扩展

    // 有符号类型:高位全部用符号位填充,保证数值不变
    char c = 1; // 内存:FF
    int i = c; // 内存:FF FF FF FF(符号位 1 填充高位)
    // 解释:c 的符号位是 1,扩展时高位全部填充 1

    // 无符号类型:高位全部用 0 填充
    unsigned char uc = 255; // 内存:FF
    unsigned int ui = uc; // 内存:00 00 00 FF(高位填充 0)

    7.3 有符号 ↔ 无符号:二进制位不变,解读方式变

    unsigned int u = 0xFFFFFFFF;
    printf("%d", u); // 输出 -1(按有符号解读)
    printf("%u", u); // 输出 4294967295(按无符号解读)
    // 同样的二进制,按 %d 解读是 -1,按 %u 解读是 4294967295

    8. sizeof 验证

    用代码验证每种类型在自己的平台上实际占多少字节。

    以下是在 Windows VS x86 环境下的测试代码和结果:

    #include <stdio.h>

    int main() {
    printf("=== Windows VS x86 环境类型大小测试 ===\\n\\n");

    // 有符号整数类型
    printf("有符号整数类型:\\n");
    printf("sizeof(char) = %zu 字节\\n", sizeof(char));
    printf("sizeof(short) = %zu 字节\\n", sizeof(short));
    printf("sizeof(int) = %zu 字节\\n", sizeof(int));
    printf("sizeof(long) = %zu 字节\\n", sizeof(long));
    printf("sizeof(long long) = %zu 字节\\n\\n", sizeof(long long));

    // 无符号整数类型
    printf("无符号整数类型:\\n");
    printf("sizeof(unsigned char) = %zu 字节\\n", sizeof(unsigned char));
    printf("sizeof(unsigned short) = %zu 字节\\n", sizeof(unsigned short));
    printf("sizeof(unsigned int) = %zu 字节\\n", sizeof(unsigned int));
    printf("sizeof(unsigned long) = %zu 字节\\n", sizeof(unsigned long));
    printf("sizeof(unsigned long long) = %zu 字节\\n\\n", sizeof(unsigned long long));

    // 指针类型
    printf("指针类型:\\n");
    printf("sizeof(int*) = %zu 字节\\n", sizeof(int*));
    printf("sizeof(char*) = %zu 字节\\n", sizeof(char*));
    printf("sizeof(void*) = %zu 字节\\n", sizeof(void*));

    return 0;
    }

    运行结果:

    === Windows VS x86 环境类型大小测试 ===

    有符号整数类型:
    sizeof(char) = 1 字节
    sizeof(short) = 2 字节
    sizeof(int) = 4 字节
    sizeof(long) = 4 字节
    sizeof(long long) = 8 字节

    无符号整数类型:
    sizeof(unsigned char) = 1 字节
    sizeof(unsigned short) = 2 字节
    sizeof(unsigned int) = 4 字节
    sizeof(unsigned long) = 4 字节
    sizeof(unsigned long long) = 8 字节

    指针类型:
    sizeof(int*) = 4 字节
    sizeof(char*) = 4 字节
    sizeof(void*) = 4 字节

    验证结论:

  • 在 Windows VS x86 环境下,int 和 long 都是 4 字节
  • 指针类型大小为 4 字节(32位系统)
  • 与前面表格中的理论值完全一致
  • 在这里插入图片描述
    在这里插入图片描述

    9. 平台差异提醒

    不同平台下类型大小可能不同,以下是常见差异对比:

    类型32位 Windows(本文环境)64位 Linux说明
    char 1 字节 1 字节 字符类型,固定1字节
    short 2 字节 2 字节 短整型,固定2字节
    int 4 字节 4 字节 整型,通常4字节
    long 4 字节 8 字节 主要差异点
    long long 8 字节 8 字节 长整型,固定8字节
    指针 4 字节 8 字节 主要差异点

    经验:跨平台代码不要假设 long 和指针的大小,用 sizeof 实测或用固定大小的类型(如 int32_t、int64_t)。

    10. 踩坑记录

    坑1:格式控制符用错

    char a = 255;
    printf("%hhd\\n", a); // -1(255 按有符号解读是 -1)
    printf("%hhu\\n", a); // 255(正确)

    坑2:char 的有符号/无符号不确定

    // char 默认是有符号还是无符号取决于编译器
    // VS 中默认 char 是有符号的
    char c1 = 255; // 可能被解释为 -1
    unsigned char c2 = 255; // 明确指定无符号,保证是 255

    // 需要明确指定 signed char 或 unsigned char
    signed char sc = 128; // 明确有符号
    unsigned char uc = 255; // 明确无符号

    坑3:long 的平台差异

    // 在 VS 32 位下 long = 4 字节
    // 在 64 位 Linux 下 long = 8 字节
    long value = 100;
    printf("sizeof(long) = %zu\\n", sizeof(long)); // 不同平台结果不同

    // 解决方案:使用固定大小类型
    #include <stdint.h>
    int32_t fixed32 = 100; // 固定 4 字节
    int64_t fixed64 = 100; // 固定 8 字节

    坑4:大小端导致的字节序问题

    // 网络传输时需要注意字节序转换
    uint32_t network_value = 0x12345678;
    uint32_t host_value = ntohl(network_value); // 网络字节序转主机字节序

    // 文件读写时也要注意字节序
    FILE* fp = fopen("data.bin", "wb");
    uint32_t data = 0x12345678;
    fwrite(&data, sizeof(data), 1, fp); // 写入的是主机字节序

    11. 学习体会:从“背锅侠”到“规则制定者”

    以前学C语言,看到 int 是4字节、char 是1字节,总觉得这是“天经地义”的设定,背就完事了。面试被问到“为什么负数要存补码”,只能支支吾吾说“计算机就是这么规定的”。

    现在终于明白了:这些不是死记硬背的规则,而是一套自洽的底层逻辑。

    • 补码:不是故意为难你,而是为了让CPU只用加法器就能搞定所有整数运算(包括减法)。就像你学会了“借位减法”,突然发现原来可以全部用加法搞定——这感觉,爽!
    • 大小端:不是CPU设计师的恶趣味,而是历史遗留和性能优化的结果。知道这个,下次面试官问“怎么判断大小端”,你就能淡定地说:“看第一个字节是高位还是低位呗!”
    • 类型转换:本质就是“二进制位不变,解释方式变”。同一个 0xFFFFFFFF,%d 看是 -1,%u 看是 4294967295——不是内存变了,是你的“眼镜”换了。

    现在再看这些规则,不再是“背锅侠”式的被动接受,而是能理解背后的设计逻辑。就像玩游戏终于看懂了规则书,从“被规则玩”变成了“玩规则”。

    12. 下篇预告:浮点数的“魔法”与“陷阱”

    恭喜你!整数家族的存储规则已经通关:补码让负数有了统一的表示,大小端决定了字节排列顺序,类型转换的本质是“二进制位不变,解释方式变”。

    但内存的“魔法秀”还没结束——接下来,让我们走进 浮点数的奇幻世界!

    🎩 下篇看点:

  • IEEE 754 标准:为什么 0.1 + 0.2 ≠ 0.3?不是计算机算错了,而是浮点数的“精度游戏”。
  • S-E-M 三段式:float 和 double 那套“符号位-指数位-尾数位”到底怎么工作?比整数复杂,但也更有趣!
  • 内存视角:同一块内存,用 int 和 float 解读会得到完全不同的结果——这不是bug,这是特性!
  • 实战踩坑:为什么 float a = 0.1; if (a == 0.1) 可能永远不成立?如何正确比较浮点数?
  • 🚀 预告金句:

    “整数是规规矩矩的上班族,浮点数是充满创意的艺术家——但艺术家也有自己的‘怪癖’。”

    准备好了吗?下一篇,我们继续拆解内存底层,看看那些看似“魔法”的现象背后,都是严谨的数学规则。

    (悄悄说:学完浮点数,你就能理解为什么游戏里的物理引擎有时候会“抽风”,也能写出更稳健的科学计算代码了。)

    赞(0)
    未经允许不得转载:171主机测评 » 面试必问!!!:补码、大小端、类型转换
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址