先说个我自己的经历。前阵子帮一个刚转行的朋友看代码,他在一个协议解析程序里把缓冲区长度写成了strlen(buf),结果中文消息一多就崩。我一问,他连“一个汉字在 UTF-8 里占几个字节”都有点含糊,更别提“bit 和 byte 到底差多少倍”这种基础问题了。其实字节、字、bit、byte、KB 这几个词,几乎每个写代码、搞网络、甚至只是买电脑的人都见过,但真要说清楚它们之间的关系,不少人会卡壳。这篇文章就用大白话把这条线捋一遍,顺便把高低字节、字节对齐、容量换算这些高频出坑点也一起讲透。
1. 先搞懂 bit:计算机里那个“非 0 即 1”的最小开关
1.1 一个 bit 就是一个二选一的开关
bit 是 binary digit(二进制数字)的缩写,中文通常翻译成“比特”。它代表计算机里最小的信息单位,本质上就是一个二选一的开关:要么是 0,要么是 1,没有第三种状态。
在硬件层面,这个开关可以是一根导线上的高低电平,也可以是硬盘上一个微小磁区的南北极,还可以是闪存里一个浮栅晶体管的电荷状态。反正不管物理形式怎么变,逻辑上就是一个“是/否”的判断。
你可以把 bit 想成家里走廊的电灯开关:按下去是开(1),弹起来是关(0)。一个开关能表达两种状态,两个开关放在一起,就能表达四种状态(开开、开关、关开、关关),也就是:00、01、10、11。三个开关就是八种状态。对应到数学上,n 个 bit 能表示的不同状态数量是 2 的 n 次方。
有个很经典的问题:“一个 bit 能存多少种信息?”答案是 2 种。那 8 个 bit 呢?2 的 8 次方等于 256 种。这个数字后面还会不断遇到。
1.2 为什么网上老说“高低字节”里的“高低”
既然 bit 只有 0 和 1 两种状态,那多个 bit 排成一串的时候,就得给每个位置约定一个“权重”。比如二进制数1010,从右往左数,第 0 位(最右边)是权重 1 的位,第 1 位权重 2,第 2 位权重 4,第 3 位权重 8。于是1010就等于 8×1 + 4×0 + 2×1 + 1×0,也就是十进制的 10。 这个过程中,最左边的那一位叫最高位(Most Significant Bit,MSB),因为它的权重最大;最右边叫最低位(Least Significant Bit,LSB),权重最小。所谓“高低字节”,指的就是一个多字节数据里,权重高的那些字节叫高字节,权重低的叫低字节。
我在实际看协议文档时,经常遇到一种情况:文档里只写了某个字段是“高字节在前”还是“低字节在前”,如果你不懂最高位和最低位的概念,后面解析数据就是纯靠猜。所以别看 bit 简单,它是理解字节序、位运算、掩码操作的地基。
1.3 bit 与网络带宽的关系
bit 还有一个特容易踩的坑,就是它跟“字节”在缩写上长得太像。运营商宣传的“100M 宽带”,单位其实是 Mbps,也就是每秒 100 兆比特(Million bits per second)。但下载软件里显示的“12.5MB/s”,单位是兆字节每秒。
这里换算关系是 1 byte = 8 bits,所以 100 Mbps 的理论最大下载速度是 100 ÷ 8 = 12.5 MB/s。你可能实际跑到 10MB/s 左右就算不错了,因为还有协议开销、信号衰减这些因素。 我帮人排查“宽带速率不达标”问题时,十次有八次都是把 bit 和 byte 搞混,最后发现根本不是运营商的问题。
2. byte(字节)的诞生:从字符编码到 8 个 bit 的“标准答案”
2.1 为什么偏偏是 8 个 bit 组成一个字节
你可能会问,为什么不是一个 bit 一个 bit 地数,非要 8 个捆在一起?这就要从计算机发展的早期说起了。
早期计算机的字长并不统一,有的机器一个“字”是 6 位,有的是 7 位。直到 ASCII 码(美国信息交换标准代码)逐渐普及,人们发现,英文字母(大小写共 52 个)、数字(10 个)、标点符号和控制字符加在一起,用 7 位二进制刚好能覆盖(2 的 7 次方等于 128)。
但 7 位在硬件设计上不够规整,因为计算机内部的数据总线、寄存器通常按 8 的倍数来设计。IBM System/360 系列计算机在 1964 年推出时,确立了以 8 位为一个基本存储单位的方案,一个字节正好能放一个 ASCII 字符,多出来的第 8 位还可以用来做奇偶校验或者扩展字符集。后来 8 位字节就成了业界事实标准,“byte(字节) = 8 bits”这个换算关系就这么定下来了。
现在你去看各种编程语言的规范,基本上sizeof(char)都是 1,而这个 1 指的就是 1 个字节。注意,C 语言标准只规定char的大小是 1 个字节,并没有强制说一个字节一定是 8 位,但在我们日常接触的 x86、ARM 这些主流平台上,一个字节就是 8 位,不用纠结特殊情况。
2.2 字母 b 和 B 的区别:差 8 倍的大坑
大小写这个问题,我从入行到现在见过太多人栽跟头了。
- 小写
b:bit 的缩写,表示位。 - 大写
B:byte 的缩写,表示字节。
比如你买固态硬盘,标注是 500GB,这里的 GB 是字节。但你写代码时如果网络带宽是 10Mbps,这里的 Mb 是兆比特。一个不小心,把 8 倍给忽略了,整个系统的吞吐量估算就全错了。
我在做日志采集系统时,经常需要估算峰值流量。假设单机每秒产生 10 万条日志,每条日志平均 1KB,那一小时就是 10 万 × 1KB × 3600 秒,算下来约 360GB。但如果上游给的指标是“每秒 100Mbps”,你就得先除以 8 得到 12.5MB/s,再对照 10 万条 × 1KB = 100MB/s 的写入需求,发现带宽根本不够。这种量级错误,全靠对 b 和 B 敏感才能避开。
3. 字、字长与字节:CPU 一次性能“咽下”多少数据
3.1 字是 CPU 处理数据的“一口量”
接下来讲“字”。字(Word)这个概念比字节抽象,因为它没有固定的位数,而是跟具体 CPU 的体系结构有关。
你可以把 CPU 想象成一个吃饭的人,字节是他的饭勺大小,而字是他一口能吃下的量。对 64 位的 CPU 来说,它最顺手的处理长度是 64 位,也就是 8 个字节,所以这个 CPU 的“字”就是 8 字节,字长就是 64 位。对 32 位的老 CPU 来说,字是 4 字节,字长 32 位。再早一点的 16 位 CPU,比如 8086,一个字是 2 字节。
字长通常跟 CPU 内部寄存器宽度、数据总线宽度是一致的。它决定了 CPU 一次能直接处理的整数范围、能访问的内存地址空间上限,也直接影响性能。
所以当有人问你“字和字节啥关系”,标准回答是:字长除以 8 就是每个字的字节数。32 位系统字长 32 位 = 4 字节;64 位系统字长 64 位 = 8 字节。
不过注意,“字”这个词在不同上下文里有不同含义。在中文文档里,“字数统计”指的是人类语言里的字,跟这里的机器字完全不是一回事;在 C 语言里根本没有标准的“word”类型,很多嵌入式编译器会定义WORD为 16 位或 32 位,你得先看头文件。这就是为什么我建议在技术讨论里,尽量说“32 位系统”“64 位系统”,少说“字是多少位”,避免误会。
3.2 字节、字、位的关系速算表
为了方便对比,我整理了一张表:
| 单位 | 英文缩写 | 常见大小(位) | 常见大小(字节) | 典型用途 |
|---|---|---|---|---|
| 位 | bit, b | 1 位 | 1/8 字节 | 表示二进制状态、带宽计量 |
| 字节 | byte, B | 8 位 | 1 字节 | 存储基本单位,一个 ASCII 字符 |
| 字 | word | 16/32/64 位不等 | 2/4/8 字节 | CPU 一次处理的数据宽度 |
| 双字 | dword | 32 位 | 4 字节 | Windows API、寄存器组合 |
| 四字 | qword | 64 位 | 8 字节 | x86 下的 64 位整数、地址 |
这张表里你只要抓住两行核心:1 字节 = 8 位;1 字 = 字长 ÷ 8 字节。其他都是衍生品。
3.3 高低字节、大小端序:多字节数据怎么摆放
讲完字,必须聊高低字节和大小端,因为这两者几乎是孪生关系。
假设有一个 16 位整数0x1234。0x12是高字节(权重高),0x34是低字节。在内存里存放时,有两种主流做法:
- 大端序(Big-Endian):高字节存在低地址,也就是先存
0x12,再存0x34。读起来就像我们正常人写数字一样,从左往右读,很符合直觉。网络协议里规定用大端序,所以也叫网络字节序。 - 小端序(Little-Endian):低字节存在低地址,先存
0x34,再存0x12。x86、ARM 这类主流处理器默认是小端。
你可能觉得小端序反直觉,但它在硬件设计上有优势:从地址递增方向看,低字节先出来,CPU 做加法进位时更自然。我在解析一个 32 位传感器数据时,就是通过((uint32_t)buf[0] << 24) | ((uint32_t)buf[1] << 16) | ((uint32_t)buf[2] << 8) | buf[3]这样的移位操作,把 4 个大端字节组装成一个整数。如果顺序搞反,读出来的数据直接差个十万八千里。
所以凡是跨系统、跨网络传二进制数据,一定要明确字节序。你自己写死一个平台还好,一旦换到另一个平台,齐头发的 bug 就来了。
4. KB、MB、GB 的换算:1024 的来历,以及硬盘容量的“缩水”之谜
4.1 为什么 1KB 是 1024 字节,而不是 1000 字节
现在进入“KB”这个单位。这里的 K 是 kilo 的缩写,直译是“千”。但计算机内部是二进制世界,最接近 1000 的 2 的幂是 2 的 10 次方 = 1024。所以早期计算机科学家干脆约定:在存储领域,1KB = 1024 字节。
同理:
- 1MB = 1024KB
- 1GB = 1024MB
- 1TB = 1024GB
这个 1024 的进制贯穿了内存、文件大小和各种存储设备的计量。
为什么会选 1024?因为计算机里地址线、数据线的位数是 2 的幂次,内存容量天然就是 2 的幂次累计出来的。比如一条地址线能寻址 2 个字节,两条能寻址 4 个字节,十条能寻址 1024 个字节。用 1024 做换算,内存容量就能直接对应到地址线的根数,特别方便。
4.2 硬盘厂商按 1000 算,系统按 1024 算
接下来是我认为最迷惑人的地方:硬盘“缩水”。
你买一个标称 500GB 的机械硬盘或固态硬盘,插到电脑上,操作系统显示通常是 465GB 左右。这是厂商做手脚吗?不完全是,而是进制不统一导致的。
- 硬盘厂商按十进制计算:1KB = 1000 字节,所以 500GB = 500 × 1000 × 1000 × 1000 字节。
- 操作系统按二进制计算:1GB = 1024 × 1024 × 1024 字节。
所以操作系统显示容量 = 500,000,000,000 字节 ÷ (1024 × 1024 × 1024) ≈ 465GB。民用产品一直沿用这个规则,才造成你“买 500G 变 465G”的观感。注意,内存条和 U 盘又不一样,U 盘很多也是按十进制标称,但内存条因为本身容量就是 2 的幂次,比如 8GB 内存,其换算基本是按 1024 来衡量的。
我记得早年帮人挑电脑,有人看到硬盘 465GB 就觉得买到了假货。后来我教他一个公式:标称值 × 0.9313 约等于系统显示的“GiB”值,他这才放心。这个 0.9313 就是 1000 的 3 次方除以 1024 的 3 次方得出的系数。
4.3 KiB 与 KB:一个规范解决混乱
为了终结这种混乱,国际电工委员会(IEC)在 1998 年制定了新标准:
- KiB(Kibibyte,基比字节):1 KiB = 1024 字节,明确用于二进制倍数。
- KB(Kilobyte):1 KB = 1000 字节,明确用于十进制倍数。
- 同理还有 MiB、GiB、TiB。
理论上,操作系统应该用 GiB 来显示容量,但 Windows 一直显示“GB”,实际上是按 GiB 算的,所以永远跟厂商的 GB 对不上。macOS 系统现在显示的是真正的十进制 GB,所以苹果电脑里硬盘容量反而跟标称值更接近。
这个知识在买 VPS 时特别有用。很多云厂商套餐写“50GB SSD”,但你在 Linux 里执行df -h看到的是 50G 还是 46G,取决于文件系统怎么算。心里有 1024 和 1000 这两把尺子,就不容易被搞晕。
5. 代码里的字节世界:8 字节能干什么、字节对齐、字符串转数字、编码差异
5.1 8 字节能干什么
现在我们进入更“程序员日常”的部分。8 字节是很多语言里长整型(long long / long)、双精度浮点数(double)的标准大小,也是 64 位平台上指针的大小。
8 字节 = 64 位 = 2 的 64 次方,这个数大约是 1.844 亿亿,也就是 18446744073709551616。如果是用来表示无符号整数,范围是 0 到 18446744073709551615;如果是地址空间,理论上能寻址的字节数就是这个数,也就是约 1600 万 TB。当然实际硬件不会让你用满,但这也是 64 位比 32 位能装下更多内存的根本原因。
我在处理二进制文件格式时,经常用 8 字节来存时间戳(Unix 纳秒时间戳)、文件偏移量或者大文件的校验值。比如一个 4GB 以上的文件,用 32 位整数来偏移肯定是放不下的,必须用 64 位(8 字节)。所以如果你在某些解析代码里看到long long或者int64_t,它天然就是 8 字节,这是语言规范保证的(x86 和 ARM 上基本如此)。
5.2 结构体字节对齐:为什么 C 结构体“虚报”内存占用
说到字节,C/C++ 程序员绕不开字节对齐(alignment)。字节对齐的本质是 CPU 访问内存时,对特定类型数据的起始地址有偏好。比如一个 4 字节的 int,如果它放在能被 4 整除的地址上,CPU 一次就能读完整;如果跨在两个“边界”上,CPU 可能需要读两次再拼接,效率低下。
所以编译器会在结构体成员之间插入 padding(填充字节),让每个成员都“对齐”到自己该在的位置。
我写一个经典例子:
#include <stdio.h> struct Test { char c; // 1 字节 int i; // 4 字节 short s; // 2 字节 }; int main() { printf("sizeof(struct Test) = %zu\n", sizeof(struct Test)); return 0; }直觉上,1 + 4 + 2 = 7 字节。但在 x86 上,这个结构体的大小通常是 12 字节。为什么?
char c从偏移 0 开始,占 1 字节。int i要求 4 字节对齐,所以编译器在 c 后面填充 3 个字节,把 i 放到偏移 4 的位置。short s要求 2 字节对齐,放在偏移 8 处没问题。- 整个结构体的大小必须是最大成员对齐数的整数倍(这里是 4),所以结尾还要填充 2 字节,凑成 12。
如果成员排列顺序优化一下:
struct Test2 { char c; short s; int i; };这样是 1(c)+ 1(填充)+ 2(s)+ 4(i)= 8 字节,省了 4 字节。所以结构体成员顺序从大到小排,往往能省内存。我在做嵌入式开发时,结构体经常要发到网络上,内部有几百个成员,如果不管对齐规则,结构体体积可能膨胀三分之一,直接影响网络传输效率。有时还会遇到跨平台问题:不同编译器、不同架构对齐规则不完全一样,导致二进制无法互通。这时可以用#pragma pack(1)或__attribute__((packed))强制紧凑排列,但代价是运行效率变低,只能在明确需要的场景下用。
5.3 字符串转数字:本质是处理一串字节
再来说“字符串转数字”这个热搜词。很多人一听到“字符转数字”,第一反应是parseInt、atoi这种函数。但底层到底发生了什么?
在内存里,字符串"123"其实是一串字节:ASCII 码是0x31(字符'1')、0x32(字符'2')、0x33(字符'3')。要把它们转成一个整数 123,程序是这样做的:
int my_atoi(const char *str) { int result = 0; while (*str >= '0' && *str <= '9') { result = result * 10 + (*str - '0'); str++; } return result; }这里*str - '0'就是把单个字符的 ASCII 码减去0x30,得到 0 到 9 的数字。然后不断乘 10 累加。这是典型的“字符串转数字”实现原理,看起来简单,但涉及的一个重要思想就是:字符在计算机中就是字节,数字也是字节序列,操作它们本质上都是操作位和字节。
在处理 UTF-8 编码时,这个问题更值得注意。UTF-8 是一种变长的字节编码:英文字母和数字占 1 字节,很多欧洲文字占 2 字节,中文汉字通常占 3 字节,甚至部分生僻字和 emoji 占 4 字节。所以一个“字”在不同编码方案下,占的字节数完全不同。
比如汉字“中”:
- 在 UTF-8 里是 3 个字节:
E4 B8 AD - 在 GBK 里是 2 个字节:
D6 D0
所以“UTF 编码不一样字一样”的热搜词,说的就是这种现象:字符相同,但在不同编码规则下,底层字节序列完全不同。如果你把一个 UTF-8 字符串按 GBK 去解析,就会出现乱码。我之前写一个日志清洗程序时,就是没注意源文件是 GBK 编码,结果中文全部变成“锟斤拷”,查了半天才发现是编码声明的问题。
5.4 实操:用一个工具把字节看个明白
说了这么多,最好还是自己动手看一眼。Windows 上可以用 HxD,Linux 上用xxd或hexdump,macOS 上可以用xxd。比如写一个文本文件,里面只有三个字符“AB中”,然后在 Linux 终端执行:
echo -n "AB中" > test.txt xxd test.txt假设终端使用 UTF-8 编码,输出会是:
00000000: 41 42 e4 b8 ad AB...41是 'A',42是 'B',后面跟着的三个字节e4 b8 ad就是“中”的 UTF-8 编码。你一眼就能看出:这个文件一共 5 个字节,A 和 B 各占 1 字节,中文占 3 字节。
如果在 Windows 记事本里用 ANSI(GBK)保存同样的内容,再用xxd看,中字会变成 2 字节d6 d0,文件总大小就变成 4 字节。这就是为什么同一个字,在不同编码下文件大小不一样的直观证据。
6. 常见问题速查与避坑清单
6.1 一张表看清常见单位换算题
我把平时答疑时最常遇到的几个换算问题做成表格:
| 问题 | 答案 | 关键点 |
|---|---|---|
| 1 byte 等于多少 bit | 8 bit | 大写 B 是字节,小写 b 是位 |
| 1 KB 等于多少字节 | 1024(严格 1 KiB) | 十进制下 1 KB = 1000 |
| 1 字等于多少字节 | 取决于 CPU 字长 | 64 位系统 8 字节,32 位系统 4 字节 |
| 一个汉字在 UTF-8 占多少字节 | 通常 3 字节 | GBK 中占 2 字节 |
| 100Mbps 宽带理论下载速度 | 12.5MB/s | 比特转字节要除以 8 |
| 为什么 500G 硬盘系统显示 465G | 进制不同 | 厂商用 1000,系统用 1024 |
这几道题基本覆盖了日常碰到的 80% 的困惑。你只要抓住:bit 是最小单位,byte 是存储基本单位,word 看 CPU,KB 以上有十进制和二进制两套标准,就能应付绝大多数场景。
6.2 我踩过的几个字节相关的坑
第一个坑是sizeof和strlen混用。sizeof是编译期计算出的类型或变量占用的字节数,strlen是运行期数到'\0'为止的字符个数。对char buf[] = "hello"来说,sizeof(buf)是 6(包括结尾的'\0'),strlen(buf)是 5。在分配缓冲区和复制内存时,用错这两个函数,轻则浪费空间,重则缓冲区溢出。
第二个坑是网络字节序转换。我以前接手过一个跨平台通信模块,发送端是小端机器,接收端也是小端机器,中间经过一个网关后,某天突然所有整数都变成了乱序。排查半天,发现网关其实是按大端解析的,需要手动做字节序转换。从那以后,我只要写二进制协议,一定会在文档里明确标注“所有多字节字段一律采用网络字节序(大端)”。
第三个坑是位宽陷阱。在 32 位系统上,long是 4 字节;在 64 位 Linux 上,long是 8 字节;但在 64 位 Windows 上,long仍然是 4 字节。这种跨平台差异,只有实际编译运行才会暴露。所以我建议在涉及文件格式、协议时,尽量用int32_t、uint64_t这类固定宽度类型,而不是int、long这种“自然类型”。
6.3 个人建议:怎么把这些概念记牢
如果你还在学习阶段,我推荐一个很笨但很有效的方法:写一个小的二进制查看器,或者直接用xxd多看看各类文件的十六进制内容。看多了之后,你会形成一个直觉:一切文件,不管是文本、图片还是可执行程序,在底层都是一串字节;而这些字节之所以有“意义”,是因为我们人为规定了编码规则和解析方式。
把“字节是存储世界的原子,bit 是信息世界的原子”这句话记在脑子里,后面遇到带宽计算、结构体大小、编码转换、协议解析,你都会比旁人更快反应过来。很多看起来玄乎的问题,拆到底,无非就是几个字节在那排列组合而已。