現代C++
"現代 C++"通常指 C++11 以及之後的標準引入的一系列寫法和庫工具。它不是把舊 C++ 全部推翻,而是在原有語法之上,提供更安全、更清晰、更適合工程開發的表達方式。
本章按"由淺入深、從常用到進階"的順序組織:先學每天都會用到的語法,再學生命週期和資源管理,接着學回調、數據表達、時間、文件和併發,最後瞭解模塊化編譯。
本章例程約定
爲了方便你直接複製驗證,本章各小節的"示例代碼"遵循這些約定:
- 示例代碼都包含完整
#include。 - 示例代碼都包含
main函數。 - 示例代碼可以複製到單個
.cpp文件中編譯運行。 - 每個示例代碼下方都給出運行結果。
- 示例儘量一次只增加一個新概念,避免一個例子塞太多東西。
文中的"常見錯誤"用於說明不要這樣寫。爲了避免誤複製,錯誤寫法會盡量放在文字說明或短片段中,不當作可運行示例。
學習順序
| 階段 | 先解決的問題 | 對應章節 |
|---|---|---|
| 基礎表達 | 少寫重複類型,減少空指針和枚舉混用,遍歷更清楚 | auto、nullptr、using、enum class、範圍 for、結構化綁定 |
| 生命週期 | 資源什麼時候釋放,誰擁有對象,大對象怎麼高效傳遞 | constexpr、RAII、智能指針、右值引用和移動語義 |
| 回調和數據模型 | 小函數、回調保存、參數適配、可能沒有值、多種類型之一 | Lambda、std::function、std::bind、std::optional、std::variant、std::span |
| 系統能力 | 格式化輸出、時間、併發、文件系統、模塊化 | std::format / std::print、std::chrono、併發編程、std::filesystem、modules |
現代 C++ 的重點不是"語法更新",而是"把意圖寫清楚"。例如:
nullptr讓空指針和整數0分開。enum class讓不同枚舉類型不能隨便混用。- RAII 和智能指針把資源釋放交給對象生命週期。
- Lambda 讓局部回調寫在使用現場。
std::function把不同類型的可調用對象統一保存和傳遞。std::format/std::print讓格式化輸出更簡潔,並保持類型安全。
舊寫法和現代寫法的關係
很多現代 C++ 特性都來自一個樸素問題:舊寫法能用,但在複雜場景裏容易出錯。
| 舊寫法 | 主要問題 | 現代寫法 |
|---|---|---|
NULL / 0 表示空指針 | 會和整數重載混淆 | nullptr |
typedef 寫類型別名 | 模板別名不直觀 | using |
| 普通枚舉 | 枚舉名污染作用域,可隱式轉整數 | enum class |
手動 new/delete | 提前返回、異常、重複釋放都容易出錯 | RAII、std::unique_ptr、std::shared_ptr |
手動 lock/unlock | 忘記解鎖會死鎖,異常路徑更危險 | std::lock_guard、std::unique_lock |
| 遠處定義普通函數做簡單回調 | 局部邏輯被拆散,不能方便攜帶上下文 | Lambda |
| 只用函數指針保存回調 | 不能保存有捕獲的 lambda 和函數對象 | std::function |
| 手寫適配函數調整參數 | 代碼重複,舊接口適配麻煩 | std::bind 或 Lambda |
| 用特殊值表示失敗 | -1、空字符串等魔法值語義不清 | std::optional |
用 union 表示多種類型 | 訪問錯誤類型會產生未定義行爲 | std::variant |
| 手寫時間單位換算 | 秒、毫秒、微秒容易混 | std::chrono |
| 平臺相關文件 API | Windows/Linux 寫法不同 | std::filesystem |
學習每個特性時,可以按三個問題來理解:
- 舊寫法哪裏容易錯?
- 現代寫法如何把意圖寫進類型或語法裏?
- 在什麼場景下差異才明顯?
比如一個智能指針示例如果只在 main 裏創建對象然後正常結束,看起來和手動 delete 差不多;一旦出現提前 return、異常、跨函數傳遞,RAII 的價值就會立刻顯現。一個 Lambda 如果隻立刻調用,按值捕獲和按引用捕獲都可能沒事;一旦保存到回調、線程、定時器裏,生命週期差異就會變得非常重要。
推薦學習路線
讓代碼更清楚
先學習 auto、nullptr、using、enum class、範圍 for 和結構化綁定。這些內容難度不高,但能明顯減少冗長代碼和低級錯誤。
這一階段重點記住:
auto不是弱類型,只是讓編譯器幫你寫類型。nullptr只表示空指針,不表示整數。enum class默認不和整數混用。- 範圍 for 默認用
const auto&遍歷大對象。 - 結構化綁定適合解包
pair、tuple、結構體和map元素。
理解生命週期
接着學習 constexpr、RAII、智能指針和移動語義。這一階段比語法更重要的是思維方式:對象什麼時候創建,什麼時候銷燬,誰擁有它,誰只是借用它。
建議按這個順序理解:
- RAII:資源和對象生命週期綁定。
unique_ptr:獨佔所有權,默認選擇。shared_ptr:確實需要多個所有者時才用。weak_ptr:只觀察,不延長生命週期,用來打破循環引用。- 移動語義:把資源轉移出去,避免大對象深拷貝。
掌握回調和數據表達
Lambda、std::function、std::bind 三章要連起來看,但不要混在一起背:
| 工具 | 解決的問題 | 重點 |
|---|---|---|
| Lambda | 在使用現場寫一個小函數 | 捕獲列表、參數、返回值、生命週期 |
std::function | 統一保存和傳遞不同類型的可調用對象 | 類型擦除、回調成員變量、回調容器 |
std::bind | 適配已有函數的參數 | 固定參數、調整順序、綁定成員函數 |
std::optional、std::variant、std::span 則分別解決"可能沒有值"、"幾種類型之一"、"借用一段連續數據"的問題。
走向工程能力
最後學習格式化輸出、時間、併發、文件系統和 modules。這些內容更接近真實項目:
std::format/std::print比stringstream簡潔,比printf類型安全。std::chrono避免手寫時間單位換算。- 併發編程要先保證正確,再考慮速度。
std::filesystem讓路徑、目錄、文件信息處理跨平臺。- modules 是 C++20 引入的新編譯模型,目前應先理解概念,再根據工具鏈支持決定是否使用。
實戰練習建議
- 學
nullptr:寫兩個重載函數,分別接收int和int*,觀察0、NULL、nullptr的區別。 - 學
enum class:把普通枚舉換成枚舉類,看看哪些隱式轉換被禁止。 - 學智能指針:先寫一個提前
return的手動new/delete例子,再改成unique_ptr。 - 學 Lambda:分別用按值捕獲和按引用捕獲,觀察外部變量是否變化。
- 學
std::function:把多個不同 lambda 放進同一個vector,統一執行。 - 學併發:讓兩個 1 秒等待任務先順序執行,再放進兩個線程中執行,對比耗時。