1. 这句“10种人”到底在说什么“世界上有10种人一种是懂二进制的人一种是不懂二进制的人。”这句话我第一次看到的时候愣了两秒才反应过来——这里的“10”不是十而是二进制的“10”也就是十进制的2。所以整句话翻译过来就是世界上有2种人一种是懂二进制的一种是不懂的。这是一个典型的程序员冷笑话也是计算机科学领域流传最广的梗之一。这个笑话之所以能传播这么广是因为它用极简的方式完成了一次“认知筛选”懂的人会心一笑不懂的人一头雾水。它本质上是一个关于进制表示的入门级谜题但背后牵扯出来的东西远不止一个笑话那么简单。二进制是计算机最底层的语言从内存里的一个比特到屏幕上你看到的每一个像素从一条机器指令到整个互联网的数据传输全部建立在0和1之上。理解二进制不只是理解一个数学概念而是理解计算机“怎么想问题”的起点。这篇文章适合谁看如果你是刚接触编程的新手对进制转换、位运算这些概念还模模糊糊那这篇内容可以帮你把地基打牢。如果你已经写了几年代码但平时都是调库、写业务逻辑对底层的东西很少深究那也不妨跟着重新梳理一遍——很多性能优化、数据压缩、权限控制的技巧根子都在二进制上。如果你完全不写代码只是对这个笑话好奇那也完全没问题我会尽量用生活化的方式把这件事讲清楚。接下来我会从这句话本身出发把二进制的前世今生、核心原理、实操转换方法、常见坑点、以及它在实际开发中的典型应用场景一层一层拆开来讲。不堆术语不搞学术腔就是像一个写了多年代码的人在跟你聊这东西到底是什么为什么重要怎么用哪里容易出错。2. 二进制的前世今生与核心逻辑2.1 为什么计算机偏偏选了二进制要理解二进制先得回答一个问题世界上有十进制、八进制、十六进制为什么计算机偏偏选了二进制答案其实很朴素因为电子元件最容易表达两种状态。电路的通和断、电平的高和低、磁介质的正和反这些都是天然的“二选一”。你要让电路表达十种状态那就需要精确区分十个不同的电压区间这在工程上极其困难抗干扰能力也差。但如果只区分“有电”和“没电”两种状态容错率就高得多——电压稍微波动一点也不影响判断。这就像你家里的电灯开关要么开要么关你不会去设计一个“半开半关”的开关。计算机就是由几亿个这样的“开关”组成的每个开关就是一个比特bit0代表关1代表开。所有的数据——文字、图片、视频、游戏——最终都被编码成这些开关的组合。这里有一个常见的误解很多人以为二进制是“计算机的发明”其实不是。二进制作为一种数学表示法早在17世纪就被系统论述过了。莱布尼茨在1703年就发表了关于二进制算术的论文比第一台电子计算机早了二百多年。计算机只是“选择了”二进制而不是“发明了”二进制。2.2 位权理解二进制的钥匙十进制我们太熟悉了以至于很少去想它背后的规则。比如数字253我们脱口而出“二百五十三”但它的数学含义是2 × 10² 5 × 10¹ 3 × 10⁰ 200 50 3 253每一位的“权重”是10的幂次从右往左依次是10⁰、10¹、10²……这就是位权的概念。二进制完全一样的逻辑只是底数从10变成了2。比如二进制数10111 × 2³ 0 × 2² 1 × 2¹ 1 × 2⁰ 8 0 2 1 11所以二进制1011就是十进制的11。你只要记住每一位的权重是2的幂次——1、2、4、8、16、32、64、128……——就能快速心算。我个人的经验是背下2的0到10次方1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024非常有用。这不仅是进制转换的基础在估算内存大小、理解数据范围、做位运算的时候都会反复用到。比如你看到“一个字节是8位”马上就能反应出它能表示2⁸256种不同的值。2.3 二进制、八进制、十六进制的三角关系二进制写起来太长了。一个32位的整数用二进制写出来是32个0和1眼睛都要看花。所以实际开发中我们经常用八进制或十六进制来“缩写”二进制。核心规律很简单八进制一位等于二进制三位十六进制一位等于二进制四位。进制基数每位对应二进制位数常用前缀典型场景二进制21位0b位运算、底层调试八进制83位0o或0文件权限Linux十进制10—无日常计算十六进制164位0x内存地址、颜色值、编码为什么十六进制这么受欢迎因为4位二进制正好对应一个十六进制位一个字节8位刚好用两个十六进制字符表示。比如二进制11111111就是十六进制的FF也就是十进制的255。这种对应关系非常整齐读写都方便。你在调试代码的时候如果看到一个内存地址是0x7FFF5A2B或者一个颜色值是#FF5733背后都是十六进制在起作用。理解了这个三角关系你在看底层日志、分析二进制文件、配置系统参数的时候就不会发怵。3. 进制转换的实操方法与心算技巧3.1 十进制转二进制除2取余法这是最经典的转换方法步骤简单但需要耐心。以十进制156为例156 ÷ 2 78 ... 余0 78 ÷ 2 39 ... 余0 39 ÷ 2 19 ... 余1 19 ÷ 2 9 ... 余1 9 ÷ 2 4 ... 余1 4 ÷ 2 2 ... 余0 2 ÷ 2 1 ... 余0 1 ÷ 2 0 ... 余1然后把余数从下往上读10011100。验证一下1281684156正确。这个方法的好处是逻辑清晰适合手算。但如果你要转的数字很大除起来就很烦。我平时更常用的是减法法先找到比目标数小的最大2的幂次减去它标记对应位为1然后对余数重复这个过程。比如156小于156的最大2的幂是1282⁷156-12828小于28的最大2的幂是162⁴28-1612小于12的最大2的幂是82³12-84小于等于4的最大2的幂是42²4-40。所以第7、4、3、2位是1其余是0得到10011100。这个方法熟练之后比除法快得多。3.2 二进制转十进制按权展开反过来就简单了把每一位乘以对应的2的幂次然后求和。比如1011011×32 0×16 1×8 1×4 0×2 1×1 32841 45心算技巧从右往左遇到1就把对应的权重加起来。权重序列就是1, 2, 4, 8, 16, 32, 64, 128……你只需要记住这个序列然后做加法就行。3.3 二进制与十六进制的快速互转这是实际工作中最高频的转换因为调试时经常需要在两者之间切换。方法很简单从右往左每4位二进制一组直接查表。二进制十六进制二进制十六进制00000100080001110019001021010A001131011B010041100C010151101D011061110E011171111F比如二进制11010110从右往左分组1101 0110查表得D6。反过来十六进制3F拆成0011 1111去掉前导零就是111111。注意分组时一定要从右往左分如果最左边不够4位前面补零。比如二进制10110分成两组应该是0001 0110对应十六进制16而不是1011 0这样错误分组。3.4 代码里的进制表示与转换在实际写代码的时候不同语言对进制的支持略有差异但核心概念是通用的。以下用Python举例# 十进制转二进制、八进制、十六进制字符串 n 156 print(bin(n)) # 0b10011100 print(oct(n)) # 0o234 print(hex(n)) # 0x9c # 二进制、八进制、十六进制字符串转十进制 print(int(10011100, 2)) # 156 print(int(234, 8)) # 156 print(int(9c, 16)) # 156 # 直接写二进制、八进制、十六进制字面量 a 0b1010 # 10 b 0o17 # 15 c 0xFF # 255在JavaScript里进制转换的方式不太一样let n 156; console.log(n.toString(2)); // 10011100 console.log(n.toString(16)); // 9c console.log(parseInt(10011100, 2)); // 156 console.log(parseInt(9c, 16)); // 156C/C里可以直接用0b、0、0x前缀写二进制、八进制、十六进制字面量但0b是C14之后才标准化的老编译器可能不支持。实操心得在代码里写十六进制常量的时候我习惯用大写字母0xFF而不是0xff这样在密集的代码里更容易和数字0区分开。另外十六进制地址通常写成固定宽度比如0x00FF而不是0xFF方便对齐阅读。4. 位运算二进制的真正威力所在4.1 六种基本位运算如果你只是会做进制转换那顶多算是“懂二进制”的入门水平。二进制真正的威力在于位运算——直接对整数的二进制位进行操作。这在性能优化、权限控制、数据压缩、加密算法里无处不在。六种基本位运算与AND两位都为1时结果为1否则为0。常用于“掩码”操作提取或清除特定位。或OR|两位只要有一个为1结果就为1。常用于“置位”操作把某些位设为1。异或XOR^两位不同时为1相同时为0。常用于“翻转”操作和简单的加密。取反NOT~把每一位翻转0变11变0。注意在补码表示下~x等于-x-1。左移所有位向左移动右边补0。相当于乘以2的n次方。右移所有位向右移动。对于无符号数左边补0对于有符号数行为取决于语言和编译器。4.2 位运算的典型应用场景场景一权限控制这是位运算最经典的应用之一。假设一个系统有读、写、执行、删除四种权限用二进制位表示READ 0b0001 # 1 WRITE 0b0010 # 2 EXECUTE 0b0100 # 4 DELETE 0b1000 # 8 # 给用户赋予读和写权限 user_perm READ | WRITE # 0b0011 3 # 检查是否有写权限 if user_perm WRITE: print(有写权限) # 添加执行权限 user_perm | EXECUTE # 0b0111 7 # 移除写权限 user_perm ~WRITE # 0b0101 5这种方式的优势是一个整数就能表示一组开关状态存储紧凑判断快速。Linux的文件权限系统rwx本质上就是这个思路的八进制版本。场景二状态压缩在动态规划、图论等算法题里经常需要用一个整数表示一个集合的状态。比如有5个任务用一个5位二进制数表示哪些任务已完成# 第0、2、4个任务已完成 state 0b10101 # 21 # 检查第3个任务是否完成 if state (1 3): print(任务3已完成) # 标记第1个任务完成 state | (1 1) # 0b10111 23这种技巧在状态压缩DP里非常常见能把原本需要布尔数组表示的状态压缩成一个整数大幅减少内存占用和哈希开销。场景三快速乘除左移一位相当于乘以2右移一位相当于除以2整数除法。这个技巧在性能敏感的场景下很有用虽然现代编译器通常会自动优化乘除2的幂次为移位操作但在某些嵌入式场景或需要精确控制指令的时候手写移位仍然有意义。int a 5; int b a 3; // 5 * 8 40 int c a 1; // 5 / 2 2注意右移对于负数的行为在不同语言里可能不同。C/C里对负数右移是实现定义的行为Python里是算术右移保持符号位Java里用表示逻辑右移。跨语言移植代码的时候要特别小心这一点。4.3 位运算的常见坑点坑点一运算符优先级。位运算符的优先级低于比较运算符高于逻辑运算符。这意味着a b c实际上会被解析为a (b c)而不是(a b) c。这个坑我踩过不止一次调试半天才发现是优先级问题。建议位运算表达式一律加括号。坑点二有符号数的右移。前面提到了负数右移的行为需要特别注意。如果你在做哈希、加密、压缩这类需要精确控制每一位的操作建议统一使用无符号整数。坑点三移位超过位宽。在C/C里如果移位位数大于等于类型宽度行为是未定义的。比如对一个32位整数左移32位结果不可预测。在Python里则不会溢出因为Python的整数是任意精度的。写跨平台代码的时候要注意这个差异。坑点四~运算符与补码。~x在数学上等于-x-1这是因为计算机用补码表示负数。很多人第一次看到~5 -6的时候会觉得莫名其妙理解了补码就清楚了。5. 二进制在实际开发中的典型应用5.1 数据存储与内存布局计算机内存里的所有数据都是二进制。一个int通常占4个字节32位一个char占1个字节8位一个double占8个字节64位。理解这些基本类型的位宽是理解数据范围和精度的前提。比如一个无符号8位整数uint8的范围是0到255一个有符号8位整数int8的范围是-128到127。为什么是-128而不是-127因为补码表示里10000000表示-128而00000000表示0正数和负数各有128个但0占了一个位置所以负数比正数多一个。浮点数的二进制表示更复杂遵循IEEE 754标准。一个32位浮点数由1位符号位、8位指数位、23位尾数位组成。这解释了为什么0.10.2不等于0.3——因为0.1和0.2在二进制里都是无限循环小数存储时被截断了。5.2 网络协议与数据包解析网络传输的数据包本质上就是二进制流。当你用Wireshark抓包的时候看到的十六进制数据就是二进制的一种可读表示。理解二进制和十六进制是读懂协议格式的基础。比如一个IP包头第一个字节的高4位是版本号IPv4是4低4位是首部长度。如果你不懂位运算看到0x45这个字节就不知道它同时编码了两个信息。实际上0x45 0100 0101高4位01004版本低4位01015首部长度5×420字节。5.3 哈希算法与布隆过滤器哈希算法的核心就是位运算。比如Java的String.hashCode()实现int hash 0; for (int i 0; i length; i) { hash 31 * hash charAt(i); }这里的31 32 - 1 2⁵ - 1乘以31可以拆成(hash 5) - hash编译器会自动做这个优化。理解了这个你就明白为什么很多哈希函数选择31、33、127这类数字作为乘数——它们接近2的幂次可以用移位和减法代替乘法。布隆过滤器更是位运算的集大成者。它用一个位数组和多个哈希函数来判断一个元素“可能存在”或“一定不存在”。插入元素时用k个哈希函数计算出k个位置把这些位置为1查询时如果所有k个位置都是1说明元素可能存在只要有一个位置是0说明元素一定不存在。这种数据结构在缓存穿透防护、爬虫URL去重等场景下非常实用。5.4 图形图像与颜色编码你在CSS里写的颜色值#FF5733本质上就是三个字节的十六进制表示红色FF255绿色5787蓝色3351。每个颜色通道用8位表示范围0-255三个通道合起来就是24位真彩色。如果加上透明度Alpha通道就是32位色比如#FF573380最后的80表示Alpha值128也就是半透明。PNG图片、CSS的rgba()函数、Android的ARGB颜色底层都是这个逻辑。5.5 加密与编码Base64编码、URL编码、Unicode编码这些日常开发中经常用到的编码方式底层都是对二进制数据的重新组织。Base64把每3个字节24位重新分成4组6位然后映射到64个可打印字符上。理解了二进制你就能明白为什么Base64编码后的数据会比原始数据大约33%——因为3字节变成4字符膨胀了1/3。6. 常见问题与排查技巧实录6.1 进制转换中的高频错误问题现象可能原因排查方法解决方案转换结果位数不对分组方向错误检查是否从右往左分组从最低位开始每4位一组十六进制出现G以上字符混淆了十六进制和十进制确认基数十六进制只到F负数转换结果异常忽略了补码表示确认是否有符号先转无符号再处理符号大数转换精度丢失浮点数精度问题检查是否用了浮点类型用整数类型或BigInteger移位结果不符合预期运算符优先级问题加括号验证位运算表达式一律加括号6.2 位运算调试技巧调试位运算代码的时候光看十进制结果很难发现问题。我的习惯是同时打印二进制和十六进制def debug_bits(x): print(fdec{x}, hex{hex(x)}, bin{bin(x)}) a 0b1010 b 0b0110 debug_bits(a b) # dec2, hex0x2, bin0b10 debug_bits(a | b) # dec14, hex0xe, bin0b1110 debug_bits(a ^ b) # dec12, hex0xc, bin0b1100这样一眼就能看出哪些位被置位、哪些位被清除。如果位数很多可以用格式化输出对齐print(f{a:08b}) # 00001010 print(f{b:08b}) # 000001106.3 跨语言位运算差异速查特性C/CJavaPythonJavaScript整数位宽取决于类型固定32/64位任意精度53位安全整数右移负数实现定义算术右移算术右移算术右移逻辑右移无符号类型无移位超位宽未定义取模不溢出取模位运算结果类型整数整数整数32位有符号实操心得如果你写的代码需要跨语言移植位运算部分一定要单独测试。我遇到过一次JavaScript里用处理大整数导致精度丢失的问题后来改用BigInt才解决。不同语言对位运算的处理差异比想象中大。6.4 二进制相关的面试题套路“世界上有10种人”这个笑话本身就是一个面试题式的存在。类似的二进制面试题还有判断一个数是不是2的幂次n 0 (n (n-1)) 0统计一个数二进制中1的个数不断n n-1直到n为0找出数组中唯一出现一次的数其他数出现两次全部异或交换两个数不用临时变量a ^ b; b ^ a; a ^ b这些题目的共同点是如果你懂二进制一行代码就能解决如果你不懂可能要写一堆循环和判断。这也是为什么面试官喜欢考位运算——它能快速区分“真正理解计算机”和“只会调API”的候选人。7. 从笑话到实战的思维转变回到开头那个笑话。它之所以好笑是因为它用二进制开了一个认知玩笑——你以为“10”是十其实是二。这种“同一符号在不同语境下含义完全不同”的现象在计算机领域比比皆是。比如你看到0x10它是十六进制的16看到010在C语言里它是八进制的8看到10默认是十进制的10看到0b10它是二进制的2。同一个“10”四种进制四个不同的值。如果你不搞清楚上下文就会犯低级错误。我在实际工作中踩过的最大的一个坑是在解析一个二进制协议的时候把文档里的“偏移量10”理解成了十进制10结果实际应该是十六进制的0x10也就是16。整个数据包解析全错排查了一整天才发现是进制理解的问题。从那以后我看任何技术文档里的数字都会先确认它的进制。二进制不是什么高深莫测的东西它只是计算机世界的一种“母语”。你不需要时时刻刻用二进制思考但你需要知道它的存在理解它的规则在需要的时候能切换过去。就像你不需要每天说文言文但读古诗的时候得能看懂。这个笑话还有另一个版本“世界上有10种人一种是懂二进制的一种是不懂的还有一种是把这个笑话讲给第三个人听的。”如果你现在能把这句话讲给别人听并且解释清楚为什么是“10种”而不是“十种”那你已经站在懂的那一边了。