词条有哪些网站可以做,多层分销网站建设,网站开发子账号,wordpress实现说说概述#xff08;Overview#xff09;
在一个 裸机#xff08;bare-metal#xff09;环境 下#xff0c;我们要展示 C 的高效使用方法。
这里涉及几个核心问题#xff1a;
为什么硬件交互#xff08;HW interactions#xff09;常用 C 语言#xff1f;
历史原因#x…概述Overview在一个裸机bare-metal环境下我们要展示C 的高效使用方法。这里涉及几个核心问题为什么硬件交互HW interactions常用 C 语言历史原因C 语言自 1970s 以来一直被用于系统编程和嵌入式开发。硬件厂商提供的寄存器访问库、启动代码startup code、中断向量表等几乎都基于 C。特性匹配C 语言提供低开销、可预测的内存布局适合直接操作硬件寄存器。例如访问一个 32 位寄存器通常写成REG320x01; REG32 0x01;REG320x01;编译器支持几乎所有嵌入式编译器对 C 的支持最成熟生成的汇编最优化且可控。从 C 调用 C 的最佳实践是什么在裸机开发中我们常常需要在 C 中使用已有的 C 库或硬件驱动。最佳实践包括使用extern C防止名字修饰externC{#includec_driver.h}这样可以保证 C 编译器调用 C 函数时不会对函数名进行 name mangling从而正确链接。封装 C 接口不要直接在业务代码中调用 C API而是通过 C 封装一层类或函数。例如classTimer{public:voidstart(){c_timer_start();}voidstop(){c_timer_stop();}};使用类型安全type-safe和 RAIIC 可以通过构造函数/析构函数保证资源正确释放避免裸指针或手动管理错误。如何用 C 让硬件访问更清晰、安全和可测试封装寄存器访问使用类封装寄存器每个寄存器字段用位域或 getter/setter 访问。例structReg{volatileuint32_tvalue;voidset_bit(intpos){value|(1upos);}voidclear_bit(intpos){value~(1upos);}boolread_bit(intpos)const{returnvalue(1upos);}};使用模板提高复用性可以写通用寄存器类模板化地址或字段宽度templateuintptr_t addrclassReg32{public:staticvoidwrite(uint32_tval){*reinterpret_castvolatileuint32_t*(addr)val;}staticuint32_tread(){return*reinterpret_castvolatileuint32_t*(addr);}};抽象接口便于测试将硬件访问抽象成接口测试时可以替换为虚拟实现classIHW{public:virtualvoidwrite(uint32_tval)0;virtualuint32_tread()0;};classMockHW:publicIHW{uint32_tdata0;voidwrite(uint32_tval)override{dataval;}uint32_tread()override{returndata;}};类型安全和范围检查C 可以在编译期或运行期检查寄存器值合法性减少错误。例如assert(valueMAXREGVALUE); assert(value MAX_REG_VALUE);assert(valueMAXREGVALUE);总结C 语言仍然是裸机硬件交互的主力但 C 可以通过封装、类型安全、模板和抽象让硬件访问更安全、清晰、可测试。最佳实践extern C C 封装 RAII 模板化寄存器访问 接口抽象。为什么 C 语言如此普及Why is C so prolific?它是操作系统内核的语言“It’s the kernel, silly!” 直译就是“它是内核语言傻瓜”C 语言从 UNIX 内核开始就被广泛使用几乎所有现代操作系统的内核都以 C 为主。由于操作系统内核对性能和可控性要求极高C 语言的低开销、直接访问硬件能力正好满足需求。历史原因30 年前所有低层次交互都是用 C 实现的。当时嵌入式系统、驱动程序、启动代码等几乎完全依赖 C。例如访问硬件寄存器通常写作REG320x01; REG32 0x01;REG320x01;这类直接内存操作在 C 中非常自然并且编译器生成的汇编可预测。组织的习惯与信任许多组织在嵌入式应用中对 C 语言非常熟悉已有大量成熟库、驱动和工具链。因此团队在开发新项目时倾向沿用已验证的 C 代码而不是冒险切换到 C。对 C 的顾虑在资源受限的环境如微控制器、裸机系统中许多人仍然对 C 保持谨慎态度。原因包括运行时开销如虚函数表、异常处理、RTTI 等编译器生成代码的不确定性对堆栈和内存占用的担忧因此即便 C 提供更高的抽象能力和类型安全性很多嵌入式团队仍优先选择 C。总结C 语言之所以普及是因为它是操作系统内核和裸机开发的历史基石易于直接操作硬件组织习惯与成熟工具链的依赖C 在资源受限环境中的成本和复杂性仍让人谨慎C 的优势What are the advantages of C?显而易见的问题为什么要用 CC 相比 C 提供了更多的语言特性和安全保障主要优势包括生命周期管理Lifetime ManagementC 可以通过构造函数和析构函数自动管理对象生命周期减少手动释放资源的错误。RAIIResource Acquisition Is Initialization资源获取即初始化模式是 C 管理资源的核心例如classFileWrapper{FILE*f;public:FileWrapper(constchar*path){ffopen(path,r);}~FileWrapper(){if(f)fclose(f);}};这样可以保证文件总是被正确关闭无论函数如何返回。类型安全Type SafetyC 提供更严格的类型检查减少因类型错误导致的 bug。例如inta5;doubleba;// 自动转换安全char*preinterpret_castchar*(a);// 明确转换强制风险对比 CC 在编译期可以捕捉更多类型相关的错误。复杂且强类型系统Sophisticated Strong Typing System支持模板、枚举类enum class、constexpr等特性。可以在编译期做更多检查和计算提高性能和安全性。示例enumclassColor{Red,Green,Blue};Color cColor::Red;// 不能直接与整数比较增强类型安全继承与多态Inheritance and Polymorphism支持面向对象编程OOP便于抽象硬件接口、复用代码和实现多态行为。示例classIHW{public:virtualvoidwrite(uint32_tval)0;virtualuint32_tread()0;};classMockHW:publicIHW{voidwrite(uint32_tval)override{/* mock */}uint32_tread()override{return0;}};这种方式使测试更简单、安全并且代码更可维护。仍可能需要 C 风格操作即便使用 C在裸机环境中有时仍需要调用内核函数Kernel functions使用现有 C APIExisting APIs转换到原始指针raw pointers但是关键在于不要让 C 的优势被危险的 C 风格代码抵消。也就是说即便写裸指针、手动管理内存也应尽量用 RAII、封装和类型安全机制保护代码。总结C 的优势主要体现在自动管理对象生命周期RAII更严格的类型安全强大且复杂的类型系统支持面向对象抽象继承与多态为什么不重写所有的 C 代码Why not rewrite all the C code?代码量巨大There’s LOTS of code out there!全球已有数十年累积的 C 语言代码库包括操作系统、驱动、嵌入式库等。这些代码经过长期验证非常稳定且被广泛使用。重写代价高To rewrite a modest sized C code base would take years!即使是中等规模的 C 项目完全用 C 重写也可能需要数年时间。重写过程中可能引入新的 bug风险高且成本大。Linux 内核重写几乎不可行Rewriting the Linux kernel is likely intractableLinux 内核是数百万行 C 代码组成的复杂系统。试图完全用 C 重写不仅工程量巨大而且兼容性、性能和稳定性难以保证。因此Ergo必须继续使用 C关键问题是如何在继续使用 C 的同时有效结合 C 的最佳实践。即在裸机或嵌入式开发中可以这样处理用 C 封装 C 接口使用 RAII、类型安全和模板等特性保护代码保留底层 C 函数和 API但通过接口抽象提高安全性和可测试性参考资料See also Stroustrup, 2023Bjarne StroustrupC 之父在 2023 年的著作中也提到不要盲目重写已有 C 代码优先考虑封装和现代 C 的增量改进策略总结C 代码库庞大且稳定重写代价高且风险大Linux 内核等关键项目几乎不可能完全重写策略继续使用 C同时用 C 提升安全性、可维护性和可测试性C 关键安全特性Key C Safety FeaturesC 提供了很多安全特性让开发者在编译期或运行期尽量避免错误但主要关注以下几个方面生命周期管理Lifetime ManagementC 可以通过智能指针smart pointers自动管理对象的生命周期减少手动调用new/delete的错误。常用的智能指针类型包括std::unique_ptrT独占所有权std::shared_ptrT共享所有权std::weak_ptrT弱引用不增加引用计数示例std::unique_ptrintpstd::make_uniqueint(42);// 不需要手动 deletep 超出作用域时自动释放内存核心优势避免内存泄漏和悬挂指针。严格类型Strict TypingC 有丰富的语言元素能在编译阶段阻止许多类型错误。示例枚举类enum class防止隐式转换模板参数类型检查const/constexpr修饰符保证值不可修改示例代码enumclassColor{Red,Green,Blue};Color cColor::Red;// 不能直接与整数比较编译器报错通过这些机制可以在编译期捕捉错误减少运行时异常。标准测试Standard TestsC 标准库提供了许多比 libc 更丰富的测试工具帮助保证程序安全性。示例std::is_same_vT1, T2类型检查std::is_base_ofBase, Derived继承关系检查算法库中的std::all_of、std::any_of等可以安全操作容器这些工具可以在编译期或运行期检查逻辑保证程序正确性。总结C 的关键安全特性包括生命周期管理使用智能指针如unique_ptrT管理资源防止内存泄漏严格类型检查利用语言特性在编译期捕捉类型错误标准库测试工具提供比 C libc 更丰富的检查和工具提高程序安全性C/C 边界的简单交互Naive crossing of the C/C boundary在 C 中智能指针非常强大但在与 C 接口交互时要格外小心。智能指针的基本概念Smart Pointers are GREAT!智能指针负责自动管理对象生命周期避免手动调用delete导致的内存泄漏或悬挂指针。例如std::unique_ptrTmy_t_ptrstd::make_uniqueT();my_t_ptr拥有对象的唯一所有权当它超出作用域时所管理的对象会被自动释放。访问底层裸指针Underlying Reference / Naked Pointer每个智能指针内部都有一个裸指针可以通过get()方法访问T*raw_ptrmy_t_ptr.get();注意get()返回的是裸指针但智能指针仍然拥有对象的所有权。调用 C 接口Crossing the C/C boundary在需要传递给 C 函数时可以使用get()获取裸指针my_c_api(my_t_ptr-get());这里的裸指针被 C 函数使用但智能指针仍然管理对象的生命周期。继续使用裸指针是否安全Then we can use the result, right?有人可能想直接在后续函数继续使用裸指针next_function(my_t_ptr-get());答案也许Maybe原因如果my_c_api或其他函数内部释放了裸指针指向的对象或者改变了对象的所有权后续使用就可能悬挂或未定义行为。即使 C 函数没有释放跨函数使用裸指针也可能存在生命周期被破坏的风险尤其在异常、返回早退或多线程环境下。总结智能指针在 C 中管理对象生命周期get()可用于调用 C 接口但必须明确谁拥有对象的所有权不能随意假设裸指针在整个程序中一直有效使用 C 接口时要特别注意生命周期和所有权管理智能指针跨 C/C 边界Smart pointers across the C/C boundary在 C 与 C 接口交互时智能指针提供了更安全的方式来管理对象但需要注意潜在风险。1. 使用场景Use case在 C 中分配对象需要通过C API传递给底层 C 代码C 代码处理对象后返回结果关键问题是如何在 C 中安全地管理对象所有权2. 风险Risks同步问题Synchronization Problem底层 C 代码可能修改了裸指针如果 C 端继续直接使用裸指针可能导致悬挂指针内存安全问题Memory Safety ProblemC 代码可能删除并重新分配内存智能指针仍然认为自己拥有旧的对象从而可能导致双重释放或未定义行为3. 标准库解决方案Standard Library HelperC 标准库提供了语法糖syntactic sugar来安全地跨 C/C 边界使用智能指针std::inout_ptr用于输入/输出参数允许 C 函数修改裸指针同时保持智能指针管理权std::out_ptr用于仅输出参数用于 C 函数创建对象并返回给智能指针4. 示例Example: make_unique into C API and backstd::unique_ptrTmy_t_ptrstd::make_uniqueT();// 传入 C APIC 代码可以修改指针my_c_api(std::inout_ptr(my_t_ptr));// 后续函数也安全使用修改后的指针next_function(std::inout_ptr(my_t_ptr));这里std::inout_ptr(my_t_ptr)会自动管理裸指针的所有权即使 C 代码修改了指针智能指针仍然保证对象生命周期安全总结直接使用get()传递裸指针有风险使用std::inout_ptr/std::out_ptr可以安全地跨 C/C 边界智能指针与标准库工具结合可以避免悬挂指针和双重释放等常见问题out_ptr 和 inout_ptr 的工作原理What’s really going on with out_ptr and inout_ptr?在 C 智能指针与 C 接口交互时std::out_ptr和std::inout_ptr是用于安全传递裸指针的工具。1. 底层原理Underlying mechanism任何智能指针内部都有一个裸指针raw pointer或引用out_ptr和inout_ptr在调用 C 函数时会把这个裸指针传给 C 函数T*raw_ptrmy_smart_ptr.get();my_c_api(raw_ptr);C 函数可以读取或修改指针内容但智能指针仍然管理对象的生命周期。2. inout_ptr 的特性inout_ptr用于输入/输出参数当 C 函数返回时inout_ptr会将智能指针重置reset为返回的新指针值这相当于下面的操作std::unique_ptrTmy_t_ptrstd::make_uniqueT();T*my_raw_t_ptrmy_t_ptr-get();// 获取裸指针my_c_api(my_raw_t_ptr);// 调用 C 函数my_t_ptr-reset(my_raw_t_ptr);// 将智能指针重置为返回值next_function(my_t_ptr-get());// 安全使用智能指针管理的对象这样可以保证即使 C 函数修改了指针智能指针依然正确管理生命周期避免内存泄漏或悬挂指针。3. out_ptr 的特性out_ptr用于仅输出参数C 函数负责创建对象并将裸指针返回给智能指针智能指针会在函数返回后接管新分配对象的所有权例如std::unique_ptrTmy_t_ptr;my_c_api(std::out_ptr(my_t_ptr));// C 函数分配对象// my_t_ptr 自动接管对象生命周期4. 总结智能指针底层是裸指针inout_ptr和out_ptr都安全地传递裸指针给 C 函数inout_ptr会在返回时重置智能指针保持生命周期管理使用这两个工具可以避免手动 reset减少悬挂指针和内存泄漏风险智能指针的get()函数The smart pointer get function is a value在 C 中智能指针提供了get()方法用于访问其底层裸指针但需要理解它的行为特性。1.get()返回的是值get() returns a value当调用get()时返回的是裸指针的拷贝即指针地址的副本pointerget()constnoexcept;pointer是裸指针类型如T*const noexcept表示不会修改智能指针本身也不会抛异常示例std::unique_ptrTmy_ptrstd::make_uniqueT();T*raw_ptrmy_ptr.get();// 这是一个拷贝注意raw_ptr只是智能指针管理对象的裸指针拷贝它并不拥有对象的生命周期。2. 潜在问题Potential Issues如果底层 C 代码修改了指针例如移动了指针pointer moved删除并重新分配了对象deleted and re-allocated那么智能指针的get()返回的裸指针就会失效变成悬挂指针stale pointerT* stale_ptr my_ptr.get(); // 如果 C 删除或重新分配stale_ptr 不再有效 \text{T* stale\_ptr my\_ptr.get(); // 如果 C 删除或重新分配stale\_ptr 不再有效}T* stale_ptr my_ptr.get(); //如果C删除或重新分配stale_ptr不再有效这种情况下继续使用stale_ptr会导致未定义行为undefined behavior可能崩溃或内存错误。3. 安全实践避免直接使用get()后在 C 端长期保留裸指针跨 C/C 边界时应使用std::inout_ptr/std::out_ptr保证智能指针在 C 函数返回后正确更新内部指针my_c_api(std::inout_ptr(my_ptr));// 安全内部自动 reset总结get()返回裸指针的拷贝不改变智能指针所有权如果 C 代码删除或修改了底层对象get()返回的指针可能失效stale跨 C/C 边界时推荐使用inout_ptr/out_ptr来安全管理对象生命周期细节很重要Details matter在裸机开发或与硬件交互时不能让编译器做“奇怪的优化”特别是内存布局和访问顺序相关的操作。1. 编译器可能的重排Memory Layout Reordering编译器为了优化性能可能会重排类或结构体中的成员变量的内存顺序例如classFoo{private:int32_ta;public:int32_tb;int32_tc;};在这个例子中a是私有成员而b和c是公有成员编译器可能根据访问权限和对齐规则重排内存以提高访问效率比如把b放在前面a放在后面这种重排在裸机或与硬件直接映射寄存器的场景下可能会导致严重错误2. 为什么会发生重排多个访问修饰符access specifiers可能让编译器认为成员间没有严格顺序依赖对齐alignment和填充padding规则也可能导致内存顺序变化例如在 32 位系统上编译器可能按 4 字节对齐排列成员变量注意编译器在某些情况下也可能不做重排但不能完全依赖这种行为3. 安全实践对于硬件寄存器或需要固定内存布局的结构体使用[[gnu::packed]]或#pragma pack等手段保证顺序struct__attribute__((packed))FooPacked{int32_ta;int32_tb;int32_tc;};这样可以确保内存布局与代码顺序一致避免编译器优化带来的潜在问题总结编译器可能重排类/结构体的内存布局在裸机或硬件访问场景中这可能导致严重问题解决方法使用packed属性或#pragma pack保证布局避免依赖编译器默认顺序C 在 C 边界的其他指导原则Other C guidance at the C boundary在裸机或嵌入式开发中C 对象如果需要跨 C 边界使用需要遵循一些严格规则以保证内存布局和行为兼容性使用相同的访问控制Use same access control所有成员的访问权限public/protected/private必须一致避免编译器优化引起内存顺序变化。对象或基类不能有虚函数No virtual in the object or any base class虚函数会引入虚函数表vtable改变对象内存布局不兼容 C 结构。最派生类不能有非静态数据成员No non-static data members in the most derived class防止派生类增加额外成员破坏基类在 C 中预期的内存布局。所有基类必须是标准布局All base class(es) are standard layout通过 C 标准的std::is_standard_layout可以检测对象的所有成员也必须遵守这些规则否则内存布局可能在 C 和 C 之间不一致参考Fertig (2020)如何测试内存兼容性How to test for memory compatibility为了保证对象在 C 和 C 中兼容需要满足两个条件平凡类型Trivial用std::is_trivialT检查平凡类型支持静态初始化static initialization并且默认构造、拷贝、移动不会引入额外逻辑std::is_trivialFoo::value⇒类型可静态初始化 \text{std::is\_trivialFoo::value} \Rightarrow \text{类型可静态初始化}std::is_trivialFoo::value⇒类型可静态初始化标准布局Standard Layout用std::is_standard_layoutT检查确保对象在 C 中的内存布局与 C 兼容std::is_standard_layoutFoo::value⇒类型在 C/C 中布局一致 \text{std::is\_standard\_layoutFoo::value} \Rightarrow \text{类型在 C/C 中布局一致}std::is_standard_layoutFoo::value⇒类型在C/C中布局一致组合这两个条件可以安全地将 C 对象用于 C API 或裸机寄存器映射。摆脱内核束缚Break free of the kernel使用 C 的原因内核使用 C裸机嵌入式系统实际上做的事情和内核类似思路可以参考内核方式但不必完全受限可以使用 C 封装和安全特性仍然遵循内存布局和生命周期管理规则总结跨 C 边界使用 C 对象时要遵守严格规则访问控制、无虚函数、标准布局等使用std::is_trivial和std::is_standard_layout检查内存兼容性裸机开发参考内核设计但可以利用 C 提供的安全性和封装能力摆脱内核束缚Break free of the kernel现状我们使用 C 是因为内核使用 C裸机嵌入式本质上做的事情类似内核操作误区直接模仿内核全部使用 C正确做法C 提供封装、类型安全、RAII 等优势应该充分利用 C 特性而不是完全依赖 C简单的硬件访问封装Naive HW access第一原则将所有硬件访问隐藏在函数内部不暴露裸指针或寄存器地址示例UART 波特率寄存器封装classmy_uart{public:voidset_baud_rate(constuint32_trate){*BAUD_RATE_REGrate;// 写寄存器}uint32_tget_baud_rate()const{return*BAUD_RATE_REG;// 读寄存器}};优点对寄存器的直接访问被封装其他代码无需关心底层寄存器地址提高可维护性和安全性Pimpl 设计模式pimpl Idiompimpl pointer to implementation通过指针将实现细节与接口分离classmy_class{private:std::unique_ptrmy_class_implp_impl;};为什么使用这种模式实现细节的修改不会影响类定义调用者无需重新编译减少依赖和编译时间提高封装性和灵活性核心思想类接口固定内部实现可以自由修改智能指针管理实现对象的生命周期总结不要盲目模仿内核C 有自己的优势硬件访问封装用函数封装寄存器访问避免裸指针泄露Pimpl 模式分离接口与实现修改实现不影响调用者智能指针管理对象生命周期Pimpl 启发的寄存器访问pimpl inspired register accessPimpl指向实现的指针pointer to implementation问题为什么不直接用寄存器集合的指针1. 定义寄存器结构体Register Setstructmy_uart_regs{volatileuint32_tBAUD_RATE_REG;// ... 其他寄存器};使用volatile修饰防止编译器优化访问寄存器的代码my_uart_regs封装所有 UART 寄存器2. 在类中使用寄存器指针Pointer to Register Setclassmy_uart{private:std::unique_ptrmy_uart_regsp_regs;};类中保存一个智能指针指向寄存器集合好处将硬件寄存器访问抽象成对象3. 为什么使用寄存器指针Why use a pointer to a register set?增加可测试性Make more testable code通过构造函数注入寄存器指针classmy_uart{public:my_uart(std::unique_ptrmy_uart_regsregs):p_regs(std::move(regs)){}private:std::unique_ptrmy_uart_regsp_regs;};好处可以传入任意可转换为my_uart_regs的对象或者继承自my_uart_regs的对象便于替换测试桩test harnesses无需修改类本身即可测试不同寄存器实现这种模式类似Pimpl 模式只是指针指向寄存器集合而非内部实现4. 核心思想类接口固定内部实现寄存器集合可替换利用智能指针管理寄存器集合生命周期便于单元测试或模拟寄存器行为参考CppCon 2023 关于HookableRegister的讲座总结使用std::unique_ptrmy_uart_regs代替直接访问裸寄存器提高封装性构造函数注入寄存器指针使代码可替换、可测试类似 Pimpl 思路接口与实现寄存器集合分离如何定义寄存器集合的结构体How to define the struct of the register set在裸机开发中需要一个与硬件寄存器内存布局完全一致的结构体。关键点包括顺序、对齐、填充以及数据宽度。1. 寄存器顺序Registers in order寄存器顺序必须与硬件手册一致如果寄存器之间有空闲空间需要显式使用填充padding占位2. 数据宽度与修饰Data width, packed, aligned, volatile不要改变数据宽度每个寄存器的位宽必须与硬件一致使用volatile防止编译器优化寄存器访问volatile uint32_t reg; \text{volatile uint32\_t reg;}volatile uint32_t reg;使用packed避免编译器在结构体成员间插入额外填充使用aligned(4)确保结构体按 4 字节对齐3. 示例代码Exampletypedefstruct__attribute__((packed))__attribute__((aligned(4))){volatileuint32_treg1;// Offset 0volatileuint32_treg2;// Offset 4volatileuint32_tpad[4];// Padding 占位volatileuint32_treg3;// Offset 20// ...}regs_t;reg1和reg2是连续寄存器pad[4]占用空闲空间保证reg3在正确的偏移量Offset 20结构体严格匹配硬件布局保证裸机访问安全4. 总结定义寄存器结构体时要保证顺序一致与硬件寄存器顺序相同填充空闲空间用数组或占位符补齐宽度不变每个寄存器位宽与硬件一致volatile修饰防止编译器优化packed与aligned保证结构体内存布局与硬件一致这样可以安全地使用结构体直接映射硬件寄存器同时便于测试和封装。1. 管理智能指针Managing the smart pointer初始化智能指针通过接口可以将原始指针初始化到智能指针中例如uint32_tmy_regs_addr0x10D030000;regs_t*my_regs_raw_ptrreinterpret_castregs_t*(my_regs_addr);std::unique_ptrregs_tp_regs;p_regs.reset(my_regs_raw_ptr);这里reinterpret_castregs_t*将一个整数地址转换为指针类型。reset()方法将原始指针交给智能指针管理。注意潜在的崩溃风险当智能指针p_regs离开作用域时它会自动调用deleter去释放所管理的对象。如果指针指向的是硬件寄存器地址或非堆对象直接调用delete会导致程序崩溃。2. 关于unique_ptr的注意事项A note about unique_ptr直觉上的声明我们通常认为unique_ptr声明如下templatetypenameTclassunique_ptr{...};意思是unique_ptr只管理类型T的对象并在析构时自动释放。实际声明实际上unique_ptr的声明更接近templatetypenameT,typenameDeleterstd::default_deleteTclassunique_ptr{...};这里多了一个deleter 类型参数Deleter默认为std::default_deleteT即普通的delete操作。用户可以自定义 deleter例如针对特殊内存或硬件寄存器。智能指针崩溃的根源当unique_ptr管理的对象不是普通堆分配的对象时默认 deleter (delete) 会错误地尝试释放它导致程序崩溃。解决方法使用自定义 deleter。或者不使用unique_ptr管理非堆对象。3. 总结unique_ptr的自动管理机制依赖于deleter。默认 deleter 对普通堆对象有效对特殊内存如硬件寄存器、内存映射 I/O可能导致崩溃。当管理非堆对象时必须提供合适的 deleterstd::unique_ptrT, CustomDeleter p(obj); \text{std::unique\_ptrT, CustomDeleter p(obj);}std::unique_ptrT, CustomDeleter p(obj);1.shared_ptr与unique_ptr的自定义 deleter 区别常规智能指针构造我们平时习惯这样创建智能指针std::shared_ptrTmy_ptrrhs;这意味着当指针离开作用域时会自动调用默认 deleterdelete释放所管理的对象。潜在问题当对象不是普通堆对象例如硬件寄存器或静态内存时默认 deleter 会在作用域结束时调用delete导致程序崩溃。因此需要告诉语言“不要删除该对象”即使用自定义 deleter。2. 自定义 deleter 的使用unique_ptr的自定义 deleterstd::unique_ptrT,Dmy_uniqueptrrhs;这里D是自定义 deleter 类型。可以定义一个“不执行删除操作”的 deleter避免作用域结束时自动 delete。shared_ptr的自定义 deleterstd::shared_ptrTmy_sharedptr(ref,D);ref是指向对象的原始指针。D是自定义 deleter 类型同样可以让shared_ptr离开作用域时不删除对象。危险提示自定义 deleter 是危险操作dangerous!一定要确保不会错误释放非堆对象。3. no_deleter 示例一个“不删除对象”的 deleterstructno_deleter{voidoperator()(T*ptr){// 什么也不做}};注意事项不要模板化no_deleter错误写法templateTstructno_deleter{...};正确写法直接用具体类型或通过类型别名绑定。这样unique_ptrT, no_deleter或shared_ptrT在离开作用域时不会调用 delete。4. 总结默认 deleter 会在作用域结束时调用deletestd::unique_ptrT p(ptr); ⟹ delete ptr on scope exit \text{std::unique\_ptrT p(ptr);} \implies \text{delete ptr on scope exit}std::unique_ptrT p(ptr);⟹delete ptr on scope exit当对象不是普通堆对象时需要自定义 deleterstruct no_deleter void operator()(T* ptr) ; \text{struct no\_deleter { void operator()(T* ptr) {} };}struct no_deletervoid operator()(T* ptr);对于unique_ptr和shared_ptrunique_ptrT, D在模板参数中绑定 deleter类型Dshared_ptrT在构造函数中传入 deleter对象D使用自定义 deleter可以安全管理非堆对象但需谨慎操作避免内存泄漏或非法释放。1. 目标在“纯 C”中对硬件寄存器memory-mapped registers进行访问同时安全管理指针避免智能指针在作用域结束时尝试释放硬件寄存器。这里采用unique_ptr 自定义 deleter的方式来安全访问硬件寄存器。2. 定义硬件寄存器结构体定义一个结构体表示硬件 UART 寄存器structmy_uart_regs__attribute__((packed))__attribute__((aligned(4))){volatileuint32_tBAUD_RATE_REG;// 其他寄存器};说明__attribute__((packed))避免编译器对结构体进行填充padding保证寄存器布局与硬件一致。__attribute__((aligned(4)))保证对齐通常硬件寄存器需要按 4 字节对齐。volatile防止编译器对寄存器的读写进行优化。确保每次访问都会真正读取/写入硬件。3. 自定义 deleter不释放硬件structuart_regs_no_deleter{voidoperator()(my_uart_regs*ptr){// 什么也不做}};作用unique_ptr在离开作用域时默认会调用delete而这里的寄存器地址是硬件地址不能释放。自定义 deleter 确保不做任何释放操作。4. 封装为类my_uartclassmy_uart{public:my_uart(uintptr_t base_addr):p_regs(reinterpret_castmy_uart_regs*(base_addr)){// 初始化操作如果有}voidset_baud_rate(constuint32_trate){p_regs-BAUD_RATE_REGrate;}uint32_tget_baud_rate()const{returnp_regs-BAUD_RATE_REG;}private:std::unique_ptrmy_uart_regs,uart_regs_no_deleterp_regs;};关键点说明构造函数p_regs(reinterpret_castmy_uart_regs*(base_addr))将硬件地址转换为指向寄存器结构体的指针并由unique_ptr管理。由于使用了自定义 deleteruart_regs_no_deleter智能指针不会尝试释放硬件寄存器。成员函数set_baud_rate和get_baud_rate直接操作寄存器p_regs-BAUD_RATE_REG rate; \text{p\_regs-BAUD\_RATE\_REG rate;}p_regs-BAUD_RATE_REG rate;return p_regs-BAUD_RATE_REG; \text{return p\_regs-BAUD\_RATE\_REG;}return p_regs-BAUD_RATE_REG;保证了对硬件寄存器的安全、清晰访问。指针成员std::unique_ptrmy_uart_regs, uart_regs_no_deleter p_regs;智能指针管理寄存器指针生命周期同时避免非法 delete。5. 总结硬件寄存器访问在 C 中可以通过结构体映射 volatile 对齐实现。智能指针可以管理寄存器指针的生命周期但需要自定义 deleter避免释放硬件。对寄存器操作的函数封装如set_baud_rate/get_baud_rate保证接口安全、易用。这个模式的核心思路unique_ptr寄存器类型, 不释放 deleter p_regs(base_addr); \text{unique\_ptr寄存器类型, 不释放 deleter p\_regs(base\_addr);}unique_ptr寄存器类型,不释放deleter p_regs(base_addr);然后通过成员函数访问硬件寄存器。1. 基本位操作Bit Manipulation在 C 中可以像在 C 语言里一样对寄存器进行位操作例如uint32_tvalue0;value|0x1;// 将第 0 位设为 1value~(0x1);// 将第 0 位清零理解value | 0x1;按位或操作OR把value的第 0 位设置为 1。数学上可以写作value←value ∣ 0x1 \text{value} \gets \text{value} \ | \ 0x1value←value∣0x1value ~(0x1);按位与操作AND 取反将value的第 0 位清零而其他位保持不变。数学上可以写作value←value ∼0x1 \text{value} \gets \text{value} \ \ \ \sim 0x1value←value∼0x12. C 的优势强类型和constexpr虽然以上操作在 C 中完全可用但 C 提供了更安全的做法强类型Strong Typing通过类型系统区分不同寄存器、不同位字段减少误操作。例如structControlReg{uint32_tenable:1;uint32_tmode:3;uint32_tunused:28;};ControlReg reg{};reg.enable1;// 比直接写 value | 0x1 更安全constexpr优化可以在编译期进行位操作计算减少运行时开销。例如constexpruint32_tFLAG10;constexpruint32_tCLEAR_MASK~FLAG;数学上FLAG1≪0 \text{FLAG} 1 \ll 0FLAG1≪0CLEAR_MASK∼FLAG \text{CLEAR\_MASK} \sim \text{FLAG}CLEAR_MASK∼FLAG3. 总结C 风格位操作依然可用OR (|) 设置位AND NOT (~) 清位**C 强类型 constexpr**提供类型安全避免误操作错误地修改寄存器的其他位。编译期计算减少运行时开销提高性能。实际寄存器操作可以结合unique_ptr 自定义 deleter管理硬件地址再结合强类型位域和constexpr安全访问。1. 摒弃#define的习惯原始宏定义问题传统 C 风格的宏定义使用#define例如#defineREPLACE_BITS(x,mask,bits)((x(~(mask)))|(bitsmask))理解宏定义危险性宏在预处理阶段展开没有类型检查。容易出现命名冲突或逻辑错误。例如操作位时可能意外修改了不相关的位。常见情况在遗留代码中仍能看到很多通过#define定义的位操作宏。2. 使用std::bitset替代宏C 提供了强类型位操作方法可以用std::bitset封装宏逻辑templatestd::size_t Nstd::bitsetNreplace_bits(std::bitsetNx,std::bitsetNmask,std::bitsetNbits){return(x(~mask))|(bitsmask);}理解使用std::bitsetN类型安全操作在编译期检查位宽。可读性高替代宏后代码逻辑更清晰。constexpr 友好可在编译期求值。上述函数逻辑与宏类似但更安全x′(x (∼mask)) ∣ (bits mask) x (x \ \ \ (\sim mask)) \ | \ (bits \ \ \ mask)x′(x(∼mask))∣(bitsmask)3. 利用constexpr进行位操作示例std::bitset32my_bits0x3433;constexprstd::bitset32fourteen0xE;constexprstd::bitset32good_bitsfourteen1;// 编译期可计算理解字面值安全使用使用constexpr和std::bitset可以在编译期完成位运算。避免了传统宏或浮点字面值中的“最大匹配maximal munch”问题例如constexprstd::bitset32bad_bits0xE1;// 编译失败正确方式constexprstd::bitset32good_bitsfourteen1;// 编译成功std::bit_cast可将浮点数转换为整数位表示std::bit_caststd::uint32_t(1.0f);优势总结类型安全避免错误位操作。编译期求值无需运行时开销。可读性强取代宏的魔法数字。4. 总结摒弃传统#define位操作宏使用强类型constexpr函数。使用std::bitset封装位操作逻辑保证类型安全。利用constexpr和std::bit_cast可在编译期完成复杂位操作。避免“最大匹配”问题和宏展开带来的潜在危险。关于字节序Endianness在裸机开发和跨平台通信中字节序问题经常出现需要根据具体情况进行字节交换byte swap。1. 交换字节的常见原因网络字节序Network endianness到处理器字节序Processor endianness网络协议通常采用大端Big Endian某些处理器可能是小端Little Endian需要进行字节交换以正确解析数据密码学字节序Cryptographic endianness到处理器字节序某些加密算法规定字节序与处理器默认字节序不一致时需要交换硬件 BUGHW Bugs硬件可能返回错误字节序的数据必须在软件层修正2. C23 提供的解决方案C23 提供了std::byteswap支持constexpr示例autoswappedstd::byteswap(my_integer);在循环中也可以对数组进行字节翻转byte swizzlestd::uint16_tarray[]{...};for(autoa:array){lhsstd::byteswap(a);}优势编译期常量表达式constexpr可计算统一、标准化字节交换函数C26 展望Upcoming in C26饱和算术Saturated Arithmeticadd_sat(uint32_tx,uint32_ty);自动处理溢出rollover检查不必手动写边界判断静态反射Static Reflection提供更多元数据和编译期操作能力示例用途枚举转字符串enum to string按索引访问成员members by index提升测试性和编译期检查能力总结Wrapping up开发者在 C 中为了安全做了很多工作C 的优势强类型系统减少类型错误生命周期管理智能指针等特性自动管理资源编译器提供更多静态检查减少手动错误静态分析依然重要但 C 可以替你做更多工作实践示例Altera 已经在使用这种方式鼓励大家借鉴