资讯动态

C++ 安全实践指南:从 const、智能指针到类型安全转换的防御性编程

发布时间:2026/9/25 3:15:26 来源:尧图企业网站定制
文档教程【免费下载链接】cppbestpracticesCollaborative Collection of C Best Practices. This online resource is part of Jason Turners collection of C Best Practices resources. See README.md for more information.项目地址https://gitcode.com/gh_mirrors/cp/cppbestpractices点击查看免费下载本文是基于开源仓库 cppbestpractices 的《Considering Safety》章节04-Considering_Safety.md展开的安全编程专题。该仓库是 Jason Turner 发起的协作式 C 最佳实践合集见 01-Preface.md以简明、示例优先为原则与《Effective C》等经典书籍互补聚焦工程落地的具体细节。读完本文你将掌握一套可直接落地的 C 防御性编程方法论如何用const约束可变性、如何选择安全的返回类型、如何用智能指针消灭裸内存访问、如何用 C 风格转换与异常替代危险的语言特性从而写出更不易出错、更易被编译器优化的代码。尽可能使用 constconst向编译器声明一个变量或方法不可变。这一声明带来两个直接收益其一编译器可以基于不变性假设做更多优化其二开发者阅读代码时能立刻判断某个函数是否具有副作用——参数与成员都不可变函数的副作用面就被收窄了。此外以const 常量左值引用形式传递参数可以避免编译器产生不必要的对象复制// Bad Idea class MyClass { public: void do_something(int i); void do_something(std::string str); // 传入时复制一份 std::string }; // Good Idea class MyClass { public: void do_something(const int i); // 简单类型按值 const void do_something(const std::string str); // 复杂类型用 const 引用避免复制 };John Carmack 曾撰写过一篇关于const的著名评论原文档引用了该文04-Considering_Safety.md 中称之为值得一读核心观点正是尽量让编译器替你检查什么东西不该被改变这比任何代码审查都更可靠。需要指出的是简单类型不要用 const 引用见下文专节与复杂类型尽量用 const 引用并不矛盾const 的价值在于避免昂贵的复制构造对int、double这类标量反而会引入指针间接寻址的开销。与 const 正确性相关的交叉实践const不只是安全问题还与线程安全、性能直接关联MM 规则Mutex 与 mutable 搭配C11见 07-Considering_Threadability.md。如果成员变量是mutable应假定它是共享数据必须用互斥锁同步或改为原子类型反过来如果成员变量本身就是mutex则它必须是mutable否则无法在const成员函数中使用它。这条规则让const成员函数也能安全地承担线程同步职责。const 成员初始化见 03-Style.md。若成员变量初始化后不再改变就应标记为const例如const int m_value{0};。同时注意const成员无法被赋值拥有const成员的类通常没有有意义的拷贝赋值运算符。谨慎选择你的返回类型该按值返回还是按引用返回是 C API 设计中最常见也最容易出错的问题之一。原文档给出了明确的分场景指南Getter访问器当调用方对返回值的常规用途是观察只读使用时返回或const 能带来显著的性能节省——避免了复制整个对象。当调用方拿到返回值后本来就要复制一份时按值返回没有任何性能损失而且对线程安全更有利返回值是独立副本多个线程各自持有一份互不干扰。如果你的 API 使用了协变返回类型covariant return types则必须返回或*引用或指针因为协变返回类型依赖继承关系下的引用/指针语义。临时对象与局部值一律按值返回。返回对局部变量或临时对象的引用/指针会形成悬垂引用是典型的未定义行为来源。原文档同时给出了一条更细化的建议对于可变数据考虑按值返回对于不可变数据用const 返回。这与 07-Considering_Threadability.md 中的相关安全讨论相呼应——const 返回值意味着调用方拿到的是共享对象的只读视图而按值返回则把数据切分给每个调用方天然规避了数据竞争。反过来如果返回的是非const引用调用方就获得了修改共享状态的能力这与避免全局数据的线程安全原则相冲突。不要用 const 引用传递和返回简单类型对int、double这类标量类型使用const 传递或返回是常见的过度设计// Very Bad Idea class MyClass { public: explicit MyClass(const int t_int_value) : m_int_value(t_int_value) { } const int get_int_value() const { return m_int_value; } private: int m_int_value; }正确做法是简单类型一律按值传递和返回如果你不打算修改传入的值声明为const按值但不要用const引用// Good Idea class MyClass { public: explicit MyClass(const int t_int_value) : m_int_value(t_int_value) { } int get_int_value() const { return m_int_value; } private: int m_int_value; }原因按引用传递和返回意味着指针操作解引用、间接寻址而按值传递/返回时标量可以直接在处理器寄存器中传递速度快得多。对小类型来说避免复制省下的开销远小于指针间接访问引入的开销得不偿失。顺带一提上面的好例子还体现了 03-Style.md 推荐的命名惯例私有成员以m_前缀标识member data构造参数以t_前缀标识用于区分作用域。避免裸内存访问裸内存的分配、访问与释放new/delete配裸指针在 C 中极难保证正确稍有不慎就会引入内存错误与泄漏。C11 起提供了完整工具链来根治这一问题——智能指针与 RAII// Bad Idea MyClass *myobj new MyClass; // ... delete myobj; // 一旦中间有异常抛出或提前 return这里可能永远执行不到 // Good Idea auto myobj std::make_uniqueMyClass(constructor_param1, constructor_param2); // C14 auto myobj std::unique_ptrMyClass(new MyClass(constructor_param1, constructor_param2)); // C11 auto mybuffer std::make_uniquechar[](length); // C14 auto mybuffer std::unique_ptrchar[](new char[length]); // C11 // 或用于需要引用计数的对象 auto myobj std::make_sharedMyClass(); // ... // myobj 在不再被使用时自动释放要点解读优先make_uniqueC14它比unique_ptrT(new T(...))更简洁、更安全避免了先new再包装窗口期内的异常泄漏风险。C11 标准库尚未提供make_unique需手写unique_ptrT(new T(...))。数组支持std::make_uniquechar[](length)/std::unique_ptrchar[](new char[length])可用于动态数组并保证以delete[]正确释放。引用计数场景用make_shared需要多份所有权共享时使用std::shared_ptr。从性能角度见 08-Considering_Performance.md看make_shared将对象内存 控制块含引用计数合并为一次堆分配而std::shared_ptrT(new T(...))需要两次堆分配。该章节与 08-Considering_Performance.md 中的 Get rid ofnew 一节互相印证堆分配远贵于栈分配而智能指针尤其是unique_ptr在栈上只占一个指针大小几乎零开销工厂函数应返回unique_ptr不可复制、所有权唯一、更高效必要时再转换为shared_ptr。用std::array或std::vector替代 C 风格数组std::array定长栈上与std::vector变长堆上都保证元素在内存中连续布局并且在绝大多数场景下可以也应该彻底取代 C 风格数组——原因与上文不用裸指针完全一致它们自带大小信息、边界检查能力at()、RAII 生命周期管理不会退化为裸指针。额外警告不要用std::shared_ptr持有数组。shared_ptr默认以delete而非delete[]释放资源用它托管 C 风格数组极易造成错误释放数组场景请用unique_ptrT[]或直接使用std::array/std::vector。此外从 08-Considering_Performance.md 的角度看shared_ptr的复制需要原子地更新引用计数比想象中昂贵得多能用unique_ptr就不要用shared_ptr。使用异常而非可忽略的返回值异常有一个返回值无法比拟的特性不可被忽略。基于返回值的错误报告如boost::optional、错误码可以被调用方随手丢弃一旦被忽略后续对无效值的解引用/使用就可能引发崩溃或内存错误。而异常一旦抛出就必然被某个层级的 catch 捕获并处理——可以一路向上传播到应用最顶层在那里统一记录日志并自动重启应用。C 之父 Bjarne Stroustrup 在关于为什么用异常的经典 FAQ 中详细阐述了这一论点04-Considering_Safety.md 引用了该讨论异常将错误检测与错误处理分离强制调用链对错误做出响应。需要同时说明的是平衡之道异常适合报告罕见、真正异常的错误路径。08-Considering_Performance.md 提醒若在正常处理流程中频繁地抛出并捕获内部异常会拖慢执行速度并破坏调试器体验调试器会监视并报告每次异常事件。因此正确姿势是用异常处理不该发生的事用普通控制流处理经常发生的事。用 C 风格转换代替 C 风格转换C 风格转换(type)expr几乎不做检查任何类型之间都可以强行转换而 C 风格转换static_cast、dynamic_cast等允许编译器做更多检查安全性显著更高// Bad Idea double x getX(); int i (int) x; // Not a Bad Idea int i static_castint(x);此外C 风格转换在代码中更醒目且具有可搜索性——static_castint可以全局检索定位而(int)形式的转换散布在代码中几乎无法追踪。对double→int这类有损转换原文档特别提醒先想清楚程序逻辑是否需要额外处理溢出与下溢overflow/underflow。原文用一句Measure three times and cut 0.9999999999981 times量三次剪 0.9999999999981 次点出不要想当然地对浮点转整型做无检查的截断必要时应在转换前显式校验值域。这一话题与 09-Considering_Correctness.md 的避免无类型接口同源类型系统是 C 最强的安全屏障之一正确的类型选择static_cast、强类型封装既减少运行时风险也给编译器更多优化机会。不要定义可变参数函数variadic function可变参数函数能接受任意数量、任意类型的参数最著名的例子是printf()。虽然语言允许你自定义这类函数但这是一个潜在的安全风险点可变参数函数不是类型安全的——参数的类型检查被完全绕过传入错误的参数类型会导致未定义行为undefined behavior轻则程序终止重则被利用为安全漏洞典型的如格式化字符串漏洞未定义行为一旦出现程序的行为不再有任何保证可能被恶意输入利用。如果编译器支持 C11请用**可变参数模板variadic templates**取而代之——它在编译期保留完整的类型信息是类型安全的。需要说明的是04-Considering_Safety.md 也引用了其 GitHub 讨论issue #53指出在部分编译器上技术上有可能实现类型安全的 C 风格可变参数函数但这属于特例技巧不应作为默认方案——常规情况下直接选择可变参数模板即可。将安全实践纳入团队规范这份《Considering Safety》章节是 cppbestpractices 整部编码标准文档的一部分。该文档以可 fork 的编码标准定位见 01-Preface.md仓库中的全部章节02-Use_the_Tools_Available.md、03-Style.md、04-Considering_Safety.md 至 12-Final_Thoughts.md总目录见 00-Table_of_Contents.md共同构成一套可裁剪、可落地的团队规范。建议做法把本文的检查点const 完整性、返回值语义、无裸 new、无 C 风格转换、无自定义变参函数、用异常报告错误写入团队的代码评审 checklist并用 03-Style.md 推荐的.clang-format与静态分析工具见 02-Use_the_Tools_Available.md自动化强制执行让安全实践从个人习惯升级为工程基线。附加资源与延伸阅读原文档推荐的延伸阅读主题原文以链接形式给出此处转述其价值《How to Prevent The Next Heartbleed》作者 David Wheeler对当时代码安全现状的深入分析探讨如何系统性地保证代码安全。Heartbleed 正是裸内存访问 缺乏边界检查这类 C/C 经典问题的灾难性案例与本文避免裸内存访问用std::array/std::vector替代 C 数组的建议直接相关。在仓库内部还可交叉阅读以下章节深化理解07-Considering_Threadability.mdconst 返回值与线程安全的关联、MM 规则mutex 与 mutable 搭配08-Considering_Performance.mdmake_shared单次分配原理、unique_ptr优先于shared_ptr、避免过量异常09-Considering_Correctness.md类型安全接口设计与 C 风格转换章节互补03-Style.md成员初始化列表、const成员、命名惯例等风格层面的安全辅助。小结安全不是 C 的一个可选主题而是写出可靠代码的前提。本文所述的六条核心防线——最大化使用const、按场景谨慎选择返回类型、对简单类型按值传递、用智能指针替代裸内存访问、用标准容器替代 C 数组、用异常与 C 风格转换替代危险语言特性——每一条都能在你编译代码之前把一类常见 bug 挡在门外。它们彼此配合const支持编译器优化、智能指针杜绝泄漏、类型安全转换消除未定义行为共同构成一套完整、可立即执行、可写进团队评审清单的 C 安全编程基线。赞分享文档教程【免费下载链接】cppbestpracticesCollaborative Collection of C Best Practices. This online resource is part of Jason Turners collection of C Best Practices resources. See README.md for more information.项目地址https://gitcode.com/gh_mirrors/cp/cppbestpractices点击查看免费下载相关推荐如何安全处理Super Mario 64反编译项目中的指针类型转换完整实践指南如何安全处理Super Mario 64反编译项目中的指针类型转换完整实践指南 Super Mario 64反编译项目sm64是一个由开发者社区共同维护的游戏开发逆向工程Nemo SkillsNVIDIA大语言模型技能提升终极指南Nemo SkillsNVIDIA大语言模型技能提升终极指南 Nemo Skills是NVIDIA推出的大语言模型技能提升项目旨在通过丰富的工具和资源帮助开Ornith-1.0-9B智能编码代理从安装到第一个工具调用的完整指南Ornith 1.0 9B智能编码代理从安装到第一个工具调用的完整指南 Ornith 1.0 9B是一款开源的智能编码代理模型专为高效单GPU部署设计能帮上一篇10个Edge.js实战案例从数据库操作到图像处理下一篇Metro运行时系统如何在设备上执行打包后的代码创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑