1. 把运算符当成决策细胞来理解1.1 运算符的本质从一次计算到一次判断很多人学编程时运算符是被一笔带过的基础章节。但我一直觉得运算符才是整个程序流程控制里最核心的细胞。为什么这么说因为不管你是写if还是switch本质都是在问一个问题这个条件到底成不成立而条件是否成立几乎全部依赖运算符产生的结果。运算符的本质就一句话把操作数变量、常量、表达式变换成一个新的值顺带可能产生副作用。比如a b把两个数变成它们的和这是一个纯计算通常不会改变a或b本身而a这类的自增运算符除了返回一个值还会改变a的内存内容这就是副作用。理解这一点非常关键。因为条件语句里的一句话if (score 60)其真正的运行过程是先让score 60这个比较表达式产生一个布尔结果true或false然后if再根据这个结果决定走哪条分支。你可以把运算符理解为制造判断依据的机器而条件语句是依据判断结果分配路径的调度员。没有前者后者就是无源之水。所以我的建议是别把运算符当计算题学要当决策工具学。比如你写一个用户登录逻辑用户名不为空并且密码匹配这个条件拆开看就是两个比较运算加一个逻辑与运算最终产出一个布尔值。一旦你用这个视角看代码你会发现所谓复杂的流程控制其实都是运算符制造条件分支语句分派路径的循环组合。1.2 五类核心运算符逐个拆解我先按实际使用频率把最核心的几类运算符过一遍。注意这里我不打算按教科书顺序平铺而是按它们如何在条件判断里起作用来讲。算术运算符 - * / %。这里重点说%取余。它除了算奇偶、算倍数更大的价值是为后续条件判断提供分类依据。比如轮询调度中index % 3可以把请求均匀分到三台服务器分页中offset % pageSize可以判断是否到了边界。%的结果天然适合放进switch或if去做分支。另外 Python 里**是幂运算符写2 ** 10就能得到 1024C/C/Java 里则没有这个语法需要用pow()函数这是跨语言时很容易踩的差异。赋值运算符 - * / %。是赋值不是数学上的等于。a 5等价于a a 5。赋值运算本身也有值这个值就是赋完后的结果所以a b 0可以把b和a连续赋值为 0。但正因为赋值表达式有值才会出现后面要讲的大坑把写成。比较运算符 ! 。它们的结果永远是布尔值。这里必须强调一个区别是比较值是否相等是赋值。在很多语言里if (x 5)不是报错而是把x赋成 5然后判断5 是否为真在 C/C 里这个条件恒为真于是你的程序会无条件地走进分支。这个错误极其隐蔽因为代码能编译、能运行但逻辑完全错了。后面我会专门讲排查方法。逻辑运算符 || !。与、||或、!非。它们操作的是布尔逻辑是条件语句里最常用的组合工具。重点在于短路求值a b中如果a为假b根本不会执行a || b中如果a为真b也不会执行。这不只是性能优化更是安全手段。比如写if (ptr ! NULL ptr-value 10)当ptr是空指针时后面的ptr-value压根不会求值从而避免了空指针崩溃。位运算符 | ^ ~ 。这是很多初学者直接跳过的东西但它才是真正体现计算机思维的运算符。按位与、|按位或、^按位异或、~按位取反、左移、右移。位运算最常见的用途是掩码和权限判断。比如一个整数的低 4 位分别代表读、写、执行、删除四种权限if ((permission 0x08) ! 0)就能判断删除权限是否开启。位运算在条件判断里极其高效而且能极大压缩状态空间。三目运算符condition ? expr1 : expr2。这是唯一一个三目运算符也是把条件判断压缩进表达式的利器。它有一个需要特别注意的特性结合方向是从右往左。也就是说a ? b : c ? d : e实际解析为a ? b : (c ? d : e)。这个特性常被拿来出题但在实际代码里我强烈建议三目运算符最多只嵌套一层超过一层直接写if-else否则代码可读性会急剧下降。2. 优先级程序按你的潜台词运行2.1 一张表看懂优先级与结合性运算符优先级是个老生常谈的话题但真正把它吃透的人不多。我见过太多代码事故最后排查到根因都是优先级理解错了。你可以不背整张表但必须记住一个分层原则先算算术再比大小再判逻辑最后赋值。更细一点核心排序如下从高到低括号()、下标[]、成员访问.最高一元运算符 -- ! ~算术运算符* / %高于 -移位 关系比较 相等比较 !按位与按位异或^按位或|逻辑与逻辑或||三目?:赋值 -等逗号,最低结合性也需要留意。算术、关系、位、逻辑这四类都是从左往右结合而赋值和三目是从右往左结合。所以a b c会先把c赋给b再把b的值赋给aa ? b : c ? d : e会先解析后面的三目前面已经提过。这张表还有个容易被忽略的点、^、|的优先级低于和!但高于和||。这意味着判断一个数的某个二进制位是否为 1 时if (x 8 8)的真实解析顺序是if (x (8 8))也就是x 1跟你想要的意思差了十万八千里。这种迷惑行为几乎在每个季度都会出现在我们团队的代码评审里。2.2 优先级引发的真实翻车现场我来讲几个真实遇到过的翻车案例比背表有用得多。第一个和打架。有个同事写权限判断意图是判断flag的第 3 位是否为 1。他写的是if (flag 0x04 0x04)。由于的优先级高于实际变成了flag (0x04 0x04)即flag 1。如果flag是偶数这个表达式恒为 0权限判断永远失败。我当时把代码走查结果贴给他的时候他盯着看了十分钟才反应过来。这类 bug 编译器不会报错逻辑上也看似合理只有走到线上才会暴露。第二个赋值与比较混淆。if (x computeValue())这种写法在 C/C 里根本不会报错而且常常碰巧能用因为只要赋值结果非零条件就为真。问题是当computeValue()返回 0 时条件为假分支不会执行。你本来的意图可能是if (x computeValue())但因为少写一个等号整个程序的业务流程全变了。更可怕的是这种代码在某些场景下测试通过、线上出错因为测试数据和线上数据分布不同。我现在养成了一个习惯任何涉及和模糊的代码一律加括号或直接改写结构杜绝歧义。第三个三目运算符嵌套。int result a b ? (a c ? a : c) : (b c ? b : c);这是我见过最精致的取最大值写法。虽然它逻辑正确但读代码的人至少需要十秒钟才能确认它没写错。这就是优先级特性和人脑解析能力的冲突。代码首先是给人读的其次才是给机器跑的。一个表达式如果让你需要反复确认优先级说明它已经不合格了。2.3 实用策略用括号表达意图既然优先级这么容易埋雷我的经验就八个字该用括号绝不省事。别把我知道优先级当成别人也知道优先级的理由。在一个团队协作的项目里你的代码会被各种经验水平的人阅读。用括号把意图写清楚不仅对别人负责更是对自己负责——因为你六周后再回来看自己代码时大概率也记不清当时的优先级心得了。我给出一个实操标准只要表达式里混用了三类以上运算符或者包含、、^、|中的任意两个就加括号。比如if ((flag 0x04) ! 0)、if ((a % 2) 0)、if ((x 0x0F) 0x0F)。这些括号在语义上不是必需的但在维护性上是必需的。另外用、||连接条件时我会给每个比较单元加括号if ((score 60) (score 90))。这样即使未来某天有人给语言改了优先级当然这不可能代码意图也不会被误解。这个习惯在很多大型开源项目里也是主流风格不是过分谨慎而是职业素养。3. if 语句条件分支里的设计思维3.1 从 if-else if-else 看分支筛选顺序if是所有编程语言最先教你的流程控制语句但很多人学好几年都写不出健壮的分支逻辑。问题往往不是语法而是分支顺序的设计。看这个经典例子根据成绩打印等级90 分以上优秀80 到 90 良好60 到 80 及格60 以下不及格。新手最容易写错的是边界值比如 90 分到底算哪个档如果你的代码是if (score 90) printf(优秀); else if (score 80) printf(良好); else if (score 60) printf(及格); else printf(不及格);这里的关键是分支条件的顺序必须是从严格到宽松或者反过来说后面的条件隐含了前面所有条件的取反。第二条score 80能执行说明第一条score 90为假所以实际区间是80 score 90。第 90 分被第一条截住了不会落到第二条里。这是一件好事但你必须清楚地知道顺序本身就在表达逻辑。如果顺序写反了if (score 60) printf(及格); else if (score 80) printf(良好); // 永远执行不到那 90 分也会被判成及格因为第一个条件就把它拦截了。这类 bug 之所以常见是因为写代码时脑子里想的是区间而if-else if实际表达的是逐级收缩的阶梯。写之前先在纸上把每个分支的真实区间算出来是我给所有新人的第一条建议。还有一种情况是条件重叠导致顺序依赖比如会员折扣普通用户 9 折VIP 用户 8 折超级会员 7 折。如果先判断普通用户就出问题了因为 VIP 也是普通用户的一种。正确做法是先判断最特殊的身份或者用更明确的包含关系。顺序不是随意排的它是业务优先级在代码里的投影。3.2 嵌套与早退控制条件复杂度嵌套if是另一类让人头疼的东西。三重、四重嵌套的代码逻辑上可能完全正确但阅读体验不亚于读一篇没有标点的文章。我处理嵌套问题的核心原则只有一个能早退就早退。早退early return/early exit是指在函数开头就把非法、异常、不适合继续处理的情况全部拦截掉用return、break、continue或throw直接结束当前单元让主流程保持扁平。看这个例子// 不推荐嵌套地狱 if (user ! NULL) { if (user-isActive) { if (orders ! NULL) { if (orders-count 0) { processOrder(orders); } else { log(no orders); } } else { log(orders null); } } else { log(inactive); } } else { log(user null); }同样的逻辑用早退改写if (user NULL) { log(user null); return; } if (!user-isActive) { log(inactive); return; } if (orders NULL) { log(orders null); return; } if (orders-count 0) { log(no orders); return; } processOrder(orders);后者在逻辑上一模一样但阅读负担下降了一个量级。每个条件独立成行错误处理集中在前面主流程露在外面。这就是我常说的把异常抛在入口把主路径留给读者。早退还有一层好处它天然消除了 else 的需求。你只需要写不满足就退出的分支剩下的就是主流程逻辑上不需要 else 来配对。这在业务代码里非常实用尤其是判断链特别多的时候。3.3 逻辑运算符优化条件德摩根定律与短路当你用、||组合条件时有两个工具必须熟练短路求值和德摩根定律。先看短路。if (list ! NULL list-size 0)中如果list是空指针前面的条件为假整个表达式的值已经确定于是后面的list-size根本不会执行。这就避免了空指针崩溃。反过来if (list NULL || list-size 0)中如果list确实是空指针条件立即为真后面的也不会执行。利用短路求值把可能出错的访问放在安全判断后面是 C/C 和 Java 里最基础、也最重要的技巧。但要注意这个特性在部分语言里并不存在。比如在早期的 VB 里And、Or不保证短路。好在现代主流语言 C/C/Java/JavaScript/Python 的、||都实现了短路。Python 里虽然没有但and、or同样短路。再看德摩根定律!(A B)等价于!A || !B!(A || B)等价于!A !B。这在实际编码中有个大用处当你觉得一个否定条件读起来别扭时把它改造成肯定条件。举个例子。你要判断不是用户已登录 且 是管理员直接写if (!(loggedIn isAdmin))时大脑要同时解析括号、取反、与运算。用德摩根定律改写为if (!loggedIn || !isAdmin)含义完全一致但可读性好得多。我在代码评审里无数次建议别人做这种等价变形——不是为了炫技而是让条件语句像一句自然语言一样直接被读懂。3.4 if 判断中的常见坑这一节全是真金白银每一种我都亲眼见过它们在生产环境里造成故障。坑一浮点数直接比较。if (sum 10.0)这种写法在大多数情况下是不可靠的。因为 IEEE 754 浮点数存在精度误差0.1 0.2并不精确等于0.3。如果业务确实需要判断浮点值标准做法是判断误差是否在可接受范围内即if (fabs(sum - 10.0) 1e-9)。这一点在涉及金额计算时尤其致命——所以正经的金融业务都要求用定点数或整数分来存储金额而不是用浮点。坑二类型隐式转换。C/C 里比较有符号和无符号整数时编译器会把有符号数转成无符号数。如果a是int值是-1b是unsigned int值是1那么if (a b)的结果是真因为-1被转换成了巨大的无符号数。JavaScript 的也存在大量隐式转换比如5 5为真。这就是为什么现在的代码规范往往推荐使用严格相等运算符从根源上避免这种看似相等的问题。坑三把赋值写进条件。前面已经提过if (x 5)。很多编译器现在会给出 warning但不少项目为了编译速度直接忽略了 warning等于放任 bug 存活。如果必须写赋值并检查我建议风格是if ((x getValue()) ! 0)用两层括号明显提示这里确实是赋值不是笔误。坑四比较链的反直觉。if (a b c)在数学上是合法的在 C/C 里也能编译但逻辑跟你想要的可能完全不同。它实际被解析成(a b) c即先比较a b得到一个布尔值0 或 1再拿这个 0/1 去和c比较大小。正确写法必须是if (a b b c)。这个坑在 Python 里反而不存在因为 Python 原生支持链式比较a b c。4. switch一张跳转表的自我修养4.1 switch 语法与 fall-through 的生死线switch是另一个流程控制的核心利器。它的基本形态大家都会写switch (grade) { case A: printf(优秀); break; case B: printf(良好); break; case C: printf(及格); break; default: printf(未知等级); break; }但它有个让无数初学者甚至资深开发翻车的机制fall-through即没有break时会继续执行下一个 case。很多人第一次写 switch 时忘记在 case 末尾加break结果程序把后续所有 case 的代码都执行了一遍。实际上 fall-through 本身不是设计缺陷它在某些场景下反而很有用。比如多个值共享同一套处理逻辑switch (key) { case y: case Y: printf(确认); break; case n: case N: printf(取消); break; }这种落空是有意设计的为了让代码表达两种情况一样处理。但在团队协作中这种写法必须加注释说明// fall through否则后面维护的人可能以为这是 bug帮你补一个break从而改变原有逻辑。具体到项目规范我一般要求没有 break 的 case 语句必须显式注释。无注释的 fall-through 一律视为错误。4.2 switch 在不同语言里的长相差异很多人在 C 语言里学完 switch 就以为天下通用其实不同语言之间差异很大踩坑点也完全不同。C/C/Javaswitch 的判断值必须是整数兼容类型——整型、字符型、枚举Java 还可以支持字符串较新版本。而且case必须是编译期常量不能用变量case x:会直接编译报错。C 还有一个陷阱在 switch 的某个 case 里定义局部变量时如果跳过初始化直接跳进另一个 case可能触发跳过变量初始化的编译错误。JavaScriptswitch 使用严格全等判断不会做类型转换。所以switch (5)中的 case5不会匹配因为一个是字符串一个是数字。这在需要精确匹配的场景下是好事但习惯了其他语言的隐式转换的话要注意别踩。Python根本没有 switch 关键字。官方推荐的替代方案有好几种最常用的是字典映射def handle_a(): return 处理A def handle_b(): return 处理B handlers { A: handle_a, B: handle_b, } result handlers.get(key, lambda: 未知 )()这种查表法在某些场景下甚至比 switch 更灵活——你可以动态注册、组合、传递函数。理解这一点对写出符合 Python 风格的代码很有帮助。其实这也让我意识到switch 的底层逻辑本质是从常量到处理动作的映射看清这一点无论语言怎么变化你的设计思路都不变。4.3 switch 适合什么场景和 if 的取舍switch 和 if 不是简单的替代关系各有各的最佳战场。我的经验判断标准有两条判断条件是精确匹配还是范围判定以及分支数量多不多。如果判断条件是变量等于某个常量、分支超过 2 到 3 个优先使用 switch。比如状态机的状态转换、协议解析里的命令字分派、菜单命令处理都是 switch 的主场。它比分写一长串if (x 1) ... else if (x 2) ...清晰得多。如果判断条件是范围——比如成绩区间、年龄分段、速度档位——用 if-else if 更合适。因为 switch 的 case 是离散常量表达不了60 到 80 之间这样的语义。如果强行用 switch你得把每一个可能的整数都列成 case完全是灾难。还有一个隐形因素编译器优化。C/C 编译器在 case 值分布较密集时会把 switch 编译成一张跳转表jump table执行时直接根据索引跳转时间复杂度是 O(1)而 if-else 链是逐个比较最坏情况是 O(n)。虽然现在 CPU 分支预测很强大性能差异不一定能感知但在热路径高频调用的代码上switch 的确定性优势还是值得认可的。如果 case 值分布特别稀疏比如 1、1000、100000编译器可能生成二分查找或 if 链这时 switch 就不一定比 if 有性能优势了。所以做取舍时先看语义再看数量最后才考虑优化。顺序反了写出来的代码就容易不伦不类。5. 综合实战把运算符和分支语句组合成可控的流程5.1 实战一成绩等级判定中的 if 与 switch 边界理论说了这么多我用三个实战案例把它们串起来。第一个案例是经典的成绩等级判定但我要故意暴露一个重新设计的过程。一个常见的写法是用if判断区间// C语言等级判定 if (score 90) { grade A; } else if (score 80) { grade B; } else if (score 70) { grade C; } else if (score 60) { grade D; } else { grade F; }这段代码是正确的但有人会问能不能用 switch 写答案是——如果只有字符 A/B/C/D/F 这几个等级需要处理可以把等级作为 switch 的输入对等级进行后续分派但从分数推出等级这一步不适合 switch因为你是范围判断不是精确匹配。这就是我前面说的边界if 负责把连续范围映射到离散类别switch 负责对离散类别做分派。这两个环节往往是串联的先用 if 把连续数据分类再用 switch 按类执行不同动作。这个案例还带出一个实用技巧把复杂判断封装成独立的评分函数不要在业务逻辑里到处写score 90这种裸条件。否则一旦等级规则变化你需要搜遍全项目逐个改。封装成getGrade(score)之后规则变化只改这一个函数这就是条件语句的高内聚技巧。5.2 实战二闰年判断——运算符优先级与逻辑组合验证第二个案例闰年判断。它有一个完备的判定规则能被 4 整除但不能被 100 整除或者能被 400 整除。很多人第一次写是逐个if拼// 不推荐的拼凑写法 if (year % 4 0) { if (year % 100 0) { if (year % 400 0) { isLeap 1; } else { isLeap 0; } } else { isLeap 1; } } else { isLeap 0; }这段代码逻辑上没错但嵌套深度让人看得头疼。用逻辑运算符直接组合规则int isLeap (year % 4 0 year % 100 ! 0) || (year % 400 0);一行搞定。注意这里的括号不是多余的的优先级高于||即使不写括号year % 4 0 year % 100 ! 0 || year % 400 0的解析结果和上面也一致。但我依然建议加括号因为读者需要一眼看出这是一个大条件分成两个子条件而不是现场查优先级。选这个案例是想展示运算符的组合能力能把嵌套三层的逻辑压成一行表达式且不损失可读性。但前提是你对优先级、短路、括号的使用都有把握。否则宁可多写几行if嵌套也不要写成一行天才代码——稳定性和可维护性永远优先于代码行数的减半。5.3 实战三用位掩码配合 switch 做状态机分派第三个案例是实际业务里很常用的组合位运算符生成状态switch 分派动作。假设一个任务管理系统任务状态用一个 8 位整数表示低 3 位分别是是否已提交是否已审核是否已发布#define FLAG_SUBMITTED 0x01 // 二进制 001 #define FLAG_REVIEWED 0x02 // 二进制 010 #define FLAG_PUBLISHED 0x04 // 二进制 100 // 读取三个状态位组合成一个 0~7 的枚举值 int state 0; if (task FLAG_SUBMITTED) state | 1; if (task FLAG_REVIEWED) state | 2; if (task FLAG_PUBLISHED) state | 4; switch (state) { case 0: // 草稿 break; case 1: // 已提交未审核 break; case 3: // 已提交并已审核未发布 break; case 7: // 全流程完成 break; default: // 其他非法或异常组合 break; }这个模式把多个开关状态组合统一成一个离散状态值再交给 switch 做 O(1) 分派。在实际项目里权限管理、功能开关、订单状态流转都能看到这种写法的变体。它的核心窍门是用位运算把多维信息压缩成一个标量用 switch 把标量的每个取值映射到清晰的业务动作。要注意的是这种模式要求你对位运算和状态组合有清晰的定义case 3表示1 | 2 同时成立不会写错但阅读时必须能心算出二进制组合。我会在代码注释里把每个关键组合的含义写清楚降低后续维护成本。5.4 代码走查用透明度原则排查流程控制 Bug最后聊一个方法论。面对流程控制相关的疑难 bug我的排查链路基本是固定的分享出来供参考。第一步确认表达式求值顺序。把可疑的条件表达式拆开逐个确认每个子表达式的值和类型。比如if (a b c)先查优先级表确认实际求值顺序是a (b c)还是(a b) c。凡是看着对但运行不对的条件九成是优先级或类型隐式转换问题。这时加括号重写往往 bug 就消失了。第二步确认短路是否被触发。如果条件里有或||要检查前一个条件是否意外地让后一个条件永远不执行。一个典型例子判断部门某个字段时if (dept ! null dept.name 研发)本来没问题但如果dept在之前的代码里被赋过一个非空但错误的默认值短路就不会触发后面的判断照样执行逻辑就悄悄变了。第三步确认分支是否被顺序截胡。把if-else if的每个分支真实区间列出来检查有没有区间被前面的更宽泛条件提前拦截。这是查看起来多余但实际上影响流程的最快方法。很多偶尔才出现的错误其实就是边界值落到了错误的区间里。第四步做最小化复现。当我看不出问题时会把可疑代码抽离成一个独立小函数用固定的几个测试输入跑一遍。比如把score分别设成 89、90、91观察输出变化。这一步能快速验证到底是我对优先级的理解错了还是业务规则本身就含糊。最后一个心得代码透明度。我始终追求的是——一段代码拿给任何有经验的工程师看不需要跑起来就能读懂它想做什么。运算符和条件语句是程序流程里最基础的两块砖但恰恰是这两块砖最能决定一个项目的代码质量是能跑还是能维护。所以值得花点时间把它们真正吃透。
企业数字化 ERP 产品动态
相关推荐
豆瓣图书知识图谱实战:Neo4j图数据库推荐系统搭建 简介:本资源是一套面向高校计算机及相关专业(人工智能、自动化、物联网等)学生的毕业设计级实践项目,聚焦豆瓣图书推荐系统与知识图谱构建,深度融合Neo4j图数据库应用开发。项目完整覆盖数据采集、清洗、图模型设计、实… · 2026/9/25 7:52:43
Oracle 19c Windows静默安装全链路指南:从解压到远程可连 简介:本资源为Oracle Database 19c官方Windows x64平台安装包(WINDOWS.X64-193000-gsm.zip),面向数据库管理员、企业级应用开发者及Oracle认证学习者,解决本地化部署高可用、云就绪型关系数据库的核心需求,… · 2026/9/25 7:52:43
死锁排查与预防实战:从CPU 100%到多线程实时采集系统的稳定之道 干实时采集系统这行的,大概都经历过这样的至暗时刻:界面上数据突然不刷新了,进程管理器里 CPU 稳稳地顶在 100%,点哪里都没反应,最后只能粗暴地杀掉进程重启。如果运气不好,连“保存现场”的机会都没有&… · 2026/9/25 7:52:43
AI Agent技能库搭建实战:让大模型从“能聊”到“能干” 如果你最近也在折腾AI Agent,大概会产生一种很微妙的感觉:大模型什么都能聊,但真让它干活的时候,总像隔着一层纱。它能告诉你“我可以帮你写脚本”,可你真让它去操作文件、调用接口、按固定流程跑一轮数据分析时&#… · 2026/9/25 8:22:18
HTML语义化与现代CSS/JS精简实践指南 1. 为什么“简洁的网页代码”不是一句空话,而是现代前端开发的生存底线你有没有遇到过这样的场景:接手一个同事留下的HTML文件,打开编辑器一看,<div>嵌套了七层,class名写着wrapper-inner-container-subsection-… · 2026/9/25 8:22:18
PowerInfer 中的 GBNF 语法完全指南:用形式文法约束 LLM 输出(从 JSON 到任意格式文本) 人工智能大模型推理引擎本地部署 【免费下载链接】PowerInfer High-speed Large Language Model Serving for Local Deployment 项目地址: https://gitcode.com/gh_mirrors/po/PowerInfer 点击查看 免费下载 本篇技术指南以 smallthinker/grammars/README.md 为核心… · 2026/9/25 8:22:05
创维E900V22D刷机全攻略:S905L3SB芯片兼容性解析与救砖实战 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 1:00:31
MQTT协议原理与Broker服务器搭建实战:从Mosquitto到EMQX /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 1:00:37