2. 基本数据类型(上):整型

大家好,我是老李。今天咱们聊聊C语言里最基础、也最常用的东西——整型。说实话,我见过不少工作两三年的工程师,写起代码来整型用得挺溜,但一问到补码、溢出这些细节,就开始含糊了。嗯,这章咱们就把这些底层的门道彻底捋清楚。

2.1 整型家族:int、short、long、long long

C语言里的整型,说白了就是用来存整数的。但为什么要有这么多种?你想想看,不同的场景对数值范围和内存占用要求不一样。我早期做单片机开发时,RAM只有几百字节,用错类型可是要出大事的。

先看一张表,把它们的“家底”摸清楚:

类型 典型字节数 取值范围(有符号) 格式化占位符
short 2 -32768 ~ 32767 %hd
int 4 -2147483648 ~ 2147483647 %d
long 4 或 8 取决于平台 %ld
long long 8 -2^63 ~ 2^63-1 %lld
我的习惯:在嵌入式开发中,我几乎不用int,而是用里定义的int16_t、int32_t这些。为什么?因为int在不同平台上的字节数不一样,移植代码时容易踩坑。用明确宽度的类型,代码的可移植性会好很多。

举个例子,看看它们怎么用:

#include <stdio.h>
#include <stdint.h>

int main() {
    short s = 100;          // 2字节,省空间
    int i = 100000;         // 4字节,最常用
    long l = 100000L;       // 注意L后缀
    long long ll = 100000LL; // 注意LL后缀

    printf("short: %hd\n", s);
    printf("int: %d\n", i);
    printf("long: %ld\n", l);
    printf("long long: %lld\n", ll);

    return 0;
}

这里有个细节:long在32位系统上是4字节,在64位Linux上是8字节,但在64位Windows上还是4字节。是不是很坑?所以我个人建议,除非你明确知道目标平台,否则尽量用int32_t或int64_t。

2.2 无符号整型

无符号整型,就是只能存非负数的整型。用unsigned关键字修饰。为什么要用它?

我在项目中遇到过这样一个场景:需要用一个变量表示数据包的长度。长度不可能为负数吧?用无符号整型就非常合适。而且,无符号整型的正数范围比有符号大一倍。

类型 字节数 取值范围
unsigned short 2 0 ~ 65535
unsigned int 4 0 ~ 4294967295
unsigned long long 8 0 ~ 2^64-1
避坑指南:我曾经在循环里写过这样的代码:for (unsigned int i = 10; i >= 0; i--)。结果程序直接死循环了!为什么?因为无符号数永远不会小于0,i减到0后再减1就变成了4294967295。嗯,这个教训让我记住了:无符号数做循环递减时一定要小心。

2.3 整型数据的存储方式:原码、反码、补码

这部分是重点,也是很多人的盲区。计算机里到底怎么存负数?我刚开始学的时候也迷糊过一阵子。

先看一张图,把三种编码的关系理清楚:

整型数据存储方式 原码 最高位:符号位(0正1负) 其余位:数值的绝对值 例:+5 = 0000 0101 -5 = 1000 0101 符号位不变 其余位取反 反码 正数:与原码相同 负数:原码取反(符号位不变) 例:-5原码 1000 0101 -5反码 1111 1010 反码 + 1 补码 正数:与原码相同 负数:反码 + 1 例:-5补码 1111 1011 核心结论 1. 正数的原码、反码、补码完全相同 2. 负数的补码 = 原码取反(符号位不变)+ 1 3. 计算机中统一使用补码存储整数,原因:减法可以统一用加法实现 4. 补码的补码 = 原码(用于从补码反推原码)

为什么计算机要用补码?说白了,就是为了把减法变成加法。你想想看,CPU里做加法器比做减法器简单多了。用补码,5 - 3就变成了5 + (-3的补码),硬件实现起来非常方便。

看个具体例子,加深理解:

#include <stdio.h>

int main() {
    // 以8位有符号整型为例
    signed char a = 5;   // 补码:0000 0101
    signed char b = -5;  // 补码:1111 1011

    printf("a = %d\n", a);
    printf("b = %d\n", b);

    // 看看-5的补码在内存里到底是什么
    unsigned char *p = (unsigned char *)&b;
    printf("b的内存值(十六进制):0x%02x\n", *p);
    // 输出:0xfb,也就是1111 1011

    // 验证:5 + (-5) = 0
    signed char c = a + b;
    printf("a + b = %d\n", c);  // 输出0

    return 0;
}
重点记忆:在C语言中,所有整型数据都是以补码形式存储的。这一点在后续学习位运算、类型转换时非常重要。

2.4 整型溢出:一个容易被忽视的问题

说到整型存储,就不得不提溢出。我在做通信协议解析时,就遇到过因为整型溢出导致的bug,排查了好几天才找到原因。

#include <stdio.h>
#include <limits.h>

int main() {
    int max = INT_MAX;  // 2147483647
    printf("INT_MAX = %d\n", max);
    printf("INT_MAX + 1 = %d\n", max + 1);  // 溢出!变成负数

    unsigned int umax = UINT_MAX;  // 4294967295
    printf("UINT_MAX = %u\n", umax);
    printf("UINT_MAX + 1 = %u\n", umax + 1);  // 溢出!变成0

    return 0;
}
我曾经踩过的坑:有次做数据采集,用unsigned short存采样值,结果数据量大了之后数值突然跳变到0。查了半天才发现是溢出了。从那以后,我养成了一个习惯:只要涉及累加、乘法运算,一定先估算最大值会不会超过类型范围。

2.5 小结

这一章咱们把整型家族的基本成员都过了一遍。从int、short、long、long long的选用,到无符号整型的应用场景,再到原码、反码、补码的存储原理。这些东西看似基础,但确实是C语言编程的根基。

我个人觉得,理解补码是理解计算机运算的关键。你想想看,为什么-1 + 1 = 0在计算机里能完美实现?就是因为补码的设计。搞懂了这些,后面学位运算、类型转换就会轻松很多。

最后送大家一句话:基础不牢,地动山摇。整型这块看似简单,但真正用好的工程师并不多。希望你能把这些细节吃透,写出来的代码既高效又可靠。


公众号:蓝海资料掘金营,微信deep3321