☰
单片机C++实战:从C迁移到面向对象开发与内存优化
2026/9/29 5:05:52 网站建设 项目流程

1. 为什么要在单片机上折腾C++

很多人第一次接触单片机编程,学的都是C语言。51单片机、STM32、GD32,翻开任何一本入门教材,清一色的C。原因很简单:资源受限、编译器支持有限、C语言足够贴近硬件。但当你真正做过几个稍微复杂的项目之后,会发现一个尴尬的现实——代码越写越乱,状态机越堆越多,模块之间的耦合像一团乱麻。这时候,C++的价值就体现出来了。

我最早在STM32F103C8T6上尝试用C++写代码,大概是在做完第三个项目之后。当时用C写了一个带菜单系统的数据采集器,光是状态切换和界面刷新就写了上千行,改一个功能要翻遍整个工程。后来换成C++重新组织,用类和命名空间把显示、按键、采集、存储拆开,代码量直接砍掉三分之一,可读性提升了一个档次。从那以后,我基本上只要芯片Flash大于64KB、RAM大于20KB,就会优先考虑C++。

当然,在单片机上用C++和在上位机上用C++完全是两码事。你不能随便new一个对象就不管了,不能大量使用虚函数,不能依赖标准模板库的大部分容器。你得时刻盯着编译出来的固件大小,盯着栈空间够不够,盯着中断响应会不会被拖慢。这些约束听起来很烦,但习惯了之后反而会让你写出更扎实的代码。

这篇文章主要面向两类人:一类是已经会用C写单片机程序,想往C++方向转的开发者;另一类是在校学生或者刚入行的朋友,正在做单片机课程设计或毕业设计,想了解一下C++在嵌入式场景下到底怎么用。我会从工程配置、语言特性取舍、内存管理、外设驱动封装、常见问题排查几个角度展开,尽量把踩过的坑和总结出来的经验都讲清楚。

2. 开发环境搭建与编译器选择

2.1 工具链的几种主流组合

在单片机上写C++,第一步不是写代码,而是把工具链配好。不同的芯片平台,可选的方案差别很大。我整理了一个对比表格,方便你根据自己的情况选择。

平台推荐工具链编译器调试方式适用场景
STM32STM32CubeIDEarm-none-eabi-g++ST-Link SWD中大型项目
STM32VSCode + Cortex-Debugarm-none-eabi-g++OpenOCD + ST-Link喜欢自定义环境
GD32Keil MDKARMCC/AC6J-Link传统工程迁移
51单片机Keil C51C51编译器STC-ISP串口下载入门学习
STC单片机Keil C51 / SDCCC51/SDCC串口下载小资源场景

这里要特别说明一下,51单片机基本上不支持C++。Keil C51编译器对C++的支持极其有限,SDCC虽然有一些C++支持,但也不完整。所以如果你用的是51单片机,比如STC89C52或者STC15系列,老老实实用C写就行。C++在单片机上的主战场是ARM Cortex-M系列,尤其是STM32和GD32这类资源相对充裕的芯片。

2.2 VSCode配置C/C++环境的要点

现在越来越多的人喜欢用VSCode写单片机代码,轻量、插件丰富、界面舒服。但配置起来确实比Keil或者CubeIDE麻烦一些。我把自己常用的配置流程梳理一下。

首先需要安装的插件包括:C/C++(Microsoft官方那个)、Cortex-Debug、ARM Assembly。然后需要下载arm-none-eabi-gcc工具链,解压后把bin目录加到系统PATH里。验证是否成功,可以在终端里输入:

arm-none-eabi-g++ --version

如果能看到版本信息,说明工具链没问题。接下来在VSCode的c_cpp_properties.json里配置头文件路径,主要是CMSIS头文件和芯片厂商的HAL库路径。这个文件可以通过Ctrl+Shift+P然后输入C/C++: Edit Configurations来生成。

编译和下载通常用Makefile或者CMake来管理。STM32CubeMX可以直接生成Makefile工程,省去很多手工配置的麻烦。我一般会用CubeMX生成初始化代码,然后手动把工程改造成C++风格,把main.c改成main.cpp,把外设初始化封装成类。

注意:把.c文件改成.cpp之后,原来用C写的头文件需要用extern "C"包裹,否则链接时会报符号找不到的错误。这个坑我踩过不止一次。

2.3 编译器选项的关键参数

用g++编译单片机代码,有几个编译选项必须关注。这些参数直接影响生成的固件大小和运行效率。

arm-none-eabi-g++ -mcpu=cortex-m3 -mthumb -Os -fno-exceptions -fno-rtti -fno-threadsafe-statics -ffunction-sections -fdata-sections -Wl,--gc-sections

逐个解释一下。-Os是优化体积,单片机Flash通常不大,优先考虑省空间。-fno-exceptions关掉异常处理,C++的异常机制会带来不小的代码膨胀,单片机项目基本用不上。-fno-rtti关掉运行时类型识别,同样是为了省空间。-fno-threadsafe-statics关掉静态局部变量的线程安全保护,裸机环境下没有多线程,这个保护纯属浪费。-ffunction-sections和-fdata-sections配合--gc-sections,可以把没用的函数和数据从最终固件里剔除掉,效果非常明显。

我实测过一个STM32F103C8T6的工程,开启这些选项之后,固件从48KB降到了36KB,省了整整12KB。对于只有64KB Flash的芯片来说,这12KB可能就是能不能加一个功能模块的区别。

3. C++在单片机上的语言特性取舍

3.1 哪些特性可以放心用

C++有很多特性,但不是所有都适合在单片机上用。我按照自己的经验,把常用特性分成了三档:放心用、谨慎用、别碰。

放心用的特性包括:类与对象、封装、继承(单继承)、命名空间、函数重载、默认参数、引用、const、内联函数、构造函数与析构函数、运算符重载。这些特性基本上不会带来额外的运行时开销,编译出来的代码和C差不多,但代码组织能力强很多。

举个例子,用类封装一个LED驱动:

class Led { public: Led(GPIO_TypeDef* port, uint16_t pin) : port_(port), pin_(pin) {} void on() { HAL_GPIO_WritePin(port_, pin_, GPIO_PIN_RESET); } void off() { HAL_GPIO_WritePin(port_, pin_, GPIO_PIN_SET); } void toggle() { HAL_GPIO_TogglePin(port_, pin_); } private: GPIO_TypeDef* port_; uint16_t pin_; };

这个类编译出来和直接写C函数几乎没有区别,但用起来清晰多了。你可以定义多个Led对象,每个对象管理一个引脚,代码自解释性很强。

3.2 需要谨慎使用的特性

虚函数、模板、STL容器这些特性,不是不能用,但要看场景。虚函数会引入虚函数表,每个对象会多一个指针的开销,调用时也有间接跳转。如果你只有几个对象,这点开销无所谓。但如果你要创建几百个对象,或者对中断响应时间要求极高,就要慎重了。

模板在单片机上的主要问题是代码膨胀。每实例化一种类型,编译器就会生成一份独立的代码。如果你用模板写了一个通用的环形缓冲区,然后分别用uint8_t、uint16_t、uint32_t实例化,那就会有三份代码。Flash够大的话没问题,Flash紧张的话就要考虑用void*或者联合体来替代。

STL容器我基本不用。std::vector、std::map这些在单片机上要么不支持,要么需要重写内存分配器。std::array可以用,因为它就是原生数组的封装,没有额外开销。std::string就别想了,动态内存分配在单片机上是个敏感话题。

3.3 坚决不能碰的特性

异常处理、RTTI、动态内存分配(new/delete)、多线程相关的库,这些在单片机上基本是禁区。异常处理会让代码体积膨胀很多,而且栈展开的过程在裸机环境下没有操作系统支持,行为不可预测。RTTI同样会增加代码体积。new和delete的问题在于内存碎片,长时间运行之后堆空间会被切得七零八落,最后导致分配失败。

如果你确实需要动态内存,可以用内存池的方式。预先分配一大块静态数组,然后自己实现分配和释放逻辑。这样虽然麻烦一点,但内存行为完全可控。

实操心得:我一般会在工程里定义一个全局的operator new和operator delete,直接把它们删掉或者指向一个内存池。这样万一不小心用了new,编译时就会报错,而不是运行时出问题。

4. 内存管理与资源约束

4.1 栈空间的估算与控制

单片机上的栈空间通常是在启动文件里定义的,比如STM32的startup_stm32f103xb.s里有一行Stack_Size,默认可能是0x400(1KB)。这个大小对于C程序可能够用,但C++的对象如果在栈上创建,尤其是对象比较大的时候,很容易溢出。

我一般的做法是:先估算最深层函数调用链上所有局部变量的大小,然后乘以一个安全系数(通常1.5到2倍)。比如最深层调用链上有5个函数,每个函数平均用100字节的局部变量,那就是500字节,乘以2就是1KB。如果用了递归,那就要格外小心,最好改成迭代。

另外,C++的构造函数和析构函数也会占用栈空间。如果一个对象在栈上创建,构造和析构的时候会有额外的栈帧。所以大对象尽量放在全局区或者用静态分配,不要放在栈上。

4.2 全局对象与静态初始化顺序

C++的全局对象会在main函数之前构造,这个特性在单片机上要特别小心。因为构造顺序是不确定的,如果一个全局对象的构造函数依赖于另一个全局对象,就可能出问题。

更麻烦的是,有些全局对象的构造函数会调用HAL库的函数,比如初始化GPIO。但这时候HAL库本身可能还没初始化,时钟还没使能,结果就是硬件行为异常。

我的建议是:尽量不要定义需要构造函数的全局对象。如果确实需要,用单例模式加显式的init函数,在main函数里手动调用。这样初始化顺序完全可控。

class UartDriver { public: static UartDriver& instance() { static UartDriver inst; return inst; } void init(uint32_t baudrate) { // 初始化代码 } private: UartDriver() = default; };

然后在main函数里:

int main() { HAL_Init(); SystemClock_Config(); UartDriver::instance().init(115200); // ... }

这样虽然多了一行调用,但初始化时机完全由你掌控。

4.3 内存池的简单实现

如果你确实需要动态创建对象,比如一个不定长的命令队列,可以用内存池。下面是一个最简单的固定大小内存池实现:

template<size_t BlockSize, size_t BlockCount> class MemoryPool { public: void* allocate() { for (size_t i = 0; i < BlockCount; i++) { if (!used_[i]) { used_[i] = true; return &pool_[i * BlockSize]; } } return nullptr; } void deallocate(void* ptr) { size_t index = (static_cast<uint8_t*>(ptr) - pool_) / BlockSize; if (index < BlockCount) { used_[index] = false; } } private: alignas(8) uint8_t pool_[BlockSize * BlockCount]; bool used_[BlockCount] = {false}; };

这个内存池在编译期就确定了大小,不会产生碎片,分配和释放都是O(n)的,对于小规模场景完全够用。

5. 外设驱动的C++封装实践

5.1 GPIO与中断的类封装

用C++封装GPIO驱动,核心思路是把每个外设抽象成一个类,把寄存器操作封装在成员函数里。但中断服务函数是个特殊的存在,它必须是C链接的全局函数,不能是类的成员函数。

我的做法是:类里定义一个静态的回调函数指针,中断服务函数里调用这个指针。这样既保持了C++的封装性,又满足了中断向量的要求。

class ExtiButton { public: using Callback = void(*)(void); ExtiButton(uint16_t pin, Callback cb) : pin_(pin), callback_(cb) {} void enable() { // 配置中断 callback_ = callback_; } static void irqHandler() { if (activeInstance_ && activeInstance_->callback_) { activeInstance_->callback_(); } } private: uint16_t pin_; Callback callback_; static ExtiButton* activeInstance_; };

然后在stm32f1xx_it.c里:

void EXTI0_IRQHandler(void) { ExtiButton::irqHandler(); HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0); }

这种模式在按键、编码器、外部触发信号等场景下非常好用。

5.2 串口通信的环形缓冲区设计

串口是单片机最常用的外设之一。用C++封装串口,重点在于接收缓冲区的设计。我一般会用环形缓冲区,配合DMA或者中断接收。

class RingBuffer { public: RingBuffer(uint8_t* buf, size_t size) : buf_(buf), size_(size), head_(0), tail_(0) {} bool push(uint8_t data) { size_t next = (head_ + 1) % size_; if (next == tail_) return false; buf_[head_] = data; head_ = next; return true; } bool pop(uint8_t& data) { if (head_ == tail_) return false; data = buf_[tail_]; tail_ = (tail_ + 1) % size_; return true; } size_t available() const { return (head_ - tail_ + size_) % size_; } private: uint8_t* buf_; size_t size_; volatile size_t head_; volatile size_t tail_; };

注意head_和tail_要加volatile,因为它们在中断和主循环里都会被访问。这个缓冲区的大小要根据你的最大帧长和波特率来定。比如波特率115200,最大帧长64字节,那缓冲区至少128字节比较稳妥。

5.3 定时器与PWM的面向对象封装

定时器和PWM的封装思路类似。把定时器的周期、分频、通道、占空比这些参数封装成类的成员,通过方法调用来修改。

class PwmChannel { public: PwmChannel(TIM_HandleTypeDef* htim, uint32_t channel) : htim_(htim), channel_(channel) {} void setDuty(float duty) { if (duty < 0.0f) duty = 0.0f; if (duty > 1.0f) duty = 1.0f; uint32_t compare = static_cast<uint32_t>( duty * __HAL_TIM_GET_AUTORELOAD(htim_)); __HAL_TIM_SET_COMPARE(htim_, channel_, compare); } void start() { HAL_TIM_PWM_Start(htim_, channel_); } void stop() { HAL_TIM_PWM_Stop(htim_, channel_); } private: TIM_HandleTypeDef* htim_; uint32_t channel_; };

用的时候直接:

PwmChannel ledPwm(&htim2, TIM_CHANNEL_1); ledPwm.start(); ledPwm.setDuty(0.5f);

比直接操作寄存器或者HAL宏清晰多了。

6. 常见问题与排查技巧实录

6.1 编译链接阶段的典型错误

用C++写单片机代码,编译链接阶段最容易遇到两类问题:符号找不到和代码超限。

符号找不到通常是因为C和C++的混合编译。C++编译器会对函数名进行名称修饰,而C编译器不会。所以如果你在C++文件里调用了一个C文件里的函数,必须用extern "C"声明。

extern "C" { #include "legacy_driver.h" }

反过来,如果你在C文件里调用C++的函数,那个C++函数也必须用extern "C"修饰。

代码超限的问题,首先要检查是不是开了异常和RTTI。如果已经关了,那就要看是不是模板实例化太多,或者虚函数表太大。可以用arm-none-eabi-size命令查看各个段的大小,用arm-none-eabi-nm命令查看符号大小,找出占空间最多的函数。

6.2 运行时异常与HardFault排查

HardFault是单片机开发中最让人头疼的问题之一。用C++的时候,HardFault的原因通常有几个:栈溢出、空指针调用、数组越界、虚函数表被破坏。

排查HardFault的第一步是找到出错的位置。可以在HardFault_Handler里加一段汇编,把压栈的寄存器读出来,然后根据LR的值判断是从哪里跳过来的。更简单的办法是用调试器,在HardFault_Handler里打断点,然后看调用栈。

我遇到过一次典型的栈溢出,原因是定义了一个大数组作为局部变量。那个数组有2KB,而栈总共才1KB。编译器不会报错,运行时直接HardFault。后来把数组改成static,问题就解决了。

避坑技巧:在启动文件里把栈大小改大一些,比如从0x400改成0x800。多出来的1KB Flash开销,换来的是更少的调试时间,很划算。

6.3 外设初始化顺序导致的诡异问题

C++的全局对象构造顺序不确定,这个特性在单片机上有时候会导致很诡异的问题。比如你定义了两个全局对象,一个依赖另一个,但构造顺序反了,运行时就会出错。

还有一种情况是全局对象的构造函数里调用了HAL_Delay,但这时候SysTick还没配置好,结果就是死循环。

我的建议是:所有需要硬件初始化的操作,都放在main函数里显式调用,不要放在全局对象的构造函数里。全局对象只做数据成员的初始化,不做硬件操作。

6.4 常见问题速查表

现象可能原因排查方法解决方案
编译报undefined referenceC/C++混合编译缺少extern "C"检查头文件是否被extern "C"包裹添加extern "C"声明
固件超出Flash容量异常/RTTI未关闭,模板膨胀用size命令查看各段大小关闭异常和RTTI,减少模板实例化
运行一段时间后死机栈溢出或堆碎片检查栈使用量,检查是否有动态分配增大栈空间,改用内存池
HardFault空指针、数组越界、虚表损坏调试器查看调用栈加断言,检查数组边界
中断响应变慢虚函数调用或长临界区测量中断延迟中断里避免虚函数调用,缩短临界区
全局对象构造异常构造顺序不确定检查全局对象依赖关系改用显式init函数

7. 从C到C++的渐进式迁移策略

7.1 先封装再重构

如果你手上已经有一个用C写的单片机工程,想迁移到C++,不要想着一次性全部重写。我的建议是渐进式迁移,先从外设驱动开始封装,再逐步重构上层逻辑。

第一步,把main.c改成main.cpp,确保能编译通过。这一步可能会遇到extern "C"的问题,逐个解决就行。

第二步,选一个最简单的外设,比如LED或者按键,用类封装起来。封装完之后,在main函数里用新类替换原来的C代码,测试功能是否正常。

第三步,把串口、定时器、ADC这些外设也逐步封装。每封装一个,就替换一个,确保每一步都是可测试、可回退的。

第四步,重构上层逻辑。把状态机、菜单系统、数据处理这些用C++的类重新组织。这一步工作量最大,但收益也最明显。

7.2 中断服务函数的处理

中断服务函数是C和C++混合编程的一个关键点。在STM32的启动文件里,中断向量表定义的是C函数名。所以中断服务函数必须是C链接的全局函数。

我的做法是:在C++文件里定义中断服务函数,用extern "C"修饰。然后在类里定义一个静态的handler函数,中断服务函数调用这个handler。

extern "C" void TIM2_IRQHandler(void) { TimerManager::handleIrq(TIM2); HAL_TIM_IRQHandler(&htim2); }

这样中断向量表能找到函数,类里的逻辑也能正常执行。

7.3 与现有C库的兼容

单片机开发中经常会用到厂商提供的C库,比如STM32的HAL库、标准外设库。这些库都是C写的,在C++里调用需要extern "C"。

通常厂商的头文件里已经加了条件编译,如果是C++编译器就自动加extern "C"。但有些老版本的库没有加,那就需要手动包裹。

extern "C" { #include "stm32f1xx_hal.h" }

另外,C库里的回调函数指针类型,在C++里赋值的时候要注意类型匹配。C++对函数指针的类型检查比C严格,有时候需要显式转换。

8. 实战案例:用C++重写一个数据采集器

8.1 项目背景与需求

我之前做过一个数据采集器,硬件平台是STM32F103C8T6,外接一个ADS1115做16位ADC采集,一个OLED显示屏做本地显示,一个串口模块做数据上传。原来的C代码大概有2000行,状态机写得很乱,改一个功能要动好几个文件。

后来我用C++重写了一遍,代码量降到1400行左右,而且结构清晰了很多。下面我把关键部分的设计思路分享一下。

8.2 模块划分与类设计

整个项目分成几个模块:ADC采集、OLED显示、串口通信、数据缓存、主状态机。每个模块一个类,类之间通过接口交互。

ADC采集类负责配置ADS1115,读取原始值,转换成电压值。OLED显示类负责初始化屏幕,刷新显示内容。串口通信类负责收发数据,解析命令。数据缓存类负责存储采集到的数据,支持循环覆盖。主状态机类负责协调各个模块,处理用户输入。

class DataCollector { public: DataCollector() : adc_(&hi2c1, 0x48), display_(&hi2c1, 0x3C), uart_(&huart1), running_(false) {} void init() { adc_.init(); display_.init(); uart_.init(115200); display_.showWelcome(); } void run() { running_ = true; while (running_) { processCommand(); if (adc_.isDataReady()) { float voltage = adc_.readVoltage(); buffer_.push(voltage); display_.update(voltage); } } } private: void processCommand() { uint8_t cmd; if (uart_.readByte(cmd)) { switch (cmd) { case 'S': running_ = false; break; case 'D': dumpData(); break; default: break; } } } Ads1115 adc_; OledDisplay display_; UartDriver uart_; RingBuffer buffer_; bool running_; };

这个结构比原来的C代码清晰太多了。每个类的职责单一,修改一个模块不会影响其他模块。

8.3 关键代码片段与说明

ADC采集类的核心是配置寄存器和读取转换结果。ADS1115的配置寄存器是16位的,需要按照数据手册的位定义来设置。

class Ads1115 { public: Ads1115(I2C_HandleTypeDef* hi2c, uint8_t addr) : hi2c_(hi2c), addr_(addr << 1) {} void init() { uint16_t config = 0x8583; writeRegister(0x01, config); } float readVoltage() { uint16_t raw = readRegister(0x00); return raw * 4.096f / 32768.0f; } private: void writeRegister(uint8_t reg, uint16_t value) { uint8_t data[3] = {reg, (uint8_t)(value >> 8), (uint8_t)(value & 0xFF)}; HAL_I2C_Master_Transmit(hi2c_, addr_, data, 3, 100); } uint16_t readRegister(uint8_t reg) { uint8_t data[2]; HAL_I2C_Master_Transmit(hi2c_, addr_, &reg, 1, 100); HAL_I2C_Master_Receive(hi2c_, addr_, data, 2, 100); return (data[0] << 8) | data[1]; } I2C_HandleTypeDef* hi2c_; uint8_t addr_; };

这里的0x8583是根据数据手册算出来的配置值,含义是:单次转换、AIN0对GND、增益±4.096V、128SPS、关闭比较器。具体怎么算的,数据手册里都有,我这里就不展开了。

8.4 编译优化与体积对比

重写完成之后,我对比了一下C版本和C++版本的固件大小。C版本是42KB,C++版本是38KB。C++版本反而更小,原因是C++的封装让一些重复代码被内联优化掉了,而且我用了一些模板技巧来减少重复的寄存器操作代码。

运行效率方面,我用示波器测了一下主循环的周期,C版本大概是120微秒,C++版本是125微秒,差距在5%以内。对于这个应用来说,完全可以接受。

9. 工具链与调试技巧补充

9.1 用VSCode调试C++单片机工程

VSCode配合Cortex-Debug插件,可以实现在线调试,打断点、看变量、单步执行都没问题。配置的关键是launch.json文件。

{ "version": "0.2.0", "configurations": [ { "name": "Debug (OpenOCD)", "type": "cortex-debug", "request": "launch", "servertype": "openocd", "cwd": "${workspaceRoot}", "executable": "build/your_project.elf", "device": "STM32F103C8", "configFiles": [ "interface/stlink.cfg", "target/stm32f1x.cfg" ] } ] }

这个配置需要OpenOCD和ST-Link驱动。OpenOCD的路径要在系统PATH里,或者用fullVersion指定绝对路径。

调试C++代码的时候,虚函数调用、模板实例化这些在调试器里看起来可能有点绕。我的建议是:调试阶段可以暂时关掉优化(-O0),这样变量不会被优化掉,单步执行也更符合直觉。发布的时候再开-Os。

9.2 用静态分析工具提前发现问题

C++的语法比C复杂,有些问题编译器不会报错,但运行时会出问题。这时候可以用静态分析工具,比如cppcheck。

cppcheck --enable=all --inconclusive --std=c++11 src/

cppcheck可以检查出未初始化的变量、数组越界、内存泄漏、未使用的函数等问题。我在工程里集成过cppcheck,每次提交代码前跑一遍,确实能提前发现不少隐患。

另外,编译器本身的警告也要重视。把-Wall -Wextra打开,把警告当错误处理(-Werror),虽然一开始会很不习惯,但长期来看能避免很多低级错误。

9.3 版本管理与团队协作建议

单片机项目的版本管理,我推荐用Git。但要注意,编译生成的中间文件(.o、.elf、.hex、.map)不要提交到仓库里,用.gitignore排除掉。

对于C++工程,头文件和源文件要分目录存放。我一般的结构是:

project/ ├── src/ │ ├── main.cpp │ ├── drivers/ │ ├── modules/ │ └── utils/ ├── inc/ │ ├── drivers/ │ ├── modules/ │ └── utils/ ├── lib/ │ └── third_party/ ├── build/ ├── .gitignore └── Makefile

这种结构清晰,找文件方便,也便于多人协作。

10. 一些个人体会与后续扩展方向

用C++写单片机代码,最大的收获不是语言本身,而是思维方式的变化。C语言面向过程,你思考的是“先做什么,再做什么”。C++面向对象,你思考的是“有哪些对象,它们之间怎么交互”。这种思维方式的转变,让代码的组织方式完全不同。

我现在的习惯是:拿到一个新项目,先在纸上画模块框图,确定有哪些类,每个类的职责是什么,类之间怎么通信。这个设计过程可能花半天时间,但能省下后面好几天的调试时间。

后续如果继续深入,有几个方向可以探索。一是用C++的模板元编程做一些编译期计算,比如查表、CRC校验,把运行时的开销转移到编译期。二是研究一下嵌入式领域的C++框架,比如ETL(Embedded Template Library),它提供了一套适合嵌入式的容器和算法,不用动态内存,完全静态分配。三是把单元测试引入到单片机开发中,用Google Test或者Catch2在PC上测试纯逻辑代码,硬件相关的部分用mock对象替代。

这些方向我还在摸索中,等有成熟的经验了再整理出来分享。如果你也在单片机上用C++,欢迎交流踩过的坑和总结的技巧。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询