当前位置: 首页 > article >正文

嵌入式C++开发第17篇:C++23特性收尾 —— 属性、链接与零开销抽象的最终证明

嵌入式C开发第17篇C23特性收尾 —— 属性、链接与零开销抽象的最终证明仓库已经开源仍然在持续建设中喜欢的话点个⭐相关的链接如下https://github.com/Awesome-Embedded-Learning-Studio/Tutorial_AwesomeModernCPP静态网页直接阅览https://awesome-embedded-learning-studio.github.io/Tutorial_AwesomeModernCPP/承接四次重构完成了代码跑起来了。这一篇我们把散落在各处的C特性集中梳理一遍然后做最终的性能验证。每个特性都不是花里胡哨的语法糖——它们在嵌入式开发中都有实际的意义。最后的分析是笔者自己的电脑看到的汇编码。建议是自己本地查看看看自己的机器的表现效果如何这篇文章本身也是LED篇的最后一篇笔者目前正在积极的重构主线C教程争取带来更多的泛领域的C开发内容[[nodiscard]]——不允许忽略的返回值clock.h中有一个看起来很特别的函数声明[[nodiscard(You should accept the clock frequency, its what you request!)]]uint64_tclock_freq()constnoexcept;[[nodiscard]]告诉编译器这个函数的返回值不应该被丢弃。如果有人写了clock.clock_freq();而没有使用返回值编译器会发出警告。C23增强了[[nodiscard]]允许你附加一个字符串信息。当警告触发时编译器会显示你写的消息——这里写的是你拿到了时钟频率请使用它比一个冷冰冰的warning: ignoring return value有用得多。为什么这个特性在嵌入式开发中特别重要考虑HAL库的函数签名HAL_StatusTypeDef HAL_RCC_OscConfig(RCC_OscInitTypeDef *RCC_OscInitStruct)和HAL_StatusTypeDef HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init)。这些函数都返回状态码。如果你不检查返回值可能忽略了硬件配置失败的错误——LED不亮你到处排查最后发现是时钟配置参数写错了但HAL已经通过返回值告诉过你了只是你没看。在我们的clock.cpp中正确地检查了返回值constautoresultHAL_RCC_OscConfig(osc);if(result!HAL_OK){system::dead::halt(Clock Configurations Failed);}如果HAL的API都标上[[nodiscard]]这类低级错误在编译时就能被捕获。[[noreturn]]——永不返回的函数// system/dead.hpp[[noreturn]]inlinevoidhalt(constchar*raw_message[[maybe_unused]]){while(1){}}[[noreturn]]告诉编译器这个函数永远不会返回到调用者。编译器会利用这个信息做两件事。第一是优化。如果编译器知道halt()不会返回它就不需要在halt()调用之后生成任何清理代码。在clock.cpp中halt()被用在if分支里if(result!HAL_OK){system::dead::halt(Clock Configurations Failed);}// 编译器知道如果执行到了halt()就不会到达这里// 所以不需要在if之后生成函数可能没有返回值的警告第二是消除假警告。如果没有[[noreturn]]编译器可能会警告函数可能在某些路径上没有返回值——因为它不知道halt()之后的代码是不可达的。加上[[noreturn]]后编译器理解控制流不会继续警告自然消失。[[maybe_unused]]——预留但未使用的参数halt()函数有一个const char* raw_message参数但当前实现只有while(1) {}死循环——根本没有使用这个参数。编译器会发出未使用的参数警告。[[maybe_unused]]告诉编译器我知道它没被使用这是故意的。这个参数是为将来扩展预留的。也许某天我们会在halt()里通过UART输出错误信息或者点亮一个错误指示灯。保留参数但标记为我知道它没被使用是好的工程实践——比删除参数以后再加回来要好得多。extern “C”——C和C和平共处的桥梁我们的项目中有多个地方出现了extern C// gpio.hppexternC{#includestm32f1xx_hal.h}// clock.cppexternC{#includestm32f1xx_hal.h}// main.cppexternC{#includestm32f1xx_hal.h}为什么需要这样做原因是C和C的函数名称修饰name mangling规则不同。在C语言中函数HAL_GPIO_Init在目标文件中的符号名就是HAL_GPIO_Init。但在C中编译器会把函数名修饰成包含参数类型信息的符号名比如_Z12HAL_GPIO_InitP11GPIO_TypeDefP15GPIO_InitTypeDef。这种修饰使得C支持函数重载——多个同名但参数不同的函数。问题在于HAL库是用C编译器编译的它的目标文件中函数符号是C风格的名称。如果C编译器去找修饰后的名称链接器会报undefined reference——因为你找的名字不存在。extern C告诉C编译器这个头文件里声明的所有函数请用C的名称规则来找它们。这样链接时编译器就会找HAL_GPIO_Init而不是修饰后的名称。还有一个关键的地方——hal_mock.cvoidSysTick_Handler(void){HAL_IncTick();}SysTick_Handler是中断向量表中的函数名。硬件复位后当SysTick中断触发时CPU会跳转到向量表中记录的SysTick_Handler地址。这个查找过程使用的是C链接的符号名——所以SysTick_Handler必须用C链接规则定义。如果它在.cpp文件中定义必须用extern C包裹否则名称修饰后的符号在向量表中找不到。noexcept——嵌入式中的异常承诺// gpio.hppstaticconstexprGPIO_TypeDef*native_port()noexcept{...}// clock.huint64_tclock_freq()constnoexcept;noexcept承诺函数不会抛出异常。在我们的项目中这是自然的保证——因为CMakeLists.txt中指定了-fno-exceptionsadd_compile_options( $$COMPILE_LANGUAGE:CXX:-fno-exceptions $$COMPILE_LANGUAGE:CXX:-fno-rtti )-fno-exceptions在编译层面禁用了C异常。任何throw语句都会导致编译错误。所以我们的代码物理上不可能抛出异常。那么为什么还要显式写noexcept第一是文档作用。noexcept告诉阅读代码的人这个函数不会抛异常——在嵌入式环境中这是重要的信息。第二是编译器优化。即使异常被禁用了noexcept仍然可以帮助编译器生成更紧凑的代码——它不需要生成栈展开stack unwinding相关的数据。在64KB Flash的STM32F103C8T6上每一点空间都很宝贵。-fno-rtti也值得一提RTTIRun-Time Type Information是C的运行时类型识别机制dynamic_cast、typeid等。禁用RTTI可以节省Flash空间因为不需要存储类型信息表。我们的代码中没有使用dynamic_cast——所有的类型多态都是通过模板在编译时实现的。聚合初始化——确保结构体从零开始// gpio.hppGPIO_InitTypeDef init_types{};// C风格的值初始化// clock.cppRCC_OscInitTypeDef osc{0};// C风格的零初始化RCC_ClkInitTypeDef clk{0};两种写法效果相同将结构体的所有字节清零。区别在于{}是C11引入的值初始化语法{0}是C语言的传统写法。在嵌入式开发中初始化结构体至关重要——未初始化的Speed字段可能包含垃圾值导致引脚以不可预测的速度运行。⚠️ 注意在嵌入式C中未初始化的变量是最大的bug来源之一。栈上的局部变量如果没有初始化它们的值取决于栈帧上一次使用时残留的数据——这就是未定义行为。GPIO_InitTypeDef init{}这种写法确保所有字节为零消除了这种风险。如果你看到有人写GPIO_InitTypeDef init;没有{}那就是一个定时炸弹——在调试模式下可能碰巧工作正常Release优化后行为就变了。纸上得来终觉浅。与其口头宣称零开销不如直接看编译器生成的机器码。以下所有汇编均来自本教程配套工程的实际编译输出arm-none-eabi-g -O2 -mcpucortex-m3 -mthumb -stdgnu23。C 模板版本源代码main.cpp中的调用方式device::LEDdevice::gpio::GpioPort::C,GPIO_PIN_13led;// ...led.on();// 点亮led.off();// 熄灭LED::on()和LED::off()在main()中编译生成的 Thumb-2 汇编如下; led.on() → 编译器将模板参数全部在编译期折叠为立即数 8000164: movs r2, #1 ; GPIO_PIN_SET 1 8000166: mov.w r1, #8192 ; GPIO_PIN_13 0x2000 800016a: ldr r0, [pc, #16] ; GPIOC 基地址 0x40011000 800016c: bl 8000564 ; 调用 HAL_GPIO_WritePin ; led.off() → 仅 r2 的立即数不同 8000150: movs r2, #0 ; GPIO_PIN_RESET 0 8000152: mov.w r1, #8192 ; GPIO_PIN_13 0x2000 8000156: ldr r0, [pc, #36] ; GPIOC 基地址 0x40011000 8000158: bl 8000564 ; 调用 HAL_GPIO_WritePin注意三件事LEVEL ActiveLevel::Low ? ... : ...这个三元表达式在编译期已求值完毕运行时完全不存在模板参数GpioPort::C地址0x40011000和GPIO_PIN_130x2000都被编译器直接编码为立即数——没有任何间接寻址开销on()和off()各只占4 条指令8 字节且仅立即数r2不同HAL_GPIO_WritePin 的实现上面两个调用最终都进入HAL_GPIO_WritePin它本身只有4 条指令、8 字节08000564 HAL_GPIO_WritePin: 8000564: cbnz r2, 8000568 ; r2 ! 0 (SET)? 跳过移位 8000566: lsls r1, r1, #16 ; r2 0 (RESET): 引脚号左移 16 位 8000568: str r1, [r0, #16] ; 写入 GPIOx-BSRR (偏移 0x10) 800056a: bx lr ; 返回工作原理STM32 的 BSRR 寄存器高 16 位用于复位清零引脚低 16 位用于置位拉高引脚。cbnz检查r2PinState如果为RESET0就把引脚号左移 16 位写入 BSRR 高半部分完成复位如果为SET1直接写入低半部分完成置位。一条str指令完成原子操作——不需要读-改-写。对比C 宏版本会生成什么如果用传统 C 宏写法#defineLED_ON()HAL_GPIO_WritePin(GPIOC,GPIO_PIN_13,GPIO_PIN_RESET)#defineLED_OFF()HAL_GPIO_WritePin(GPIOC,GPIO_PIN_13,GPIO_PIN_SET)预处理器展开后编译器看到的代码与上面 C 模板版本生成的内容完全一致加载三个参数GPIOC 地址、引脚号、状态到r0/r1/r2然后bl调用HAL_GPIO_WritePin。没有任何额外指令。资源消耗一览整个程序的 Flash 占用段大小.text代码 只读数据2992 字节.data已初始化全局变量12 字节.bss零初始化全局变量8 字节STM32F103C8T6 拥有 64KB Flash、20KB SRAM。上面的 LED 闪烁程序只占用了4.6%的 Flash 空间——其中绝大部分是 HAL 库本身和中断向量表C 模板抽象带来的额外代码量为零。这就是零开销抽象你用 C 的高级抽象模板、enum class、constexpr写了更安全、更可维护的代码但最终生成的机器码与手写 C 代码完全一致。模板的代价只体现在编译时间上编译器需要为每个不同的模板参数组合生成一份代码。但这个代价是在开发机上付出的不是在 STM32 的 64KB Flash 上。我们回头看所有C23特性讲完了零开销抽象也验证了。回顾一下我们用到的全部特性enum class带底层类型——类型安全的GPIO配置常量static_cast——零开销的枚举到整数转换非类型模板参数NTTP——编译时绑定端口和引脚constexpr——编译时求值的地址转换if constexpr——编译时自动选择时钟使能宏[[nodiscard]]带自定义消息——防止忽略重要返回值[[noreturn]]——永不返回函数的优化提示[[maybe_unused]]——预留但未使用的参数标记noexcept——异常禁用环境下的文档和优化extern C——C和C互操作的桥梁聚合初始化{}——确保结构体从零开始每一个特性都有明确的为什么在嵌入式中有用。这不是炫技——这是在资源受限的环境中用编译器的能力替代人脑的记忆和 vigilance。下一篇常见坑位汇总和三个实战练习——把LED玩出花样来。相关阅读第15篇第三次重构 —— if constexpr让时钟使能在编译时自动选对 - 相似度 100%

相关文章:

嵌入式C++开发第17篇:C++23特性收尾 —— 属性、链接与零开销抽象的最终证明

嵌入式C开发第17篇:C23特性收尾 —— 属性、链接与零开销抽象的最终证明 仓库已经开源!仍然在持续建设中,喜欢的话点个⭐!相关的链接如下:https://github.com/Awesome-Embedded-Learning-Studio/Tutorial_AwesomeModer…...

4N80-ASEMI功率电子领域的能效标杆4N80

编辑:LL4N80-ASEMI功率电子领域的能效标杆4N80型号:4N80品牌:ASEMI沟道:NPN封装:TO-220F漏源电流:4A漏源电压:800VRDS(on):3.8Ω批号:最新引脚数量:3封装尺寸&#xff1a…...

终极色彩校准指南:如何用novideo_srgb解决NVIDIA显卡色彩过饱和问题

终极色彩校准指南:如何用novideo_srgb解决NVIDIA显卡色彩过饱和问题 【免费下载链接】novideo_srgb Calibrate monitors to sRGB or other color spaces on NVIDIA GPUs, based on EDID data or ICC profiles 项目地址: https://gitcode.com/gh_mirrors/no/novide…...

第八章:vue性能优化与最佳实践

核心目标:将应用性能提升至极致。掌握从打包体积到渲染流畅度的全方位优化技巧,确保应用在各种低功耗设备上也能秒开且丝滑运行。 📋 本章核心知识点 知识点说明难度性能指标LCP, FID, CLS 是什么⭐⭐虚拟列表处理万级数据的标准方案⭐⭐⭐懒…...

AI Agent崛起:从对话到行动,解锁智能体时代!

AI Agent作为大模型应用落地的关键范式,具备感知、推理、工具使用与自主迭代能力。本文系统梳理了AI Agent的核心架构、能力体系与发展脉络,阐述了从ReAct开创闭环范式到协议层成熟的演进过程。一个成熟的Agent采用ModelHarness的双层架构,具…...

Reference Extractor:如何从已丢失的文档中找回宝贵参考文献?

Reference Extractor:如何从已丢失的文档中找回宝贵参考文献? 【免费下载链接】ref-extractor Reference Extractor - Extract Zotero/Mendeley references from Microsoft Word files 项目地址: https://gitcode.com/gh_mirrors/re/ref-extractor …...

别再乱用MC_Power了!CodeSys轴控指令Enable和bRegulatorOn的正确操作顺序(附避坑案例)

CodeSys轴控指令MC_Power的深度解析与安全实践 在工业自动化领域,伺服控制系统的稳定性和安全性至关重要。作为CodeSys平台中最基础的轴控指令之一,MC_Power的正确使用往往被工程师们低估。许多项目现场出现的"幽灵使能"现象——明明已经发出…...

告别硬件SPI引脚冲突:用STM32任意GPIO软件模拟SPI驱动RC522的避坑指南

STM32软件模拟SPI驱动RC522:突破硬件限制的实战指南 1. 为什么需要软件模拟SPI? 在嵌入式开发中,硬件资源冲突是开发者经常面临的棘手问题。想象一下这样的场景:你的STM32项目已经使用了SPI1接口连接TFT屏幕,SPI2接口连…...

DownKyi终极指南:5步掌握B站8K超高清视频下载的完整方法

DownKyi终极指南:5步掌握B站8K超高清视频下载的完整方法 【免费下载链接】downkyi 哔哩下载姬downkyi,哔哩哔哩网站视频下载工具,支持批量下载,支持8K、HDR、杜比视界,提供工具箱(音视频提取、去水印等&…...

别再对着手册发愁了!STM32驱动ADS1115的完整代码与配置详解(附避坑点)

STM32驱动ADS1115实战指南:从寄存器配置到避坑全解析 1. 硬件连接与基础配置 在开始编写代码之前,确保你的硬件连接正确无误。ADS1115模块与STM32之间通过I2C接口通信,典型的连接方式如下: SCL:连接STM32的I2C时钟线&a…...

c语言课程设计总结

c语言课程设计总结 篇1 回顾起此次课程设计,至今我仍感慨颇多,的确,在这些日子,能够学到很多很多的的东西,同时不仅仅能够巩固了以前所学过的知识,而且学到了很多在书本上所没有学到过的知识。虽然我的这个…...

OBS背景移除插件终极指南:无需绿幕打造专业直播效果

OBS背景移除插件终极指南:无需绿幕打造专业直播效果 【免费下载链接】obs-backgroundremoval An OBS plugin for removing background in portrait images (video), making it easy to replace the background when recording or streaming. 项目地址: https://gi…...

为什么你的文章没人读?聊聊文章可读性

文章可读性不是“写得简单”就完事我以前以为,只要把字写短一点、句子弄直白点,别人就能轻松看懂我的文章。后来才发现,事情没那么简单。文章可读性其实不只是关于词汇难易或句子长短,它更像是一种“读者友好度”——你有没有站在…...

告别玄学调试:深入Linux休眠机制,解决SAR Sensor在口袋中的唤醒与功率控制难题

告别玄学调试:深入Linux休眠机制,解决SAR Sensor在口袋中的唤醒与功率控制难题 当你的手机滑入口袋时,系统进入深度休眠以节省电量,但此时一个关键问题浮现:如何确保SAR Sensor(特定吸收率传感器&#xff0…...

Element-UI中el-switch的@change事件传参踩坑记:如何同时获取开关状态和自定义标识

Element-UI中el-switch事件传参实战:多开关场景下的精准控制方案 在Vue.jsElement-UI的中后台系统开发中,el-switch组件因其简洁直观的交互体验而广受欢迎。但当页面出现多个开关组件需要共享同一个回调函数时,开发者往往会陷入一个典型困境—…...

Avue表单进阶玩法:手把手教你用slot自定义日期选择器和批量操作菜单

Avue表单进阶玩法:手把手教你用slot自定义日期选择器和批量操作菜单 在Vue生态中,Avue作为一款高效的前端开发框架,其表单组件因其开箱即用的特性广受开发者喜爱。但当项目需求超出默认组件能力范围时,如何优雅地扩展功能成为关键…...

如何5步搞定RTAB-Map多相机视觉对齐:新手的完整实战指南

如何5步搞定RTAB-Map多相机视觉对齐:新手的完整实战指南 【免费下载链接】rtabmap RTAB-Map library and standalone application 项目地址: https://gitcode.com/gh_mirrors/rt/rtabmap RTAB-Map是一个强大的实时定位与建图开源库,特别擅长处理多…...

二维码修复新方案:QrazyBox如何拯救损坏的二维码

二维码修复新方案:QrazyBox如何拯救损坏的二维码 【免费下载链接】qrazybox QR Code Analysis and Recovery Toolkit 项目地址: https://gitcode.com/gh_mirrors/qr/qrazybox 你是否曾遇到过这样的情况:打印出来的会议签到二维码模糊不清&#xf…...

Flutter音频开发避坑指南:just_audio插件在iOS/Android平台上的常见问题与解决方案

Flutter音频开发避坑指南:just_audio插件在iOS/Android平台上的常见问题与解决方案 在跨平台音频开发领域,Flutter的just_audio插件因其简洁的API和强大的功能而备受青睐。然而,正如许多开发者所经历的那样,当项目从Demo阶段迈向生…...

2025最权威的AI辅助写作平台实测分析

Ai论文网站排名(开题报告、文献综述、降aigc率、降重综合对比) TOP1. 千笔AI TOP2. aipasspaper TOP3. 清北论文 TOP4. 豆包 TOP5. kimi TOP6. deepseek 1. 在内容创作里头,降低人工智能生成内容所占比例,也就是降低AIGC率&…...

LILYGO T-FPGA开发套件:ESP32-S3与FPGA协同开发指南

1. LILYGO T-FPGA开发套件概览LILYGO T-FPGA开发套件是一款将ESP32-S3无线微控制器与Gowin GW1NSR-4C FPGA集成在一起的创新硬件平台。这个M.2规格的模块设计非常巧妙,通过标准接口可以轻松插入配套的扩展底板,为开发者提供了完整的物联网可编程逻辑开发…...

快速上手OpenVINO AI音频插件:从安装到实战

快速上手OpenVINO AI音频插件:从安装到实战 【免费下载链接】openvino-plugins-ai-audacity A set of AI-enabled effects, generators, and analyzers for Audacity. 项目地址: https://gitcode.com/gh_mirrors/op/openvino-plugins-ai-audacity OpenVINO™…...

Docker低代码配置安全红线(CNCF认证工程师紧急预警:3个高危默认值正在泄露你的K8s集群)

第一章:Docker低代码配置安全红线全景图在低代码平台日益集成容器化能力的今天,Docker 配置正悄然成为安全防线中最易被忽视的薄弱环节。大量可视化编排工具自动生成 docker-compose.yml 或封装 Dockerfile 模板,却常默认启用高危选项——如特…...

别再空谈概念了!用Python+Unity3D,手把手教你搭建一个简易的智慧交通数字孪生Demo

用PythonUnity3D实战:从零构建智慧交通数字孪生系统 十字路口的红绿灯交替闪烁,车流如织——这个再普通不过的交通场景,正成为城市管理的痛点。传统交通仿真往往停留在二维图表阶段,而今天我们尝试用Python处理实时数据流&#x…...

仅剩3%团队真正启用镜像签名!深度拆解Docker Content Trust弃用后,Cosign替代方案的5层可信验证架构

第一章:Docker镜像签名的现状与信任危机在容器化生产环境中,Docker镜像已成为软件分发的事实标准。然而,镜像来源不可信、中间人篡改、供应链投毒等事件频发,暴露出签名机制在实际落地中的严重断层。尽管Docker Content Trust&…...

从动态规划到DTW:一个Python可视化教程,带你亲手画出时间规整路径图

从动态规划到DTW:一个Python可视化教程,带你亲手画出时间规整路径图 在信号处理和机器学习领域,时间序列的相似性比较是一个基础但极具挑战性的问题。想象一下,当你需要比较两段语音、心电图或股票走势时,简单的逐点对…...

从‘调参噩梦’到‘一键收敛’:全局快速Terminal滑模控制参数整定心得分享

从‘调参噩梦’到‘一键收敛’:全局快速Terminal滑模控制参数整定实战指南 滑模控制工程师的日常,往往始于理论推导的兴奋,终于参数调试的崩溃。当你在Simulink里反复拖动α、β、p、q的滑块,看着仿真曲线在发散与抖振之间反复横跳…...

Face3D.ai Pro使用技巧:掌握这几点,让你的3D重建效果提升一个档次

Face3D.ai Pro使用技巧:掌握这几点,让你的3D重建效果提升一个档次 1. 为什么你的3D重建效果不够理想? 1.1 输入照片的质量决定重建上限 Face3D.ai Pro虽然强大,但"垃圾进、垃圾出"的原则依然适用。经过上百次测试&am…...

ChemCrow实战指南:用AI大模型解决复杂化学问题的终极方案

ChemCrow实战指南:用AI大模型解决复杂化学问题的终极方案 【免费下载链接】chemcrow-public Chemcrow 项目地址: https://gitcode.com/gh_mirrors/ch/chemcrow-public 你是否曾为复杂的化学计算感到头疼?需要计算分子量、预测反应产物&#xff0c…...

2026年云端新手步骤:如何安装OpenClaw?Coding Plan配置及大模型API Key接入

2026年云端新手步骤:如何安装OpenClaw?Coding Plan配置及大模型API Key接入。OpenClaw(前身为Clawdbot/Moltbot)作为开源、本地优先的AI助理框架,凭借724小时在线响应、多任务自动化执行、跨平台协同等核心能力&#x…...