news 2026/9/29 1:51:37

STM32实战入门:用C++写下第一行点灯代码

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32实战入门:用C++写下第一行点灯代码

这句吐槽我收到了,而且得正面回应:你说得对。前四篇我一直在讲开发板怎么选、工具链怎么搭、时钟树长什么样、GPIO的推挽和开漏到底有什么区别,确实没有让你亲手写过哪怕一行代码。但这篇的目的就一个——让你真正写出第一行能在STM32上跑起来的C++代码,然后看着板子上的LED按照你的节奏亮起来。

这篇不整虚的,我们就做三件事:把工程环境准备好、写一个点灯的C++类、再把它烧进板子看现象。中间我会顺带解释那些必须解释的坑,比如为什么CubeMX生成的main.c要改成main.cpp、为什么嵌入式里C++的构造函数不是你想象的那个执行顺序、为什么异常和RTTI默认要关掉。看完这篇,你不需要再看别的教程,照着做就能把这“第一行”落地。适合谁?适合已经装好Keil、有一块STM32开发板、前几篇看了但又有点囫囵吞枣的朋友。零基础也能跟下来,我不会跳过任何一个关键操作。

1. 为什么拖到第五篇才让你写代码

1.1 嵌入式写代码的“第一行”门槛

很多人觉得编程的第一课就应该是printf("Hello World"),但在嵌入式里,这条路走不通。你在电脑上写个Python脚本,双击能跑,是因为操作系统替你包办了内存管理、进程调度、外设驱动。单片机上没有这些东西,你写的那行代码要直接面对CPU、寄存器、中断向量表和链接脚本。

用一个做菜的类比:在电脑上编程,相当于你进了后厨什么都给你备好了,锅是热的,调料是齐的,你只管炒。在单片机上编程,相当于给你一堆原材料和一个空灶台,你得先确认煤气罐接没接、火打不打得着、锅放在哪个位置,然后才能开始炒菜。前四篇我做的就是在给你搭灶台、接煤气、备锅。第一篇如果就直接让你写点灯代码,你连GPIO引脚在哪、时钟树为什么有SYSCLK和PCLK2这些概念都搞不清楚,遇到问题只会瞎猜,最终结果大概率是“程序烧进去了,灯就是没反应”,然后放弃。

1.2 前四篇铺垫的是什么

前四篇的铺垫其实分成了两条线,一条是硬件线,一条是工具线。

硬件线讲的是STM32的基本结构:电源、时钟、复位、GPIO、定时器、串口这些外设分别是什么,它们挂在哪些总线上,寄存器操作和HAL库封装之间的关系是什么。这些知识不是“理论推演”,而是你写代码时每个函数调用背后的实质。比如HAL_GPIO_WritePin这个函数,你得知道它的本质是往GPIO的ODR寄存器里写一个位,这样你才不会因为“引脚配置成复用还是推挽”这种问题卡两个小时。

工具线讲的是开发环境的搭建和使用:Keil MDK怎么安装、芯片包怎么装、ST-Link调试器怎么连接、CubeMX怎么生成工程、烧录工具怎么用。这些是纯操作性的内容,但你绕不过去,因为嵌入式开发就是“工具链 + 代码 + 硬件”三者互动的过程。很多人被劝退不是因为代码难,而是因为工具链没搞定。前几篇把工具链的雷都给你排了一遍,这篇就可以专心写代码了。

所以,前四篇不是在拖你进度,而是在帮你建立“代码是跑在具体硬件上”的思维习惯。这篇终于到了动真格的时候,而且我会尽量把每一行的来龙去脉都讲清楚,不仅让你写出来,还让你知道它是怎么跑起来的。

2. 开发环境准备:从CubeMX到C++工程

2.1 CubeMX的四个关键配置

要得到第一个可用的C++工程,还是得先用STM32CubeMX生成一个基础工程。这里我用最常见的STM32F103C8T6和一块带板载LED的普中/正点原子类开发板举例,型号不同配置思路完全一样。

打开CubeMX,选好芯片型号后,需要配置下面几项。

第一项是RCC,在System Core > RCC里把High Speed Clock (HSE)和Low Speed Clock (LSE)设置为Crystal/Ceramic Resonator。这样板子上的8MHz外部晶振才会被启用,否则芯片只会用内部RC振荡器跑,精度和稳定性都有问题。

第二项是调试接口,在System Core > SYS里把Debug选成Serial Wire。这个设置非常关键,如果你不选,CubeMX默认会把SWD引脚当成普通GPIO初始化,导致你烧录一次程序之后,ST-Link再也连不上芯片,只能通过ISP擦除或按复位键才能恢复。踩过这个坑的人绝对不止我一个。

第三项是时钟树,在Clock Configuration页面里配置PLL倍频。以F103为例,外部8MHz经过PLL Source后,通过PLL Multiply设置成9倍,得到72MHz的SYSCLK,然后再分频给APB1和APB2总线。CubeMX会在你拖动数值后自动检测合法性,你只要确认HCLK那一栏显示72MHz就行。

第四项是GPIO,找到板载LED对应的引脚,把它配置为Output Push Pull。我这里假设LED接在PC13上,根据板子的原理图可能是PA1、PB0,自己对着改。注意的是很多板子LED是低电平点亮,所以GPIO的初始电平可以设成High,避免上电后LED先亮一下。

生成工程前,Project Manager里把Toolchain选成MDK-ARM,最小堆栈大小建议设0x200以上,其他默认就行。点击GENERATE CODE,基础工程就出来了。

2.2 把main.c改成main.cpp

这是本篇文章最关键的一步:C++和C的编译器处理逻辑不同,链接时的符号命名规则也不同。CubeMX生成的是C工程,main.c、stm32f1xx_it.c这些文件都是C语言写的。要让C++程序跑起来,最简洁的做法是把main.c重命名成main.cpp。

具体操作是:在Keil里进入工程管理界面(Manage Project Items),把Application/User/Core组里的main.c移除,再把这个文件在Windows资源管理器里改名成main.cpp,然后回到Keil里把main.cpp添加回同一个组。这一步做完,工程里的主文件就已经按C++规则编译了。

但不建议把其它.c文件也改成.cpp。HAL库源码本身是C语言写的,C++编译器虽然能编译C代码,却可能遇到C++关键字冲突等问题。比如class、template这些C++关键字如果在C代码里作为变量名出现,编译会直接报错。而且HAL库里很多函数是用C语言风格设计的,没必要强行改成C++编译。

改完main.cpp后,打开文件看一眼,你会发现CubeMX生成的代码里有大段的/* USER CODE BEGIN */和/* USER CODE END */注释块,这些是给用户代码预留的保护区。以后你在CubeMX里修改配置,重新生成代码时,这些区块里的内容不会被覆盖。我们的代码就要写在这些区域里。

还有一点要特别留意:stm32f1xx_it.c文件里的SysTick_Handler、ADC_IRQHandler这些中断服务函数如果将来要放到.cpp文件里实现,必须在函数外面加上extern "C"保护,否则C++编译器会对函数名做修饰,中断向量表就找不到对应符号了。

2.3 Keil编译器选项:AC6、MicroLib与C++标准

打开Keil的Options for Target,在Target选项卡里你会看到ARM Compiler的选项,默认可能是Use default toolchain,对应的是AC5(armcc)。如果你用的是Keil MDK 5.36以上版本,建议直接选用Use latest installed version,也就是AC6(armclang)。AC6跟AC5相比,对C++11、C++14的支持彻底得多,模板、lambda表达式、constexpr这些现代C++特性都能用,这对我们写嵌入式C++很有意义。AC5对C++的支持停留在老古董水平,很多写法会报错。

在C/C++选项卡的Language Standard里选择GNU++11或GNU++14,别选C++03。GNU++系列比标准C++系列多支持一些GNU扩展,比如__attribute__关键字,这在链接脚本和启动文件里会用到。

还有一个选项是MicroLib,在Target选项卡里勾选Use MicroLib。MicroLib是ARM提供的精简版C运行库,体积小、占用RAM少,非常适合单片机。代价是某些C标准库函数实现得比较粗糙,比如printf的浮点输出默认不支持,需要额外勾选Use MicroLIB的Floating Point选项或用printf重定向。对咱们点灯程序来说,直接用MicroLib是完全没有问题的。

同时,在C/C++选项卡的Misc Controls里,建议加上下面这两行,它们的作用分别是关闭C++异常支持和关闭RTTI:

-fno-exceptions -fno-rtti

为什么默认要关掉这两个东西,我后面单独讲。现在先加上,省得工程里不小心用到异常特性时编译出一堆看不懂的错误。

3. 第一行C++代码:写一个点灯类

3.1 类设计:Led需要封装什么

嵌入式C++和桌面C++最大的区别在于,我们想要的不是抽象程度极高的类体系,而是能直接映射到硬件行为的小而美的封装。

点灯这个操作,从硬件的角度看就三件事:控制引脚的输出电平、读取引脚当前的电平、翻转引脚电平。对应到C语言里,就是反复调用HAL_GPIO_WritePin和HAL_GPIO_ReadPin。但这有两个问题。

第一个问题是可读性差。当你写了几百行C代码之后,看到HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET),你得去查GPIOC和GPIO_PIN_13是谁,才知道这是点灯。但如果你写的是led.TurnOn(),任何人一看就懂。

第二个问题是复用性差。一个板子上可能有电源指示灯、状态灯、报警灯,每个灯的引脚和有效电平不同。C语言的做法是复制粘贴一大堆宏定义,C++的做法是定义一个类,每次创建不同对象就行。

所以我设计了下面这样一个Led类,它要封装的信息有三个:引脚所属的GPIO端口、引脚编号、有效电平。有效电平是这个类设计里容易被忽略的地方。有的板子LED接地,引脚输出高就亮,这是高电平有效;有的板子LED接在电源和引脚之间,引脚输出低才亮,这是低电平有效。同一个类如果不处理这个差异,将来换一块板子就要改一堆代码,违背了封装的本意。

3.2 完整实现与主程序调用

我把头文件led.h这样写:

#pragma once #include "stm32f1xx_hal.h" class Led { public: Led(GPIO_TypeDef* port, uint16_t pin, bool activeHigh = true); void TurnOn(); void TurnOff(); void Toggle(); bool GetState() const; private: GPIO_TypeDef* m_port; uint16_t m_pin; bool m_activeHigh; bool m_state; };

实现文件led.cpp这样写:

#include "led.h" Led::Led(GPIO_TypeDef* port, uint16_t pin, bool activeHigh) : m_port(port) , m_pin(pin) , m_activeHigh(activeHigh) , m_state(false) { } void Led::TurnOn() { if (m_activeHigh) { HAL_GPIO_WritePin(m_port, m_pin, GPIO_PIN_SET); } else { HAL_GPIO_WritePin(m_port, m_pin, GPIO_PIN_RESET); } m_state = true; } void Led::TurnOff() { if (m_activeHigh) { HAL_GPIO_WritePin(m_port, m_pin, GPIO_PIN_RESET); } else { HAL_GPIO_WritePin(m_port, m_pin, GPIO_PIN_SET); } m_state = false; } void Led::Toggle() { uint32_t current = HAL_GPIO_ReadPin(m_port, m_pin); if (current == GPIO_PIN_SET) { HAL_GPIO_WritePin(m_port, m_pin, GPIO_PIN_RESET); m_state = false; } else { HAL_GPIO_WritePin(m_port, m_pin, GPIO_PIN_SET); m_state = true; } } bool Led::GetState() const { return m_state; }

仔细看构造函数,你会发现它没有做任何引脚初始化,只是把端口、引脚、极性这些参数存到成员变量里。这是刻意为之。初始化的事情交给CubeMX生成的MX_GPIO_Init()去做,Led类只负责“操作”,不负责“初始化”。这样可以避免构造函数里调用HAL函数带来的一个隐含风险——如果全局Led对象的构造函数在main之前执行,而MX_GPIO_Init()还没跑,引脚配置就是错误的,此时在构造函数里操作GPIO会出大问题。

在main.cpp里,我这样实例化并使用:

#include "main.h" #include "led.h" // USER CODE BEGIN PV Led statusLed(GPIOC, GPIO_PIN_13, false); // 低电平点亮 // USER CODE END PV int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); while (1) { statusLed.TurnOn(); HAL_Delay(200); statusLed.TurnOff(); HAL_Delay(200); } }

如果你的板子LED是高电平有效,第三个参数用默认值或显式写true就行。这个例子里的GPIOC, GPIO_PIN_13要看你的板子原理图,如果是PA1或PB0要自己改。

编译烧录之后,你应该看到LED以200毫秒的周期闪烁。

3.3 “写这一行”背后发生了什么

在电脑上写一个int a = 1;,编译器生成一条指令,操作系统帮你分配内存。在单片机上写statusLed.TurnOn(),实际发生的事要多得多。

TurnOn()函数里调用HAL_GPIO_WritePin,HAL库函数的实现是往GPIO端口的BSRR寄存器写入一个值。BSRR是一个32位寄存器,高16位是引脚复位控制,低16位是引脚置位控制。你写GPIO_PIN_SET到对应位,硬件电路就会在下一个时钟沿把引脚电平拉高或拉低。整个过程没有操作系统介入,没有虚拟内存,CPU直接操作内存映射的寄存器地址。

所以你写的这行代码,本质上是一个“精确控制硬件电平”的快捷方式。这也解释了为什么嵌入式编程要求你对硬件足够熟悉——你写的一行代码直接决定了某个物理引脚的电平状态,而不是产生一个“抽象的变量变化”。这就是嵌入式C++和普通C++学习路径最大的分岔点。

4. 嵌入式C++的开关学问:异常、RTTI与模板

4.1 为什么我把异常关掉

在桌面C++里,try/catch是处理错误的标准手段,但在单片机上,异常机制默认应该关掉,原因有两条。

第一,异常机制的实现依赖RTTI和栈展开,编译器需要为每个可能抛出异常的函数生成额外的处理代码,这会显著增加代码体积。举个例子,同样一个简单的点灯程序,开启异常和RTTI之后,编译出来的二进制可能从2KB膨胀到4KB以上,这对动辄只有几十KB Flash的单片机来说很奢侈。

第二,栈展开需要足够的栈空间。STM32F103C8T6的RAM只有20KB,默认栈大小在启动文件里通常只有1KB。如果异常在函数调用链深处抛出,栈展开过程中需要调用析构函数、释放局部对象,栈空间不够的话直接硬件错误。而且异常一旦抛出,程序跳到了哪里你很难在调试器里追踪,对嵌入式系统来说这种“不可预期”的行为很要命。

所以我在编译选项里加了-fno-exceptions。这样做的另一个好处是,new运算符失败时不会抛异常,而是返回nullptr,我们可以在代码里直接判断返回值。这对嵌入式编程反而更友好。

4.2 RTTI、模板和体积的取舍

RTTI(运行时类型信息)是dynamic_cast和typeid的支撑。在桌面C++里,多态类型的识别靠的就是RTTI。但在嵌入式里,RTTI意味着每个有虚函数的类都要额外存储类型信息,类越多,体积越大。

我个人的建议是,不用dynamic_cast和typeid,就把RTTI关掉。如果确实需要判断对象类型,更推荐的做法是自己加一个类型枚举字段,或者在设计阶段通过接口抽象来避免类型判断。这正是嵌入式C++和桌面C++思维上的差别:用编译期多态和设计模式替代运行期类型识别。

模板在嵌入式C++里是完全可用的,而且我用得还挺多。模板的开销发生在编译期,不占用运行时资源,这非常适合单片机。比如下面这个寄存器映射的模板类,就是嵌入式C++里的经典用法:

template <uint32_t regAddr> struct Reg { static void Write(uint32_t value) { *(volatile uint32_t*)regAddr = value; } static uint32_t Read() { return *(volatile uint32_t*)regAddr; } };

用的时候就是Reg<GPIOB_BASE + 0x14>::Write(0x00000001),它跟直接操作寄存器生成的汇编指令一模一样,没有任何运行时开销。

但模板也不能滥用。模板的每一次实例化都会生成一份代码,如果你用模板封装了十个不同引脚的LED操作,最终生成的代码量可能比一个普通Led类大不少。嵌入式开发里“过度模板化”导致的Flash溢出,是个很现实的坑。我的经验是:用模板处理“编译期就知道的东西”,比如寄存器地址、波特率、引脚号;用普通类处理“运行期才决定的东西”,比如状态机的状态、通信协议的包长。

4.3 AC5和AC6的编译选项对照

如果你在用AC5(armcc),关闭异常和RTTI的选项不是-fno-exceptions,而是--no_exceptions和--no_rtti。这两个编译器的选项风格不一样,容易踩坑。

功能AC5(armcc)AC6(armclang)
关闭异常--no_exceptions-fno-exceptions
关闭RTTI--no_rtti-fno-rtti
C++标准--cpp11-std=gnu++14
优化选项-O1 -O2 -O3-O1 -O2 -O3

我建议能用AC6就用AC6,不只是因为C++标准支持得更好,还因为armclang是LLVM架构,错误提示比armcc友好得多。你在网上搜到的一些AC5的老经验,不一定能直接套到AC6上,这点要小心。

5. 烧录、调试、看现象

5.1 编译烧录的标准流程

先把代码写完,然后点Keil的编译按钮。正常情况下,0 Error,0 Warning。然后接上ST-Link,点击Download按钮,程序就烧进去了。如果之前烧录时忘记配置Debug选项导致连不上,可以按住开发板复位键不放,点烧录,在下载进度刚开始的时候松开复位,通常能救回来。

烧录完成后,按一下复位键,或者断电重新上电,程序开始运行。你应该看到LED以200毫秒周期闪烁,而不是一上电就常亮或者完全不亮。

如果LED不亮,先从硬件查起:板载LED有没有被跳线帽禁用?LED本身是不是坏的?这个排查顺序很重要,很多新手一上来就怀疑代码,其实嵌入式里硬件问题的概率比你想的高得多。

5.2 调试器里观察C++对象

Keil的调试器进去之后,按F10单步执行,你会看到程序跳进了各种系统初始化函数。这是因为单片机的启动流程是:复位向量 -> 启动文件 ->SystemInit->__main-> C库初始化 ->main函数。所以在main函数第一行断点之前,CPU已经执行了上百行汇编了。

在调试器里,你可以打开View > Watch窗口,把statusLed对象添加进去观察。你会看到对象内部有m_port、m_pin、m_activeHigh、m_state这几个成员变量。单步执行TurnOn(),观察m_state从false变成true的过程,这会让你对“对象在被真实硬件使用”这件事有非常直观的感觉。

还可以把断点打在Led的构造函数里。你会惊讶地发现,如果statusLed是全局对象,构造函数在main函数之前就已经执行了,而不是在main里执行到那一行才调用。这正是我前面强调的“全局对象构造时机”问题,现场看一次比我说一百次都管用。

5.3 一个重要执行顺序问题:构造函数到底什么时候跑

如果你把Led对象创建成全局变量,那么它的构造函数在main函数之前就会被C运行库调用。Keil的启动文件里,__main会调用__scatterload完成代码和数据的加载,接着调用__rt_entry,这个过程中会执行__libc_init_array,而__libc_init_array正是负责遍历.init_array段、调用所有全局对象的构造函数。

这意味着一个隐患:如果你的构造函数里有操作外设的代码,比如初始化GPIO、配置定时器,但这些外设的初始化通常在main函数里HAL_Init()之后才执行,构造顺序就反了。

为了解决这个顺序问题,嵌入式C++里有一个老派但实用的约定:不在全局对象的构造函数里做任何硬件初始化。构造函数只保存参数和状态,所有对外设的实际操作留到main函数里,由对象的方法显式调用。这就是我为Led类构造函数只保存参数、不调用HAL函数的原因,看懂这个设计,你就真正理解了嵌入式C++“初始化顺序”这个核心问题。

6. 常见问题速查与踩坑记录

下面这些问题是我实际测试和帮朋友排查时遇到过的,整理成表格方便你快速定位。

6.1 编译阶段问题

错误提示可能原因解决办法
Undefined symbol SysTick_Handler把中断函数放进了.cpp却没用extern "C"用extern "C"包裹中断函数定义
expected primary-expression before 'class'某些C库文件被强行改成.cpp只把main.c改成main.cpp,其它C文件保持.c
Compiler error C290AC5不支持某些C++11语法换成AC6编译器
cannot open source input file "stm32f1xx_hal.h"头文件路径没包含在Options for Target > C/C++ > Include Paths里添加HAL库路径
L6218E: Undefined symbol HAL_GPIO_WritePin链接时没包含HAL库源文件确认stm32f1xx_hal_gpio.c已加入工程组

6.2 运行阶段问题

现象可能原因解决办法
烧录后LED常亮极性设置反了把Led构造函数第三个参数改成相反的布尔值
烧录后LED完全无反应GPIO没初始化确认CubeMX里LED引脚已配置为输出
程序跑飞卡死在HardFault栈溢出或指针越界调大启动文件里的Stack大小,检查数组越界
只在复位时LED闪一下然后停住时钟配置有问题确认CubeMX时钟树里HCLK为72MHz而不是默认的8MHz
ST-Link连不上芯片SWD引脚被占用按住复位键烧录,或用ISP工具擦除Flash

6.3 我踩过的一个真实教训

有一段时间我帮朋友调一个基于STM32F407的板子,他把整个工程的所有.c文件都改成了.cpp,编译报错一大堆,我帮他改回只保留main.c为main.cpp,才恢复正常。这个过程中我发现板子烧录后稳压芯片发烫,排查后才知道是GPIOC的某个引脚被配置成了高频翻转模式,大电流灌进去导致的。从那以后我就养成了一个习惯:任何LED或IO操作,先量引脚电平,再查代码逻辑,而不是一上来就埋头改代码。

还有一个细节是启用MicroLib之后,printf默认不支持浮点。我第一次用C++串口打印浮点数时,出来的全是乱码,单步跟踪发现是库的问题,不是串口配置问题。如果你要打印浮点数,要么在Options for Target > Target里勾选Use MicroLIB的Floating Point,要么干脆不用printf,改用snprintf配合整数运算拼接字符串。这个坑我帮你踩过了,别再踩一次。

最后再分享一个很小但很实用的习惯:写嵌入式C++时,尽量把类的头文件做得“自包含”,即它自己包含它依赖的所有头文件,而不是依赖调用者提前包含。这样你新建一个对象时,不需要思考“是否已经包含过GPIO头文件”,直接#include "led.h"就能用。我在真实项目里维护了几十个类似的设备类,这个习惯让协作成本降了很多。接下来这个系列,我会用同样的节奏把定时器、串口、状态机都写成C++类,慢慢搭出一套属于你自己的嵌入式C++小型框架。

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

Linux离线环境安装软件包全指南:解决依赖地狱与本地源配置

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

作者头像 李华
网站建设 2026/9/29 1:51:26

2026秋招必看:AI大模型与Agent赛道高薪就业指南(收藏版)

2026年AI岗位需求激增789.47%&#xff0c;薪资高出行业均值26%。传统前后端岗位需求下降52%-65%&#xff0c;而AI Agent赛道岗位需求暴涨300%-455%。大厂AI岗位占比超60%-90%。文章总结了Agent赛道的7个岗位方向&#xff0c;包括模型应用、AI Coding、Agent框架开发等&#xff…

作者头像 李华
网站建设 2026/9/29 1:50:37

图像融合评估指标全解析:SSIM、MI与QAB/F的应用

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

作者头像 李华
网站建设 2026/9/29 1:50:03

AB交换与灰度发布实战:nginx流量切换与生产环境稳定性保障

1. AB交换的整体设计&#xff1a;先搞清楚为什么不能直接切做后端和运维的同学应该都见过这种场面&#xff1a;一个核心服务要升大版本&#xff0c;代码review了两轮&#xff0c;测试环境跑了两周&#xff0c;所有人都觉得没问题&#xff0c;结果上了生产还是出事。不是慢查询把…

作者头像 李华
网站建设 2026/9/29 1:49:29

Qt 5.14.2 通讯录示例:Model/View 与教程双路线

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

作者头像 李华
网站建设 2026/9/29 1:48:52

死区时间详解:防止直通短路的硬件安全阀

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

作者头像 李华