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)
## 文檔基礎信息
- 作者: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. 詳細總結
一、文檔基礎信息
- 作者背景:Arne Mertz(@arne_mertz),軟件工程師,主要專注嵌入式領域,擁有近20年C++學習經驗,同時是C++與可維護代碼的培訓師。
- 代碼示例説明:
- 所有示例均來自開源代碼(如SFML網球示例、Qt主窗口示例、libsass、LeddarSDK等);
- 示例目的是展示代碼異味的普遍性,而非批評特定開發者或代碼;
- 多數示例非生產代碼(如使用示例),但仍需具備可維護性與可讀性。
- 代碼異味定義(源自Martin Fowler):
- 本質:系統深層問題的表面跡象;
- 核心特徵:
- 相對易識別;
- 並非實際問題本身;
- 不總是構成問題;
- 常違反設計原則;
- 缺失合適的模式、慣用法或抽象;
- 最終導致可維護性問題。
- 參考鏈接:https://martinfowler.com/bliki/CodeSmell.html
二、8類核心C++代碼異味分析(含特徵、問題、修復方法)
|
代碼異味類型
|
表面跡象
|
深層問題
|
修復方法
|
關鍵示例/説明
|
|
長函數 |
函數代碼行數過多;含標記“功能”的單行註釋塊(如“// Create the ball”)
|
違反單一職責原則、單一抽象層級原則 |
1. 提取獨立函數(複用並非函數提取的唯一目的);2. 註釋塊可作為函數名參考;3. 複雜功能數據考慮封裝為類
|
Qt主窗口示例中 |
|
過早泛化 |
1. 存在無用/未使用的參數、回調;2. 僅單一類型實例化的模板;3. 僅1個派生類的基類(依賴倒置除外)
|
違反KISS(保持簡單)、**YAGNI(無需過度設計)**原則;設計複雜、維護難;測試用例冗餘或缺失 |
保持設計“儘可能簡單,但不過度簡化”
|
不必要的模板類(僅支持int類型卻設計為模板);多餘的回調參數(從未被調用)
|
|
深層嵌套控制流 |
多層循環與條件嵌套(如while嵌套for,再嵌套if判斷按鍵事件)
|
1. 難追蹤代碼執行路徑(“如何到達當前邏輯”);2. 違反單一職責與單一抽象層級原則;3. 常與長函數共存
|
1. 提取嵌套邏輯為獨立函數;2. 條件倒置實現提前返回(如 |
SFML網球示例中 |
|
複雜表達式 |
長且多條件的判斷表達式(如球與球拍碰撞判斷含4個變量比較)
|
違反單一抽象層級原則;可讀性差、易出錯 |
1. 提取中間變量(如 |
球與左球拍碰撞判斷:原表達式含4個比較條件,修復後拆分為多箇中間變量與布爾判斷
|
|
缺少const/constexpr |
1. 可標記為const的函數/對象未標記(如返回成員變量的非const函數 |
1. 語義模糊(無法判斷是否可修改);2. 易發生意外修改
|
1. 成員函數無修改操作時標記為const;2. 編譯期確定的常量用constexpr;3. Jason Turner觀點:任何缺少const的情況都是代碼異味 |
libsass示例中 |
|
缺失RAII |
手動管理資源(如手動 |
1. 資源泄漏(如異常導致未執行 |
1. 使用標準庫RAII類( |
LeddarSDK示例中手動管理 |
|
違反“五法則” |
僅定義“五大函數”(析構、拷貝構造、拷貝賦值、移動構造、移動賦值)中的1個或部分,其餘依賴編譯器生成
|
編譯器生成的函數可能引發意外錯誤(如淺拷貝導致雙重釋放)
|
需定義其中1個時,其餘優先用 |
|
|
原生循環 |
使用原生for循環遍歷(如查找列表中指定索引的操縱桿、拷貝員工數據),未用標準庫算法
|
代碼冗餘、可讀性差;未利用C++標準庫優化
|
1. 優先用基於範圍的for循環;2. 使用 |
查找操縱桿:原for循環遍歷 |
三、工具與補充資源
- 推薦工具(檢測與修復代碼異味):
- 編譯器警告:啓用
-Wall(所有警告)、-Werror(警告視為錯誤)、-pedantic(嚴格遵循標準); - 性能與分析工具:優化器、代碼分析器;
- 靜態分析工具:
clang-tidy、cppcheck; - 測試工具:
sanitizers(如地址 sanitizer,檢測內存問題); - IDE工具:重構工具(如函數提取、變量重命名)。
- 補充學習資源:
- Jason Turner:CppCon 2019 演講 “C++ Code Smells”;
- Kate Gregory:CppCon 2019 演講 “Naming is Hard: Let’s Do Better”、ACCUConf 2022 演講 “Abstraction Patterns: Making Code Reliably Better Without Deep Understanding”;
- 網站:https://sourcemaking.com/refactoring/smells(重構與代碼異味參考)。
四、核心結論
- 普遍性:代碼異味在所有代碼庫中都存在,文檔中的示例代碼未必是“壞代碼”;
- 非錯誤屬性:代碼異味不總是“錯誤”,部分情況無需立即修復(需結合實際維護需求判斷);
- 版本兼容性:即使無法使用C++11及以上版本,也可通過合理設計避免代碼異味;
- 核心目標:識別與修復代碼異味的最終目的是提升代碼可維護性與可讀性,而非追求“完美代碼”。
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. 識別資源邊界:梳理舊代碼中資源的“獲取-釋放”邏輯(如
lSensor的new與delete、Disconnect()調用),標記所有資源操作點;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個異味)。