结构体与函数:从数据封装到模块化编程的核心实践
1. 从“数据孤岛”到“智能单元”为什么我们需要结构体与函数如果你刚开始接触编程可能会觉得“结构体”和“函数”是两个独立的概念一个管数据打包一个管逻辑封装。但当你真正开始写一个稍微复杂点的项目比如用STM32控制一个机器人或者用Python处理一份包含用户姓名、年龄、地址的表格数据时你就会发现把它们分开用代码很快就会变得一团糟。想象一下你要管理一个班级的学生信息。没有结构体你可能需要这样char* student1_name 张三; int student1_age 18; float student1_score 95.5; char* student2_name 李四; int student2_age 19; float student2_score 88.0; // ... 更多学生每新增一个学生你就得手动声明三个独立的变量它们之间只有名字上的微弱关联。如果你想写一个函数来打印某个学生的信息你得把三个变量都传进去printStudent(student1_name, student1_age, student1_score)。这太容易出错了万一传错了顺序或者漏了一个bug就来了。这就是“数据孤岛”。而结构体就是来解决这个问题的。它像一个智能的收纳盒把逻辑上属于一个整体的数据打包在一起。上面的例子用结构体可以这样写struct Student { char name[50]; int age; float score; }; struct Student student1 {张三, 18, 95.5}; struct Student student2 {李四, 19, 88.0};现在一个学生的所有信息被封装在student1这个“盒子”里。数据不再是孤立的它们有了一个共同的家。那函数呢函数是行为的封装。继续上面的例子打印学生信息的函数现在清爽多了void printStudent(struct Student stu) { printf(姓名%s 年龄%d 分数%.1f\n, stu.name, stu.age, stu.score); }你只需要传递一个结构体变量stu函数就能访问其内部所有成员。这不仅仅是代码变短了更重要的是它建立了一种清晰的契约这个函数就是用来处理Student这种数据类型的。所以结构体与函数的结合其核心价值在于创建高内聚、低耦合的代码单元。结构体定义了“是什么”数据模型函数定义了“能做什么”行为操作。两者结合就能构建出模拟现实世界实体的“智能对象”雏形。这对于任何想要编写可维护、可扩展、逻辑清晰代码的程序员来说都是必须掌握的基本功。无论你是做嵌入式开发如STM32 HAL库中大量的结构体配置函数还是进行数据分析如Pandas的DataFrame本质上就是高级结构体其str、groupby等都是函数这个组合拳都是你的核心工具。2. 结构体不仅仅是数据的集装箱很多人把结构体理解成“变量打包”这没错但只看到了第一层。在实际开发中结构体的设计直接影响着程序的效率、内存布局乃至整个架构的清晰度。2.1 定义与初始化打好地基定义结构体就是蓝图设计。以C语言为例// 定义一个传感器数据的结构体 struct SensorData { uint32_t timestamp; // 时间戳 int16_t raw_value; // 原始ADC值 float filtered_value; // 滤波后的值 uint8_t sensor_id; // 传感器ID bool is_valid; // 数据是否有效 };这里有几个关键点成员选择哪些数据应该放在一起原则是“高内聚”。SensorData里的所有成员都共同描述一次传感器采样事件。如果把不相关的“设备IP地址”也塞进来就破坏了内聚性。类型选择int16_t、uint32_t这些定宽类型来自stdint.h在嵌入式开发中至关重要它能确保在不同平台上的数据长度一致。bool类型需要C99或以上标准支持。命名清晰filtered_value比f_val好得多能让人一眼看懂用途。初始化结构体有几种常见方式// 方式1声明时按顺序初始化C/C struct SensorData data1 {1630454400, 2048, 2047.5, 1, true}; // 方式2C99/C指定的初始化器更安全顺序无关 struct SensorData data2 { .sensor_id 2, .raw_value 4095, .timestamp 1630454500, .is_valid false, .filtered_value 0.0 }; // 方式3先声明后逐个赋值 struct SensorData data3; data3.timestamp 1630454600; data3.raw_value 1024; // ... 其他赋值实操心得在团队协作或维护旧代码时强烈推荐使用方式2指定初始化。即使未来结构体成员顺序发生变化或者新增了成员你的初始化代码也不会因此出错。这对于维护大型代码库稳定性非常有益。2.2 内存对齐看不见的性能与坑这是结构体进阶必须理解的概念。CPU访问内存时并不是以字节为单位而是以“字长”如4字节、8字节为单位。为了提升访问效率编译器会自动对结构体成员进行“内存对齐”。struct InefficientStruct { char a; // 1字节 int b; // 4字节 char c; // 1字节 short d; // 2字节 };你以为它的内存大小是 1412 8字节在32位系统默认4字节对齐上实际可能是12字节编译器会在char a后面插入3字节的“填充”让int b从4的倍数地址开始在char c后面插入1字节填充让short d从2的倍数地址开始。如何查看和优化可以使用sizeof运算符和offsetof宏。printf(Sizeof struct: %zu\n, sizeof(struct InefficientStruct)); printf(Offset of b: %zu\n, offsetof(struct InefficientStruct, b));优化技巧将大的、对齐要求严格的成员如double,int64_t放在前面小的、对齐要求宽松的如char,bool放在后面可以最大限度减少填充字节节省内存。这在嵌入式开发内存紧张或需要通过网络传输结构体数据时减少带宽占用尤其重要。struct EfficientStruct { int b; // 4字节 short d; // 2字节 char a; // 1字节 char c; // 1字节 // 编译器可能在此处添加2字节填充使整个结构体大小为4的倍数8字节 };这个优化后的版本大小很可能就是8字节。2.3 结构体数组与链表管理数据集合单个结构体用处有限我们通常需要管理一群结构体。结构体数组适用于元素数量固定、需要随机访问的场景#define MAX_STUDENTS 100 struct Student class[MAX_STUDENTS]; int student_count 0; // 添加一个学生如果数组未满 if (student_count MAX_STUDENTS) { class[student_count].age 20; strcpy(class[student_count].name, 王五); student_count; }结构体链表则适用于元素数量动态变化、频繁插入删除的场景。这也是搜索热词“c结构体链表基本语法”的核心。// 定义链表节点 struct ListNode { int data; struct ListNode* next; // 指向下一个节点的指针 }; // 创建链表、插入节点、遍历链表等操作需要配合动态内存管理malloc/free链表的核心在于每个节点结构体内部包含一个指向同类型节点的指针从而形成链式结构。它避免了数组大小固定的限制但失去了随机访问的能力访问第N个元素需要从头遍历。避坑指南处理链表时务必注意指针初始化和内存释放。新节点的next指针应初始化为NULL。在删除节点或释放整个链表时要确保不会出现“野指针”或“内存泄漏”。一个常见的技巧是使用“二级指针”或“返回新的头指针”来简化链表头节点的修改操作。3. 函数让结构体“活”起来结构体封装了数据而函数赋予了数据生命。函数操作结构体主要有三种方式传值、传指针、传引用C。3.1 传值 vs. 传指针效率与副作用的权衡传值调用函数收到的是结构体的一份完整拷贝。void printStudentByValue(struct Student stu) { printf(Name: %s\n, stu.name); // 修改stu只会影响函数内的副本不影响原数据 stu.age 100; }优点安全函数内部修改不会影响外部变量。 缺点当结构体很大时比如包含数个数组成员复制整个结构体的开销非常大影响性能。传指针调用函数收到的是结构体地址的拷贝。void printStudentByPointer(struct Student *pStu) { if (pStu NULL) return; // 良好的习惯检查指针有效性 printf(Name: %s\n, pStu-name); // 使用 - 操作符访问成员 pStu-age 100; // 这个修改会直接影响原结构体 } void calculateAverageScore(struct Student *array, int size, float *result) { float sum 0.0f; for(int i 0; i size; i) { sum array[i].score; // 通过指针访问数组元素 } *result sum / size; // 通过指针返回计算结果 }优点高效无论结构体多大只传递一个指针通常是4或8字节。函数可以直接修改原数据。 缺点存在意外修改原数据的风险需要程序员小心控制。选择策略只读操作如果函数只是读取结构体内容而不修改且结构体很小比如只有两三个基本类型成员传值简单安全。写入操作或结构体较大一律使用传指针。这是C语言中最常见的做法。对于“只读”的参数可以用const修饰指针增加安全性并表达意图。void printStudentConst(const struct Student *pStu) { // pStu-age 100; // 编译错误不能通过const指针修改数据 printf(Age: %d\n, pStu-age); }3.2 返回结构体从函数中“产出”数据函数也可以返回一个结构体。struct Student createStudent(const char* name, int age, float score) { struct Student s; strncpy(s.name, name, sizeof(s.name)-1); s.name[sizeof(s.name)-1] \0; // 确保字符串终止 s.age age; s.score score; return s; // 返回整个结构体 }在C语言中返回结构体意味着发生一次拷贝。现代编译器通常会进行“返回值优化”但为了代码清晰和效率更常见的模式是让调用者提供存储空间函数通过指针填充。// 更优的模式由调用者管理内存 bool createStudent(struct Student *s, const char* name, int age, float score) { if (s NULL) return false; // ... 填充s指向的结构体 ... return true; // 返回状态指示成功/失败 }这种模式在嵌入式或系统编程中非常普遍它明确了内存的所有权属于调用者避免了不必要的拷贝也便于错误处理。3.3 回调函数将行为作为参数传递这是函数指针与结构体结合的强大模式也是热词“回调函数c顿悟”所指的核心概念。它允许你将一个函数行为像数据一样传递给另一个函数。一个经典的应用场景是事件处理或遍历算法// 定义回调函数类型 typedef void (*DataProcessor)(struct SensorData*); // 一个可以处理数据数组的函数具体处理逻辑由回调函数决定 void processSensorDataArray(struct SensorData array[], int size, DataProcessor processor) { for (int i 0; i size; i) { if (array[i].is_valid) { processor(array[i]); // 调用回调函数处理当前数据 } } } // 具体的回调函数1打印数据 void printData(struct SensorData* data) { printf(Time: %u, Value: %.2f\n,>#include stdio.h #include string.h #include stdbool.h #define MAX_NAME_LEN 50 #define MAX_STUDENTS 100 // 1. 定义核心结构体 typedef struct { int id; // 学号 char name[MAX_NAME_LEN]; // 姓名 float score; // 成绩 } Student; // 2. 定义管理系统上下文结构体 typedef struct { Student students[MAX_STUDENTS]; // 学生数组 int count; // 当前学生数量 } StudentManager;这里我们做了两个关键设计使用typedef为结构体创建了别名Student和StudentManager这样后面就不需要每次都写struct关键字。StudentManager是一个“管理器”结构体它封装了学生数组和计数器使得状态管理更集中。这是一种简单的面向数据设计。4.2 核心功能函数的实现接下来我们实现针对StudentManager的增删改查函数。注意这些函数都接收StudentManager*指针以便直接修改管理器状态。// 3. 初始化管理器 void manager_init(StudentManager* mgr) { if (mgr NULL) return; mgr-count 0; memset(mgr-students, 0, sizeof(mgr-students)); // 清零数组 printf(学生管理系统已初始化。\n); } // 4. 添加学生 bool manager_add_student(StudentManager* mgr, int id, const char* name, float score) { if (mgr NULL || name NULL) return false; // 检查数组是否已满 if (mgr-count MAX_STUDENTS) { printf(错误学生数量已达上限(%d)\n, MAX_STUDENTS); return false; } // 检查学号是否重复简单的线性查找 for (int i 0; i mgr-count; i) { if (mgr-students[i].id id) { printf(错误学号 %d 已存在\n, id); return false; } } // 添加新学生 Student* new_stu mgr-students[mgr-count]; new_stu-id id; strncpy(new_stu-name, name, MAX_NAME_LEN - 1); new_stu-name[MAX_NAME_LEN - 1] \0; // 确保字符串安全终止 new_stu-score score; mgr-count; printf(成功添加学生%s (学号%d)\n, name, id); return true; } // 5. 根据学号查找学生返回指针便于后续修改 Student* manager_find_student_by_id(StudentManager* mgr, int id) { if (mgr NULL) return NULL; for (int i 0; i mgr-count; i) { if (mgr-students[i].id id) { return mgr-students[i]; // 返回找到的学生结构体的地址 } } return NULL; // 未找到 } // 6. 显示所有学生信息 void manager_display_all(const StudentManager* mgr) { if (mgr NULL) return; if (mgr-count 0) { printf(当前没有学生记录。\n); return; } printf(\n 所有学生信息 \n); printf(%-10s %-20s %-10s\n, 学号, 姓名, 成绩); printf(-----------------------------------\n); for (int i 0; i mgr-count; i) { printf(%-10d %-20s %-10.2f\n, mgr-students[i].id, mgr-students[i].name, mgr-students[i].score); } printf( 共 %d 名学生 \n\n, mgr-count); } // 7. 计算平均分演示如何通过指针返回结果 bool manager_calculate_average(const StudentManager* mgr, float* p_average) { if (mgr NULL || p_average NULL) return false; if (mgr-count 0) { *p_average 0.0f; return true; // 视为计算成功平均分为0 } float sum 0.0f; for (int i 0; i mgr-count; i) { sum mgr-students[i].score; } *p_average sum / mgr-count; return true; }4.3 主函数与用户交互最后我们编写一个简单的主函数来驱动这个系统形成一个可运行的完整程序。// 8. 主函数 int main() { StudentManager mgr; // 在栈上创建管理器实例 manager_init(mgr); // 初始化 // 添加一些测试数据 manager_add_student(mgr, 1001, 张三, 85.5f); manager_add_student(mgr, 1002, 李四, 92.0f); manager_add_student(mgr, 1003, 王五, 76.5f); // 显示所有学生 manager_display_all(mgr); // 查找并修改学生成绩 int search_id 1002; Student* found manager_find_student_by_id(mgr, search_id); if (found ! NULL) { printf(找到学生%s原成绩%.2f\n, found-name, found-score); found-score 95.0f; // 直接通过指针修改 printf(已将其成绩修改为%.2f\n, found-score); } else { printf(未找到学号为 %d 的学生。\n, search_id); } // 重新显示查看修改结果 manager_display_all(mgr); // 计算并显示平均分 float avg_score; if (manager_calculate_average(mgr, avg_score)) { printf(全班平均分%.2f\n, avg_score); } // 尝试添加重复学号应失败 manager_add_student(mgr, 1001, 赵六, 88.0f); return 0; }编译与运行 将以上所有代码块按顺序保存到一个文件如student_system.c使用C编译器编译运行。gcc -o student_system student_system.c -Wall ./student_system系统输出示例学生管理系统已初始化。 成功添加学生张三 (学号1001) 成功添加学生李四 (学号1002) 成功添加学生王五 (学号1003) 所有学生信息 学号 姓名 成绩 ----------------------------------- 1001 张三 85.50 1002 李四 92.00 1003 王五 76.50 共 3 名学生 找到学生李四原成绩92.00 已将其成绩修改为95.00 所有学生信息 学号 姓名 成绩 ----------------------------------- 1001 张三 85.50 1002 李四 95.00 1003 王五 76.50 共 3 名学生 全班平均分85.67 错误学号 1001 已存在4.4 从示例中学到的关键点数据与操作的绑定StudentManager结构体将数据学生数组和状态数量封装在一起所有操作函数都围绕它展开代码组织清晰。指针的灵活运用manager_find_student_by_id返回一个指针这使得查找后的修改操作非常高效和直接无需再次查找或拷贝数据。错误处理函数通过返回bool类型或检查NULL指针来指示操作成功与否这是健壮性编程的基本要求。模块化思想每个函数职责单一。manager_display_all只负责显示manager_calculate_average只负责计算。这使得代码易于测试、理解和维护。可扩展性这个简单的框架可以轻松扩展。例如你可以很容易地添加按成绩排序、将数据保存到文件、从文件加载数据等功能只需增加新的函数而无需大幅改动现有结构。这个实战项目虽然小但它完整地展示了结构体与函数如何协同工作构建出一个功能清晰、逻辑完整的小型系统。理解了这套模式你就能应对更复杂的、由成百上千个结构体和函数组成的大型项目。

相关新闻

批量更新主键排序防死锁 标准化规范(错误案例+正确案例+原理复盘)

批量更新主键排序防死锁 标准化规范(错误案例+正确案例+原理复盘)

批量更新主键排序防死锁 标准化规范(错误案例正确案例原理复盘) 一、核心概述 数据库批量更新、批量删除场景下,90%偶发性死锁、无规律锁超时故障,均源于「多线程事务加锁顺序不一致」。该类问题本地单线程测试100%正常&#xf…

2026/10/7 12:22:07 阅读更多 →
二叉树_堆的实现

二叉树_堆的实现

1.树树是一种非线性的数据结构,是由m(m>0)个有限节点组成的有层次关系的结合,树的结构中,子树之间是不能有交集的否则就是图,树是递归定义的。常见树的相关术语:⽗结点:若⼀个结点含有⼦结点&#xff0c…

2026/10/13 22:15:06 阅读更多 →
审计专业考研和考证怎么规划

审计专业考研和考证怎么规划

考研和考证,是审计专业学生绕不开的成长主题。很多人在大二大三就开始纠结:应该先全力准备考研,还是趁在校多拿几个证书?其实两者并非对立,如果能结合职业方向做一份清晰的规划,反而能相互借力。这篇文章梳…

2026/10/8 14:34:19 阅读更多 →

最新新闻

拷贝构造函数详解:从深拷贝陷阱到移动语义的现代C++实践

拷贝构造函数详解:从深拷贝陷阱到移动语义的现代C++实践

拷贝构造函数这东西,平时写代码可能不太在意,一旦踩坑,程序崩得莫名其妙,连排查都要花上大半天。我见过太多人因为它半夜抓狂,也见过不少老手在处理对象复制时翻车。这篇文章就把对象复制底层的门道彻底说透&#xff0…

2026/10/15 9:15:05 阅读更多 →
工控AI架构与协议选型:OPC UA、MCP与数字孪生实战

工控AI架构与协议选型:OPC UA、MCP与数字孪生实战

1. 工控AI这盘棋,到底在下什么干了十几年自动化,从最早拿着手持编程器给S7-200灌梯形图,到后来用TIA Portal做整线SCADA,再到现在天天跟数字孪生和AI模型打交道,我最大的感受是:工控行业正在经历一次底层逻…

2026/10/15 9:15:05 阅读更多 →
纯RL也能卷出推理能力:DeepSeek-R1技术报告复现笔记

纯RL也能卷出推理能力:DeepSeek-R1技术报告复现笔记

简介:这份资源围绕DeepSeek团队首个基于强化学习的推理模型展开,系统梳理了DeepSeek-R1-Zero与DeepSeek-R1的研发脉络,面向机器学习研究人员、大模型开发者以及希望提升语言模型推理能力的从业者。内容涵盖纯RL训练下推理行为的自演化过程、冷…

2026/10/15 9:15:05 阅读更多 →
Excel 公式与样式怎么扩展:从 DSL 规则到 JQuick-Excel 源码边界

Excel 公式与样式怎么扩展:从 DSL 规则到 JQuick-Excel 源码边界

Excel 公式与样式怎么扩展:从 DSL 规则到 JQuick-Excel 源码边界 tags: #JQuickExcel #JavaExcel #开源 #POI #Excel工具 简介 一个导出文件“能打开”远远不等于可交付。财务表需要公式,运营表需要日期、金额格式,管理报表需要表头、边框和隔…

2026/10/15 9:15:05 阅读更多 →
小型团队高效管理模式与工具链实践

小型团队高效管理模式与工具链实践

1. 为什么小型团队需要特殊管理模式创业公司或短期项目团队通常面临资源有限、时间紧迫的双重压力。我见过太多团队在初期照搬大公司的层级管理方式,结果陷入流程僵化、决策迟缓的困境。一个10人左右的App开发团队,曾经因为每个功能改动都需要经过三层审…

2026/10/15 9:15:05 阅读更多 →
Tuxedo客户端域连接配置与TPEDOMAIN错误排查指南

Tuxedo客户端域连接配置与TPEDOMAIN错误排查指南

1. 项目概述:这不是一次普通网络连通测试,而是一次面向生产环境的域身份验证闭环验证“tuxedo域连接配置与测试(客户端测试)”这个标题里藏着三个关键信号:tuxedo——不是Linux发行版Tuxedo OS,而是BEA&…

2026/10/15 9:14:01 阅读更多 →

日新闻

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

1. 为什么选择容器化方式部署 LoRa 网络服务LoRa 物联网项目落地时,网络服务器是整个链路的核心枢纽。它负责接收网关上报的数据包、完成去重与解密、再把有效载荷分发给上层应用。早期大家习惯把网络服务器直接装在物理机上,依赖一堆系统库和特定版本的…

2026/10/15 0:00:46 阅读更多 →
共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

1. 共享推理服务里,扩缩容到底难在哪做过大模型推理服务部署的人,大概都经历过这样的场景:一个团队共用一套推理集群,A 业务跑的是 7B 的小模型,B 业务跑的是 72B 的大模型,C 业务白天流量高、晚上几乎归零…

2026/10/15 0:00:46 阅读更多 →
Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

1. 引子:一次内核恐慌引发的追查先从一个真实场景说起。某次我给一台老旧的嵌入式设备折腾系统裁减,为了把启动时间压进两秒以内,我手贱地把一串自认为“没用”的 systemd 服务给 disable 了,其中就包括systemd-remount-fs.servic…

2026/10/15 0:00:46 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/14 3:30:28 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/14 16:30:29 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/14 3:29:58 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

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

2026/10/15 0:14:36 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

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

2026/10/14 22:45:47 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

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

2026/10/14 7:53:34 阅读更多 →