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

news2026/4/28 2:03:51
嵌入式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%

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2543449.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SpringBoot-17-MyBatis动态SQL标签之常用标签

文章目录 1 代码1.1 实体User.java1.2 接口UserMapper.java1.3 映射UserMapper.xml1.3.1 标签if1.3.2 标签if和where1.3.3 标签choose和when和otherwise1.4 UserController.java2 常用动态SQL标签2.1 标签set2.1.1 UserMapper.java2.1.2 UserMapper.xml2.1.3 UserController.ja…

wordpress后台更新后 前端没变化的解决方法

使用siteground主机的wordpress网站,会出现更新了网站内容和修改了php模板文件、js文件、css文件、图片文件后,网站没有变化的情况。 不熟悉siteground主机的新手,遇到这个问题,就很抓狂,明明是哪都没操作错误&#x…

网络编程(Modbus进阶)

思维导图 Modbus RTU(先学一点理论) 概念 Modbus RTU 是工业自动化领域 最广泛应用的串行通信协议,由 Modicon 公司(现施耐德电气)于 1979 年推出。它以 高效率、强健性、易实现的特点成为工业控制系统的通信标准。 包…

UE5 学习系列(二)用户操作界面及介绍

这篇博客是 UE5 学习系列博客的第二篇,在第一篇的基础上展开这篇内容。博客参考的 B 站视频资料和第一篇的链接如下: 【Note】:如果你已经完成安装等操作,可以只执行第一篇博客中 2. 新建一个空白游戏项目 章节操作,重…

IDEA运行Tomcat出现乱码问题解决汇总

最近正值期末周,有很多同学在写期末Java web作业时,运行tomcat出现乱码问题,经过多次解决与研究,我做了如下整理: 原因: IDEA本身编码与tomcat的编码与Windows编码不同导致,Windows 系统控制台…

利用最小二乘法找圆心和半径

#include <iostream> #include <vector> #include <cmath> #include <Eigen/Dense> // 需安装Eigen库用于矩阵运算 // 定义点结构 struct Point { double x, y; Point(double x_, double y_) : x(x_), y(y_) {} }; // 最小二乘法求圆心和半径 …

使用docker在3台服务器上搭建基于redis 6.x的一主两从三台均是哨兵模式

一、环境及版本说明 如果服务器已经安装了docker,则忽略此步骤,如果没有安装,则可以按照一下方式安装: 1. 在线安装(有互联网环境): 请看我这篇文章 传送阵>> 点我查看 2. 离线安装(内网环境):请看我这篇文章 传送阵>> 点我查看 说明&#xff1a;假设每台服务器已…

XML Group端口详解

在XML数据映射过程中&#xff0c;经常需要对数据进行分组聚合操作。例如&#xff0c;当处理包含多个物料明细的XML文件时&#xff0c;可能需要将相同物料号的明细归为一组&#xff0c;或对相同物料号的数量进行求和计算。传统实现方式通常需要编写脚本代码&#xff0c;增加了开…

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器的上位机配置操作说明

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器专为工业环境精心打造&#xff0c;完美适配AGV和无人叉车。同时&#xff0c;集成以太网与语音合成技术&#xff0c;为各类高级系统&#xff08;如MES、调度系统、库位管理、立库等&#xff09;提供高效便捷的语音交互体验。 L…

(LeetCode 每日一题) 3442. 奇偶频次间的最大差值 I (哈希、字符串)

题目&#xff1a;3442. 奇偶频次间的最大差值 I 思路 &#xff1a;哈希&#xff0c;时间复杂度0(n)。 用哈希表来记录每个字符串中字符的分布情况&#xff0c;哈希表这里用数组即可实现。 C版本&#xff1a; class Solution { public:int maxDifference(string s) {int a[26]…

【大模型RAG】拍照搜题技术架构速览:三层管道、两级检索、兜底大模型

摘要 拍照搜题系统采用“三层管道&#xff08;多模态 OCR → 语义检索 → 答案渲染&#xff09;、两级检索&#xff08;倒排 BM25 向量 HNSW&#xff09;并以大语言模型兜底”的整体框架&#xff1a; 多模态 OCR 层 将题目图片经过超分、去噪、倾斜校正后&#xff0c;分别用…

【Axure高保真原型】引导弹窗

今天和大家中分享引导弹窗的原型模板&#xff0c;载入页面后&#xff0c;会显示引导弹窗&#xff0c;适用于引导用户使用页面&#xff0c;点击完成后&#xff0c;会显示下一个引导弹窗&#xff0c;直至最后一个引导弹窗完成后进入首页。具体效果可以点击下方视频观看或打开下方…

接口测试中缓存处理策略

在接口测试中&#xff0c;缓存处理策略是一个关键环节&#xff0c;直接影响测试结果的准确性和可靠性。合理的缓存处理策略能够确保测试环境的一致性&#xff0c;避免因缓存数据导致的测试偏差。以下是接口测试中常见的缓存处理策略及其详细说明&#xff1a; 一、缓存处理的核…

龙虎榜——20250610

上证指数放量收阴线&#xff0c;个股多数下跌&#xff0c;盘中受消息影响大幅波动。 深证指数放量收阴线形成顶分型&#xff0c;指数短线有调整的需求&#xff0c;大概需要一两天。 2025年6月10日龙虎榜行业方向分析 1. 金融科技 代表标的&#xff1a;御银股份、雄帝科技 驱动…

观成科技:隐蔽隧道工具Ligolo-ng加密流量分析

1.工具介绍 Ligolo-ng是一款由go编写的高效隧道工具&#xff0c;该工具基于TUN接口实现其功能&#xff0c;利用反向TCP/TLS连接建立一条隐蔽的通信信道&#xff0c;支持使用Let’s Encrypt自动生成证书。Ligolo-ng的通信隐蔽性体现在其支持多种连接方式&#xff0c;适应复杂网…

铭豹扩展坞 USB转网口 突然无法识别解决方法

当 USB 转网口扩展坞在一台笔记本上无法识别,但在其他电脑上正常工作时,问题通常出在笔记本自身或其与扩展坞的兼容性上。以下是系统化的定位思路和排查步骤,帮助你快速找到故障原因: 背景: 一个M-pard(铭豹)扩展坞的网卡突然无法识别了,扩展出来的三个USB接口正常。…

未来机器人的大脑:如何用神经网络模拟器实现更智能的决策?

编辑&#xff1a;陈萍萍的公主一点人工一点智能 未来机器人的大脑&#xff1a;如何用神经网络模拟器实现更智能的决策&#xff1f;RWM通过双自回归机制有效解决了复合误差、部分可观测性和随机动力学等关键挑战&#xff0c;在不依赖领域特定归纳偏见的条件下实现了卓越的预测准…

Linux应用开发之网络套接字编程(实例篇)

服务端与客户端单连接 服务端代码 #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <pthread.h> …

华为云AI开发平台ModelArts

华为云ModelArts&#xff1a;重塑AI开发流程的“智能引擎”与“创新加速器”&#xff01; 在人工智能浪潮席卷全球的2025年&#xff0c;企业拥抱AI的意愿空前高涨&#xff0c;但技术门槛高、流程复杂、资源投入巨大的现实&#xff0c;却让许多创新构想止步于实验室。数据科学家…

深度学习在微纳光子学中的应用

深度学习在微纳光子学中的主要应用方向 深度学习与微纳光子学的结合主要集中在以下几个方向&#xff1a; 逆向设计 通过神经网络快速预测微纳结构的光学响应&#xff0c;替代传统耗时的数值模拟方法。例如设计超表面、光子晶体等结构。 特征提取与优化 从复杂的光学数据中自…