news 2026/9/27 1:10:54

Java继承成员变量访问规则:就近原则、this/super与变量隐藏详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java继承成员变量访问规则:就近原则、this/super与变量隐藏详解

写Java写了几年之后回头看,很多新手阶段一头雾水的知识点其实特别简单,只是当时没人把话说透。比如刚接触继承时,一碰到“子类父类有同名成员变量”就懵——代码明明是这个值,输出怎么是那个值?再去查资料,“就近原则”“this关键字”“super关键字”三个概念一起砸过来,更晕了。这篇聊的就是这件事:Java继承里成员变量的访问规则,把就近原则的判定逻辑、this和super的真实区别讲明白,配上可以直接跑起来的代码和一套排查思路。不管是准备面试还是日常写代码遇坑,这篇应该都能帮上忙。

1. 从一道易错题说起:为什么输出不是你想的那个值

先看一个最常见的场景。父类和子类里定义了一个同名变量,在子类方法里直接访问这个名字时,Java到底会拿到谁的值?很多人的第一反应是“既然是子类的方法,那肯定拿子类的”,但这不完全对,更准确的说法是:访问发生在哪个类的作用域里,就从那个类开始往上找。

这里要先建立一个概念:Java的成员变量并不像方法那样天然支持“覆盖(Override)”。方法可以重写,是因为调用时走的是动态绑定,看的是对象实际类型。但变量不一样,变量访问是静态行为,编译期就决定了。也就是说,哪怕你用子类对象去调用一个父类里定义的方法,这个方法里访问的成员变量仍然是父类定义的那一份,不会因为你传进来的是子类对象就自动变成子类的值。这一点极其关键,很多隐蔽bug都埋在这里。

打个比方,父类和子类各开了一家同名门店“总店”,父类方法里写的是“去总店调货”,这个“总店”在父类代码里已经写死指向父类自己的门店,无论谁来调用这个方法,它都按自己的地址去调货。子类虽然也开了同名的“总店”,但父类的流程根本不会经过子类那家。

所以在理解成员变量访问时,请务必把视角从“对象是谁”切换到“代码写在哪里”。这是绕开所有混淆的第一步。

2. 就近原则:变量名冲突时的默认裁决规则

2.1 就近原则到底是怎么走的

就近原则是Java处理同名变量访问的基础规则,准确描述是:在方法内部访问一个变量时,Java会按照“局部变量 > 当前类成员变量 > 父类成员变量 > 更上层父类成员变量”的顺序查找,一旦找到就停止。

看这个例子:

class Parent { int num = 100; } class Child extends Parent { int num = 200; public void show(int num) { System.out.println(num); // 输出参数num System.out.println(this.num); // 输出子类成员num System.out.println(super.num); // 输出父类成员num } } public class Demo { public static void main(String[] args) { Child c = new Child(); c.show(300); } }

结果依次是300、200、100。第一个输出300,就是因为参数num是局部变量,优先级最高,直接命中,查找过程就结束了。第二个是this.num,跳过局部变量,从当前类(子类)找成员变量,找到200。第三个super.num,明确要求从父类开始找,拿到100。

很多初学者以为就近原则只指“当前局部有没有,没有再往上找”,其实这里还隐藏了一个语义:就近的边界是“类成员”这一层。局部变量只存在于方法内部,出了方法就失效,所以它算“最近”是合理的。而成员变量分属不同的类层级,“当前类”自然比“父类”近。

这个规则最实用的一点在于:它意味着你不需要记住复杂的继承结构就能推导结果,只需要问三个问题——局部变量里有吗?当前类里有吗?父类链上有吗?

2.2 一个容易忽略的细节:变量“隐藏”而非“覆盖”

继续深入一下,子类和父类定义同名成员变量这件事,专业叫法是变量隐藏(Variable Hiding)。它和方法重写完全不是一回事。方法被重写之后,调用时永远是实际对象类型的方法获胜;但变量被“隐藏”之后,是通过“引用类型”来决定访问哪个版本。

class Parent { String name = "Parent"; } class Child extends Parent { String name = "Child"; } public class AmbiguousDemo { public static void main(String[] args) { Parent p = new Child(); System.out.println(p.name); // 输出 "Parent" Child c = (Child) p; System.out.println(c.name); // 输出 "Child" } }

p的静态类型是Parent,所以p.name拿到的是Parent的成员变量,即使p实际指向的是Child对象。这跟方法多态的表现恰恰相反:

class Parent { public void hello() { System.out.println("Hello from Parent"); } } class Child extends Parent { @Override public void hello() { System.out.println("Hello from Child"); } } Parent p = new Child(); p.hello(); // 输出 "Hello from Child",走动态绑定

同样是“父类引用指向子类对象”,变量看引用类型,方法看实际类型。这是两套完全不同的访问机制,建议在理解的时候彻底分开,别混在一起记。

如果这个例子看明白了,你就能理解为什么很多Java编码规范建议“不要定义与父类同名的成员变量”——不是语法不允许,而是这种行为会产生极大的可读性陷阱。你写的时候觉得不过是重名,别人维护的时候可不知道你脑子里哪一行是“当前类”哪一行是“父类”。

3. this和super:显式指定归属的两种手段

3.1 this到底指什么

this是Java里一个特殊引用,指向“当前正在执行方法的对象”。在实例方法里它等于“调用这个方法的对象”,在构造器里它表示“正在初始化的对象”。用这个机制,可以显式地告诉编译器“我要访问本类的成员变量”。

最常见的用法就是解决局部变量和成员变量同名的问题:

class Student { private String name; public void setName(String name) { // 这里左边name是成员变量,右边name是参数 this.name = name; } }

如果去掉this,代码就变成了“把参数name赋值给参数name”,成员变量永远得不到更新,这就是典型的“阴影变量赋值”bug。因为就近原则会优先命中参数,没有this的话,没有任何手段能引用到成员变量。所以说,this在这里不只是“可选的修饰”,而是唯一能打破就近原则的入口。

this还有一个容易忽视的特性:它可以调用本类的其他构造器。

class Student { private String name; private int age; public Student() { this("unknown", 0); // 调用Student(String, int) } public Student(String name) { this(name, 0); } public Student(String name, int age) { this.name = name; this.age = age; } }

不过这是构造器重载的范畴,和成员变量访问关联不大,但要注意:this()语句必须写在构造器第一行,否则编译不过。这一点和super()的调用约束一模一样,后面会提到。

3.2 super的完整面貌

super和this相对,它指向的是“父类对象的部分”,也可以理解为“父类的空间”。注意,它不是像parentObj那样一个独立的引用,而是一个编译层面的指示符,告诉编译器“从这个继承链的上一层开始查找”。

super最基础的用法是访问被隐藏的父类成员变量:

class Employee { double salary = 8000; } class Manager extends Employee { double salary = 15000; public void showSalaryInfo() { System.out.println("Manager salary: " + this.salary); // 15000 System.out.println("Base salary: " + super.salary); // 8000 } }

这里super只跳过了一层子类成员,直接定位到父类的salary。如果父类的父类也有同名变量,super不会无限往上找,它只负责“上一层”。想访问上上层,需要在父类里再提供一个方法,间接返回父类的父类变量,因为子类不能用super.super。

super还有一个高频用法:在子类构造器里显式调用父类构造器。

class Animal { String type; public Animal(String type) { this.type = type; } } class Dog extends Animal { String breed; public Dog(String type, String breed) { super(type); // 必须放在第一行 this.breed = breed; } }

这里super(type)的作用是让父类完成它自己的初始化工作。如果父类没有无参构造器,而子类构造器又没有调用super(有参版本),编译器会直接报错,因为子类构造器隐式调用了super()(父类无参构造器),但父类根本不存在无参构造器。这是一个高频编译错误,稍后在第6节展开讲。

3.3 this与super的对比速查表

这张表方便日常查阅,建议存下来:

维度thissuper
访问当前类隐藏的同名成员变量支持:this.num不支持,this只能本类
访问父类同名成员变量不能直接跨层支持:super.num
调用本类其他构造器this(参数)不支持
调用父类构造器不支持super(参数)
必须放在构造器第一行是是
可用于static上下文不能不能
本质作用显式指向当前对象显式指向父类部分

最后一行要特别注意:static方法里不能出现this和super。因为static方法不依赖具体对象,而this和super都是“跟对象走”的引用,在static上下文使用会直接编译报错。很多初学者会在这上面栽跟头,记一下可以少踩一个坑。

4. 完整实操:从零构建一个继承案例

前几节讲的是规则,这一节我们用代码把规则串起来,做一个完整的可运行案例。场景设定:一个简单的工资管理模块,有一个基类Employee和两个子类Developer、Manager,用来演示成员变量访问在不同继承层级上的表现。

class Employee { String role = "员工"; double baseSalary = 8000; public double calculateSalary() { return baseSalary; } public void showDetail() { System.out.println("岗位:" + role); System.out.println("基础工资:" + baseSalary); System.out.println("实发工资:" + calculateSalary()); } } class Developer extends Employee { String role = "开发工程师"; double projectBonus = 5000; @Override public double calculateSalary() { // 注意:super.baseSalary 显式从父类取;baseSalary 就近取父类的(子类没有定义) return super.baseSalary + projectBonus; } @Override public void showDetail() { System.out.println("岗位:" + this.role); // 子类role System.out.println("基础工资:" + super.baseSalary); // 父类baseSalary System.out.println("实发工资:" + calculateSalary()); } } class Manager extends Developer { String role = "项目经理"; double managementAllowance = 3000; @Override public double calculateSalary() { // 调用父类Developer的calculateSalary,同时叠加管理津贴 return super.calculateSalary() + managementAllowance; } @Override public void showDetail() { System.out.println("岗位:" + this.role); // super.role 拿的是Developer的role System.out.println("父类岗位:" + super.role); // 注意:这里baseSalary无论直接访问还是super访问,拿的都是Employee的baseSalary System.out.println("基础工资:" + baseSalary); System.out.println("实发工资:" + calculateSalary()); } } public class SalaryDemo { public static void main(String[] args) { Employee emp = new Employee(); emp.showDetail(); System.out.println("---"); Developer dev = new Developer(); dev.showDetail(); System.out.println("---"); Manager mgr = new Manager(); mgr.showDetail(); } }

运行结果:

岗位:员工 基础工资:8000.0 实发工资:8000.0 岗位:开发工程师 基础工资:8000.0 实发工资:13000.0 岗位:项目经理 父类岗位:开发工程师 基础工资:8000.0 实发工资:16000.0

这里有几个值得细品的地方:

第一,Developer里没有重新定义baseSalary,所以访问baseSalary时,就近原则从当前类找不到,就往父类找,找到了Employee的baseSalary。这里的“就近”体现在“类继承链的当前类开始往上”。

第二,Manager里调用super.role拿到的是“开发工程师”,即Developer的role。这说明super可以跨一层,但跨层的顺序是逐级的,不可以一步跨到Employee那层。如果你确实想在Manager里拿到Employee的role,只能通过Developer再提供一个方法间接帮你取,或者就用baseSalary这种没有被中间层重定义的变量,让就近原则自然帮你落到最顶层。

第三,在Manager.showDetail()中,baseSalary没有写this也没有写super,但输出仍然是8000。因为Manager和Developer都没有定义baseSalary,就近原则一路往上找到Employee。这和“从哪个类开始找”有关,和“到底是this还是super”无关。

实操时可以在每个类里分别加上不同变量组合,观察输出的变化,这是理解继承变量访问最直接的方法,比死记硬背规则高效得多。

5. 三个高频陷阱与底层原理剖析

5.1 陷阱一:构造器里同名变量赋值失败

这是新手最容易踩的坑。看这段代码,猜猜输出是什么:

class Base { String message = "Base Message"; public Base() { System.out.println("Base Constructor: " + this.getMessage()); } public String getMessage() { return message; } } class Derived extends Base { String message = "Derived Message"; public Derived() { super(); System.out.println("Derived Constructor: " + this.getMessage()); } @Override public String getMessage() { return message; } }

运行后输出:

Base Constructor: null Derived Constructor: Derived Message

这个例子非常经典。Base的构造器调用了getMessage(),而getMessage()是动态绑定到Derived的版本,这时Derived的成员变量message还没有被初始化,所以拿到的是默认值null。

这个问题的本质涉及Java对象初始化的顺序:先分配内存,然后给所有成员变量赋默认值(null、0、false),再执行父类构造器,最后才执行当前类构造器。Derived的message在父类构造器执行期间只是默认值null,直到Derived构造器里的代码真正跑起来,它才被赋值成"Derived Message"。

这个现象告诉我们的一个实战准则是:不要在构造器里调用可能被重写的方法。Effective Java第二条就明确说过这个,但很多人是在踩坑后才真正理解的。用一句话解释:构造器里调用的方法如果是多态的,那么这个方法的执行时机可能早于对象完全初始化,你访问的成员变量可能还是默认值。

5.2 陷阱二:在静态上下文中使用this/super

class Demo { int value = 10; public static void print() { // 这两个都会编译报错 // System.out.println(this.value); // System.out.println(super.toString()); } }

编译器提示“无法从静态上下文中引用非静态变量或方法”。看完前面的解释,这一点就不奇怪了:static方法属于类的,不依托于任何具体对象,而this和super的本质都是对象引用。既然没有对象,自然也就没有“当前对象”或“父类部分”。

平时写工具类时要特别注意,如果需要在static方法里访问成员变量,要么把方法改成实例方法,要么把方法参数带上对象引用。我见过不少代码在重构时把方法误改成static,然后发现一堆编译错误,就是因为成员变量访问依赖实例。

5.3 陷阱三:变量隐藏导致“更新失效”

先看代码:

class Counter { int count = 0; public void increment() { count++; } public void printCount() { System.out.println(count); } } class SubCounter extends Counter { int count = 100; // 隐藏父类的count @Override public void increment() { // 这里count++ 操作的是子类的count count++; // 但父类的count没有动! } }

假设执行:

SubCounter sc = new SubCounter(); sc.increment(); sc.printCount();

printCount()是父类方法,里面的count是父类的count,输出0;而子类increment()里递增的是子类的count,父类count从未变化。这里的“看似整数+1”实际上操作的是两个完全不同的字段。

要修复,可以让子类increment里显式用super.count++。但更推荐的方案是:在父类把count声明为private,并提供对应的getter/setter,子类完全不直接触碰这个字段。这样既避免隐藏带来的混乱,也遵循了封装原则。记住一个设计判断:如果子类和父类出现同名变量,十有八九是设计失误,需要重新审视继承关系。

5.4 一个顺带一提的问题:为什么Java不允许super.super

不少学习者会好奇:既然有super,那为什么没有super.super?原因是Java设计者认为多级跳转访问会让继承体系变得复杂且脆弱。如果允许super.super,代码就和具体继承层级强耦合,父类结构一调整,子类代码就崩了。所以Java只提供了一层super访问的能力,跨多层的需求应该通过设计来规避,而不是在语法上给你开个“天窗”。

理解了这一点,也就能想明白为什么那些“继承深度超过三层”的系统往往很难维护——因为变量的可见性和访问链太深,心智负担呈指数级上升。

6. 面试高频八股:连续追问与标准思路

成员变量访问这块在面试里几乎必考,而且面试官很喜欢连续追问。整理几个常见连环套,把思路梳理清楚。

6.1 第一次追问:输出顺序是什么

题目通常是:

class A { int x = 1; } class B extends A { int x = 2; void print() { System.out.println(x); // 2 System.out.println(this.x); // 2 System.out.println(super.x); // 1 } }

标准回答思路:先用就近原则说明默认访问命中子类x;再展开this也指向当前对象,结果同接访问;super强制从父类取,得到父类x。光背答案有用,但如果能补一句“这里本质是变量隐藏不是覆盖”,面试官通常会更满意,因为这表明你不只是记住了结论,还理解了机制。

6.2 第二次追问:换一种调用方式

面试官把代码改成:

A a = new B(); System.out.println(a.x);

此时输出是1,不是2。这是最经典的“变量看静态类型,方法看实际类型”的陷阱。a的编译期类型是A,所以a.x就是A.x,哪怕实际对象是B。如果面试官再追问((B) a).x,输出就是2,因为强转后类型变成了B。

这一步能筛掉很多只背结论的人。建议在面试时直接画一下内存里对象的结构:一个B对象内存中其实包含了两份x,一份对应A,一份对应B(实际内存布局可能经过优化,但逻辑上是这个意思),用哪个x取决于你通过哪个“视角”去看。

6.3 第三次追问:构造器里的打印

再接招:

class A { A() { print(); } void print() { System.out.println("A.print"); } } class B extends A { int y = 10; B() { print(); } @Override void print() { System.out.println("B.print, y = " + y); } }

这个来自经典面试题Android中很火的那道“构造函数里多态方法”变种。输出是什么?先回忆对象初始化顺序。B的构造器前,会先隐式调用A的构造器;A的构造器里print()被动态绑定到B的print(),打印B.print, y = 0(y未初始化);之后B构造器执行,打印B.print, y = 10。

同样一句话总结:构造器执行期间,多态方法可以先于子类初始化被调用,访问到的变量值是默认值。这个题既考继承初始化顺序,又考动态绑定,还考成员变量默认值,综合性极强,面试遇到的概率不低。

6.4 面试总结要说的“一句话版本”

不管面试官怎么追问,最终的总结建议都围绕这样一个核心逻辑链条:

成员变量访问遵循就近原则:方法内局部 > 本类成员 > 父类链逐级。变量隐藏是静态编译期行为,与对象实际类型无关;方法重写是动态绑定,与对象实际类型相关。this用来显式指向当前对象,super用来显式指向父类区域,两者既可在变量访问中打破就近规则,也可在构造器中完成初始化调用。

能把这一句话说完整,并且用6.1、6.2、6.3的例子证明你确实理解,面试这一关就稳了。

7. 日常开发中的实战建议

回头看这个问题,确实不只是面试八股,日常代码里也有不少落点。总结几条我自己写代码时的经验:

优先用方法和构造器传递数据,减少跨层变量访问。子类需要父类的数据时,通过父类的getter方法获取,而不是直接碰父类变量。这样既绕开了变量隐藏的坑,也保证了封装性。

如果确实需要定义同名变量,请用@Deprecated或显式注释说明原因。比如历史遗留代码迁移时,临时保留旧字段和新字段双轨运行。但这类代码一定要有清晰注释,否则几个月后自己都看不懂为什么会有同名变量。

构造器里绝不调用可重写方法。这是一个铁律。如果必须在初始化时调用某个方法,请把它设为private、static或final,让编译器阻止重写,从机制上避免前言中说的那种“变量值为null”的诡异问题。

善用IDE的提示。IDEA里如果子类和父类成员变量重名,通常会给出灰色提示或警告。在日常开发中看到这种提示,建议停下来想想这个重名有没有必要。很多时候删掉子类那个变量,逻辑反而更清晰。

写单元测试时专门覆盖“父子同名变量”的边界情况。这类bug通常不在正常流程暴露,而是某天有人用父类引用去调子类对象时才突然出现。提前写一个测试用例钉住行为,能防止后续重构时无意踩雷。

我在一个老项目里就遇到过类似的事:一个父类保存了订单金额的中间值,子类为了展示格式也定义了一个同名字段,结果某次促销活动因为金额类型改造,子类字段被赋了新值,但父类计算的时候用的是父类字段,导致有两套金额,结算对不上。排查了两天才发现是变量隐藏惹的祸。后来清理设计时果断把字段私有化,子类全走getter,问题彻底消失。这个经历让我对同名变量有了刻骨铭心的警惕。

8. 扩展阅读:字段隐藏与早期绑定的一些补充

8.1 隐藏并不限于继承

成员变量隐藏还有一种常见形式发生在内部类和外部类之间。内部类里如果定义了和外部类同名的变量,直接访问拿的是内部类的,用OuterClass.this.变量名才能拿到外部类的。

public class Outer { String name = "Outer"; class Inner { String name = "Inner"; void print() { System.out.println(name); // Inner System.out.println(Outer.this.name); // Outer } } }

这种写法在构建Builder模式或数据结构时偶尔见到,理解原理后再说上很自然。它背后的逻辑依然是作用域优先,只是作用域从“继承链”变成了“嵌套层级”。

8.2 变量隐藏与final的相互作用

如果父类的成员变量用final修饰,子类还能定义同名变量吗?语法上是允许的,但你会被IDE和编译器警告“隐藏了父类的final字段”。因为final字段的语义是“只能赋值一次且不可改变”,你在子类再定义一份同名变量,意义不大还容易误导读者。更好的做法是干脆不要重名,用不同的字段名表达不同的含义。

8.3 关于早期绑定与晚期绑定

成员变量访问走的是早期绑定(编译期确定引用目标),方法调用走的是晚期绑定(运行时确定实际方法)。这是Java多态底层的标准表述,也是整篇文章所有现象背后的统一解释。

用更生活化的说法:变量的“邮寄地址”在代码编译时就印好了,不会换;方法的“电话号码”是到了打电话那一刻才查到的,实际接听人可能变。这个类比记牢,至少能应对八成相关讨论。

9. 最后补充一个调试技巧

如果哪天线上代码出现“预期是子类值,输出却是父类值”的诡异问题,可以从下面几个方向排查:

1. 确认访问语句写在哪个类的方法里——方法归属决定了默认查找起点 2. 确认被访问的变量是成员变量还是局部变量——局部变量永远优先 3. 确认变量有没有用this/super显式指定——指定之后就不走就近规则 4. 确认引用类型是什么——父类引用访问到父类字段,即使对象是子类 5. 确认方法有没有被重写——父类方法里访问自己的字段,子类字段不参与

排查时最好直接打印一份“变量归属链”,把每一层的值都打出来对比,比如:

System.out.println(this.x); // 当前类 System.out.println(super.x); // 父类

如果两个值不一样,说明隐藏问题被触发了;如果一样,边界情况(比如中间层没有定义同名变量)就不存在。实测下来这个办法比肉眼盯代码快得多,因为变量隐藏的代码往往表面上完全正常。

关于继承的成员变量访问就聊到这里。学习这一块最有效的方式不是背规则,而是自己多写几个父子类,把this、super、直接访问、强制转型全部组合一遍,看输出、改代码、再看输出,几轮下来自然就内化了。以后遇见继承体系里的怪问题,也能第一时间想到“变量隐藏”这一个方向了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/27 1:09:47

Proteus STM32仿真调试:超声波测距与OLED显示全流程实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/27 1:08:57

CH340驱动装不上?从原理到实战彻底解决USB转串口驱动问题

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/27 1:08:55

Wireshark从入门到实战:抓包、过滤器与网络故障排查全攻略

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/27 1:08:35

Wokwi本地仿真:VS Code+Docker+Arduino CLI零成本搭建开发板环境

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/27 1:08:17

埃夫特ER_Factory_Trail工业机器人仿真:从安装到工作站搭建全攻略

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/27 1:08:13

STM32软解码EV1527:从波形到按键码的完整实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华