C++ 常见代码异味(Code Smells)

0. 信息来源

https://github.com/arnemertz/presentations/tree/main/IdentifyingCommonCodeSmells

详细的Rule检查可以看这个列表:https://rules.sonarsource.com/cpp/

1. 一段话总结

该文档由软件工程师 Arne Mertz 撰写,聚焦 C++ 中的常见代码异味 ,首先依据 Martin Fowler 定义明确代码异味是系统深层问题的表面迹象(易识别、非实际问题、可能不构成问题但常违反原则/缺失模式/影响可维护性),随后通过 SFML 网球示例等开源代码片段,详细分析了长函数、过早泛化、深层嵌套控制流、复杂表达式、缺少 const/constexpr、缺失 RAII、违反"五法则"、原生循环这 8 类核心代码异味的表面特征、深层问题及修复方法,同时强调代码异味在各类代码库中普遍存在,并非必然是错误代码,且提及编译器警告、静态分析等工具对检测异味的作用,还推荐了 Jason Turner、Kate Gregory 等人的相关技术分享作为补充学习资源。


2. 思维导图(mindmap)

mindmap 复制代码
## 文档基础信息
- 作者:Arne Mertz(软件工程师,嵌入式领域为主,近20年C++学习经验,C++与可维护代码培训师)
- 代码示例来源:开源代码(如SFML网球示例、Qt示例、libsass、LeddarSDK等),非针对特定开发者或代码批评
- 核心定义:代码异味(Martin Fowler)- 系统深层问题的表面迹象,具易识别、非实际问题、可能不构成问题、违反原则、缺失模式/惯用法/抽象、影响可维护性特点
## 8类核心C++代码异味
- 长函数
  - 表面迹象:函数过长,含单行"功能"注释块
  - 深层问题:违反单一职责原则、单一抽象层级原则
  - 长度判断:无固定量化标准(10行可能过长,20行可能合适,100行大概率过长)
  - 修复方法:提取函数(复用非唯一目的,注释块可作函数名参考)、考虑为复杂功能数据创建类
- 过早泛化
  - 表面迹象:存在无用/未使用参数/回调、仅单一类型实例化的模板、仅一个派生类的基类(依赖倒置除外)
  - 深层问题:违反KISS(保持简单)、YAGNI(无需过度设计)原则,设计复杂、维护难、测试用例冗余或缺失
  - 修复方法:保持设计尽可能简单(不过度简化)
- 深层嵌套控制流
  - 表面迹象:多层循环/条件嵌套(如while嵌套for再嵌套if)
  - 深层问题:难追踪代码执行路径、违反单一职责原则与单一抽象层级原则,常与长函数并存
  - 修复方法:提取函数、条件倒置实现提前返回
- 复杂表达式
  - 表面迹象:长且多条件的判断表达式(如多变量比较的if条件)
  - 深层问题:违反单一抽象层级原则
  - 修复方法:提取中间变量、封装为函数
- 缺少const/constexpr
  - 表面迹象:可标记为const/constexpr的函数或对象未标记(如返回成员变量的非const函数)
  - 深层问题:语义模糊、易发生意外修改
  - 重要性:const可提升代码规范性、避免常见错误、促进算法使用(Jason Turner观点)
- 缺失RAII
  - 表面迹象:未利用RAII机制管理资源(如手动释放传感器、播放器资源)
  - 深层问题:资源泄漏、清理/重置错误
  - 修复方法:使用标准库RAII类(智能指针、锁等)、自定义类中用析构函数清理、编写RAII包装器
- 违反"五法则"
  - 表面迹象:仅定义"五大函数"(析构函数、拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符)中的部分,其余依赖编译器生成
  - 深层问题:编译器生成的函数可能引发意外错误(如浅拷贝问题)
  - 修复方法:需定义其中一个时,其余优先用=default(默认实现)或=delete(禁用)
- 原生循环
  - 表面迹象:使用原生for循环(如遍历列表查找元素、拷贝数据),未用标准库算法
  - 修复方法:优先用基于范围的for循环、<algorithm>库函数(如std::find_if、std::copy、std::transform)、C++20及以上用 ranges(如std::views::filter)
## 工具与补充资源
- 推荐工具:编译器警告(-Wall、-Werror、-pedantic)、优化器与分析器、静态分析工具(clang-tidy、cppcheck)、 sanitizers(测试时使用)、IDE重构工具
- 补充学习资源:Jason Turner(CppCon 2019 "C++ Code Smells")、Kate Gregory(CppCon 2019 "Naming is Hard: Let's Do Better"、ACCUConf 2022 "Abstraction Patterns...")、sourcemaking.com/refactoring/smells(重构与异味参考)
## 核心结论
- 代码异味在所有代码库中普遍存在,示例代码未必是劣质代码
- 代码异味不总是错误,无需立即全部修复
- 即使无法使用C++11及以上版本,代码也可避免异味

3. 详细总结

一、文档基础信息

  1. 作者背景:Arne Mertz(@arne_mertz),软件工程师,主要专注嵌入式领域,拥有近20年C++学习经验,同时是C++与可维护代码的培训师。
  2. 代码示例说明
    • 所有示例均来自开源代码(如SFML网球示例、Qt主窗口示例、libsass、LeddarSDK等);
    • 示例目的是展示代码异味的普遍性,而非批评特定开发者或代码;
    • 多数示例非生产代码(如使用示例),但仍需具备可维护性与可读性。
  3. 代码异味定义 (源自Martin Fowler):
    • 本质:系统深层问题的表面迹象
    • 核心特征:
      • 相对易识别;
      • 并非实际问题本身;
      • 不总是构成问题;
      • 常违反设计原则;
      • 缺失合适的模式、惯用法或抽象;
      • 最终导致可维护性问题
    • 参考链接:https://martinfowler.com/bliki/CodeSmell.html

二、8类核心C++代码异味分析(含特征、问题、修复方法)

代码异味类型 表面迹象 深层问题 修复方法 关键示例/说明
长函数 函数代码行数过多;含标记"功能"的单行注释块(如"// Create the ball") 违反单一职责原则单一抽象层级原则 1. 提取独立函数(复用并非函数提取的唯一目的);2. 注释块可作为函数名参考;3. 复杂功能数据考虑封装为类 Qt主窗口示例中newLetter()函数(含框架设置、格式定义、表格插入等多功能);10行可能过长,100行大概率过长
过早泛化 1. 存在无用/未使用的参数、回调;2. 仅单一类型实例化的模板;3. 仅1个派生类的基类(依赖倒置除外) 违反KISS(保持简单)、**YAGNI(无需过度设计)**原则;设计复杂、维护难;测试用例冗余或缺失 保持设计"尽可能简单,但不过度简化" 不必要的模板类(仅支持int类型却设计为模板);多余的回调参数(从未被调用)
深层嵌套控制流 多层循环与条件嵌套(如while嵌套for,再嵌套if判断按键事件) 1. 难追踪代码执行路径("如何到达当前逻辑");2. 违反单一职责与单一抽象层级原则;3. 常与长函数共存 1. 提取嵌套逻辑为独立函数;2. 条件倒置实现提前返回(如if (!isValid) return; SFML网球示例中while (window.isOpen())循环内嵌套事件处理、按键判断、游戏状态切换
复杂表达式 长且多条件的判断表达式(如球与球拍碰撞判断含4个变量比较) 违反单一抽象层级原则;可读性差、易出错 1. 提取中间变量(如ballLeftEdge = ball.getPosition().x - ballRadius);2. 封装为独立函数(如ballHitsLeftPaddle() 球与左球拍碰撞判断:原表达式含4个比较条件,修复后拆分为多个中间变量与布尔判断
缺少const/constexpr 1. 可标记为const的函数/对象未标记(如返回成员变量的非const函数getDbgFile());2. 可constexpr的常量未标记(如paddleSize用普通变量而非constexpr) 1. 语义模糊(无法判断是否可修改);2. 易发生意外修改 1. 成员函数无修改操作时标记为const;2. 编译期确定的常量用constexpr;3. Jason Turner观点:任何缺少const的情况都是代码异味 libsass示例中SharedObj类的getDbgFile()函数(仅返回成员变量却非const);ballRadius用普通float而非constexpr
缺失RAII 手动管理资源(如手动delete传感器、播放器对象,未用智能指针);资源释放逻辑重复(try与catch中重复写传感器断开与删除) 1. 资源泄漏 (如异常导致未执行delete);2. 清理/重置错误;3. 代码冗余 1. 使用标准库RAII类(std::unique_ptrstd::lock_guard);2. 自定义类用析构函数自动清理资源;3. 编写RAII包装器 LeddarSDK示例中手动管理lSensorlPlayer资源,try与catch中重复资源释放代码
违反"五法则" 仅定义"五大函数"(析构、拷贝构造、拷贝赋值、移动构造、移动赋值)中的1个或部分,其余依赖编译器生成 编译器生成的函数可能引发意外错误(如浅拷贝导致双重释放) 需定义其中1个时,其余优先用=default(保留默认实现)或=delete(禁用) LdCanKomodo类仅定义析构函数与Disconnect(),未处理拷贝/移动,可能导致资源重复释放
原生循环 使用原生for循环遍历(如查找列表中指定索引的操纵杆、拷贝员工数据),未用标准库算法 代码冗余、可读性差;未利用C++标准库优化 1. 优先用基于范围的for循环;2. 使用<algorithm>库函数(std::find_ifstd::copystd::transform);3. C++20+用rangesstd::views::filter 查找操纵杆:原for循环遍历joystickList,修复后用std::find_if;拷贝员工数据:原循环push_back,修复后用std::copy

三、工具与补充资源

  1. 推荐工具 (检测与修复代码异味):
    • 编译器警告:启用-Wall(所有警告)、-Werror(警告视为错误)、-pedantic(严格遵循标准);
    • 性能与分析工具:优化器、代码分析器;
    • 静态分析工具:clang-tidycppcheck
    • 测试工具:sanitizers(如地址 sanitizer,检测内存问题);
    • IDE工具:重构工具(如函数提取、变量重命名)。
  2. 补充学习资源

四、核心结论

  1. 普遍性 :代码异味在所有代码库中都存在,文档中的示例代码未必是"坏代码";
  2. 非错误属性:代码异味不总是"错误",部分情况无需立即修复(需结合实际维护需求判断);
  3. 版本兼容性:即使无法使用C++11及以上版本,也可通过合理设计避免代码异味;
  4. 核心目标 :识别与修复代码异味的最终目的是提升代码可维护性与可读性,而非追求"完美代码"。

4. 关键问题

问题1:在C++中,"长函数"作为常见代码异味,其判断标准并非固定行数,实际开发中如何结合代码逻辑准确识别长函数?修复时需遵循哪些核心原则?

答案

  • 识别方法 :无需依赖固定行数,重点从两方面判断:1. 功能集中度 :若函数内包含多个独立功能(如同时处理窗口创建、资源加载、逻辑计算),即使行数少(如20行)也可能是长函数;2. 注释特征:函数内存在标记"单一功能"的单行注释块(如"// Create the ball""// Load font"),说明代码可拆分为独立函数,属于长函数特征。
  • 修复核心原则 :1. 函数提取优先 :将注释块对应的逻辑提取为独立函数,函数名直接沿用注释语义(如createBall()"loadFont()"),且需明确"复用并非函数提取的唯一目的",提升可读性是关键;2. 抽象层级一致 :提取后的函数需保持与原函数抽象层级一致(如高层级的"初始化游戏"函数,内部调用的应是"创建球拍""加载音效"等同层级函数,而非直接操作像素坐标);3. 复杂数据封装 :若函数内涉及多变量协同的复杂功能(如球拍的尺寸、颜色、位置设置),可将数据与操作封装为类(如Paddle类),替代分散的变量与函数调用。

问题2:文档中提及"缺失RAII"是C++特有的代码异味,其可能导致资源泄漏等严重问题,实际开发中如何正确应用RAII机制?对于已有手动资源管理的旧代码,如何逐步重构以引入RAII?

答案

  • 正确应用RAII的方法 :1. 优先使用标准库RAII类 :资源管理优先选择C++标准库提供的RAII组件,如用std::unique_ptr/std::shared_ptr管理动态内存(替代new/delete)、std::lock_guard/std::unique_lock管理互斥锁(替代手动lock()/unlock())、std::fstream管理文件句柄(替代fopen()/fclose());2. 自定义RAII类 :对于标准库未覆盖的资源(如硬件设备句柄、网络连接),自定义类时需在构造函数中获取资源 (如LdCanKomodo类构造时初始化mHandle),析构函数中释放资源 (如析构时调用km_close(mHandle)),且需遵循"五法则"避免浅拷贝问题;3. 禁止手动释放 :RAII类封装后,禁止在外部手动调用资源释放接口(如Disconnect()),确保资源释放仅由析构函数触发。
  • 旧代码重构步骤 :1. 识别资源边界 :梳理旧代码中资源的"获取-释放"逻辑(如lSensornewdeleteDisconnect()调用),标记所有资源操作点;2. 局部封装 :先对独立资源(如单个传感器)创建简单RAII包装器(如SensorWrapper类,构造时new LSensor(),析构时Disconnect()+delete),替换旧代码中的手动管理;3. 消除冗余释放 :移除try-catch、函数返回前的重复释放逻辑(如原代码中try与catch均调用lSensor->Disconnect()),依赖RAII类的析构自动释放;4. 扩展到复杂资源 :逐步将多个关联资源(如传感器+播放器)封装为聚合RAII类(如DeviceManager),统一管理资源生命周期;5. 测试验证 :重构后通过sanitizers(如地址sanitizer)检测资源泄漏,确保RAII机制生效。

问题3:文档强调"代码异味不总是错误",在实际项目中如何判断某一代码异味(如原生循环、长函数)是否需要修复?修复时需平衡哪些因素?

答案

  • 代码异味修复判断标准 :1. 维护频率 :若异味代码所在模块是高频修改模块 (如业务逻辑层的订单处理函数),即使异味轻微(如20行的长函数)也需修复,避免后续修改时引入错误;若为低频修改的工具类(如仅初始化一次的配置读取函数),短期可暂不修复;2. 风险影响 :若异味可能引发严重问题(如"缺失RAII"导致内存泄漏、"违反五法则"导致浅拷贝),无论使用频率均需优先修复;若仅影响可读性(如简单的原生循环遍历),可根据团队优先级安排;3. 理解成本 :若新人接手时需超过30分钟理解该段代码(如深层嵌套的条件判断),说明异味已影响团队效率,需修复;4. 修改成本 :若修复需大量重构(如将旧C风格代码的原生循环改为ranges),且当前项目周期紧张,可记录为技术债务,待迭代间隙修复;若修复仅需提取1-2个函数(如20行长函数拆分为2个10行函数),可立即处理。
  • 修复平衡因素 :1. 可读性与性能 :修复时避免为追求"无异味"牺牲性能(如将简单原生循环改为复杂std::transform_if,但导致编译期变长或运行效率下降),需通过** Profiler 验证性能**,确保修复后性能无显著下降;2. 团队一致性 :若团队多数成员不熟悉ranges等高级特性,修复"原生循环"时可先选择std::for_each等易理解的算法,而非直接使用复杂语法;3. 兼容性 :若项目需兼容C++11以下版本,无法使用constexpr、智能指针等特性,可通过"伪RAII"(如手动管理但封装为函数)降低异味影响,而非强行使用高版本特性导致兼容性问题;4. 业务优先级:修复代码异味需与业务开发任务平衡,避免因修复异味导致业务上线延迟,可采用"小步修复"策略(如每次修改业务代码时顺带修复周边1-2个异味)。
相关推荐
老猿讲编程9 小时前
C++中的奇异递归模板模式CRTP
开发语言·c++
Yupureki11 小时前
从零开始的C++学习生活 16:C++11新特性全解析
c语言·数据结构·c++·学习·visual studio
紫荆鱼11 小时前
设计模式-迭代器模式(Iterator)
c++·后端·设计模式·迭代器模式
应茶茶12 小时前
C++11 核心新特性:从语法重构到工程化实践
java·开发语言·c++
-森屿安年-13 小时前
STL 容器:stack
开发语言·c++
charlee4413 小时前
最小二乘问题详解6:梯度下降法
c++·梯度下降·雅可比矩阵·非线性最小二乘·参数拟合
房开民13 小时前
OpenCV C++ 中,访问图像像素三种常用方法
c++·opencv·计算机视觉
报错小能手14 小时前
C++笔记(面向对象)深赋值 浅赋值
c++·笔记·学习
Maple_land14 小时前
编译器的“隐形约定”与本地变量:解锁Linux变量体系的关键密码
linux·运维·服务器·c++·centos