文章

从 C++98 到 C++23 —— 老 C++ 程序员的现代化升级指南

面向有 C++11 之前经验的开发者,系统梳理现代 C++ 的核心特性、开发范式变化和实战应用,帮你跨越十年技术断层

从 C++98 到 C++23 —— 老 C++ 程序员的现代化升级指南

如果你在 C++11 之前写过 C++,你熟悉的可能是这样的代码:手动 new/delete、原始指针满天飞、for(int i=0; i<v.size(); i++)、写模板时一屏编译错误、用 typedef 定义类型别名、boost 补标准库的缺……

当你再回来看现代 C++ 代码时,可能会觉得”这还是 C++ 吗?”

这篇文章帮你系统地跨越这道鸿沟。不是罗列所有新特性(那是几本书的量),而是聚焦在你日常开发中最常用、对写法影响最大的变化上。


版本时间线

先对齐一下年份和版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
C++98/03 ── 你熟悉的版本
    │
    │  (漫长的 8 年空白)
    │
C++11 ── 2011 ── 革命性版本!现代 C++ 的起点
C++14 ── 2014 ── C++11 的小修补
C++17 ── 2017 ── 实用特性大量加入
C++20 ── 2020 ── 又一次大升级(Concepts、Ranges、Coroutines)
C++23 ── 2023 ── 继续完善
C++26 ── 2026 ── 制定中

从 C++11 开始,每 3 年发布一个新版本
C++11 的重要程度 ≈ 重新发明了 C++

一、智能指针 —— 告别手动内存管理

1.1 以前:new/delete 的噩梦

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// C++98 风格:手动管理内存
void oldStyle() {
    Widget* w = new Widget();

    // ... 一堆代码 ...

    if (error) {
        delete w;    // 别忘了释放!
        return;      // 每个退出路径都要 delete
    }

    // ... 更多代码 ...

    doSomething(w);  // w 还有效吗?谁拥有它?
    delete w;        // 双重 delete?忘记 delete?
}

问题一大堆:忘记 delete 导致内存泄漏、多次 delete 导致崩溃、异常抛出时来不及 delete、不清楚”谁负责释放”……

1.2 现在:智能指针自动管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <memory>

// 现代 C++ 风格
void modernStyle() {
    // unique_ptr:独占所有权,离开作用域自动释放
    auto w = std::make_unique<Widget>();

    if (error) {
        return;  // 不需要手动释放!自动析构
    }

    doSomething(w.get());  // 传原始指针给不涉及所有权的函数

    // 函数结束,w 自动释放,不可能泄漏
}

1.3 三种智能指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ① unique_ptr —— 独占所有权(最常用,默认选它)
auto p1 = std::make_unique<Widget>();
// 不能复制,只能移动
// auto p2 = p1;              // ❌ 编译错误
auto p2 = std::move(p1);      // ✅ 所有权转移,p1 变空

// ② shared_ptr —— 共享所有权(引用计数)
auto p3 = std::make_shared<Widget>();
auto p4 = p3;                 // ✅ 引用计数 +1
// p3 和 p4 都销毁后,Widget 才被释放

// ③ weak_ptr —— 弱引用(不增加引用计数,解决循环引用)
std::weak_ptr<Widget> wp = p3;
if (auto sp = wp.lock()) {    // 尝试获取 shared_ptr
    sp->doSomething();         // 成功才能使用
}

1.4 现代 C++ 的内存管理原则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
新原则(重要!):

1. 永远不要写 new 和 delete
   → 用 make_unique / make_shared 替代

2. 默认用 unique_ptr
   → 只在真正需要共享所有权时用 shared_ptr

3. 函数参数传递规则:
   → 不涉及所有权:传引用(const Widget&)或原始指针(Widget*)
   → 要转移所有权:传 unique_ptr(按值或 std::move)
   → 要共享所有权:传 shared_ptr

4. 原始指针仍然可以用
   → 但只用于"借用",不用于"拥有"

二、auto 和类型推导 —— 让编译器帮你写类型

2.1 以前:类型写到手酸

1
2
3
// C++98:每个变量都要写完整类型
std::map<std::string, std::vector<int>>::iterator it = myMap.begin();
std::pair<std::string, int> result = getResult();

2.2 现在:auto 搞定

1
2
3
4
// 现代 C++:编译器自动推导类型
auto it = myMap.begin();         // 编译器知道它是什么类型
auto result = getResult();       // 自动推导返回类型
auto lambda = [](int x) { return x * 2; };  // Lambda 的类型无法手写

2.3 auto 的使用指南

1
2
3
4
5
6
7
8
9
// ✅ 适合用 auto 的场景
auto it = container.begin();           // 迭代器类型太长
auto ptr = std::make_unique<Widget>(); // 右侧已经明确类型
auto [key, value] = *it;              // 结构化绑定(C++17)
auto result = someFunction();          // 函数返回类型复杂

// ❌ 不建议用 auto 的场景
auto x = 42;        // int 直接写 int 就好,auto 反而不清晰
auto price = 9.99;  // 是 float 还是 double?写明确更好

2.4 结构化绑定(C++17)—— 解包利器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 以前:访问 pair/tuple 的元素
std::pair<int, std::string> p = {1, "hello"};
int id = p.first;
std::string name = p.second;

// 现在:一行解包
auto [id, name] = p;   // 直接拆开!

// 遍历 map 也变得优雅
std::map<std::string, int> scores = { {"Alice", 90}, {"Bob", 85} };

// 以前
for (auto it = scores.begin(); it != scores.end(); ++it) {
    std::cout << it->first << ": " << it->second << std::endl;
}

// 现在
for (const auto& [name, score] : scores) {
    std::cout << name << ": " << score << std::endl;
}

三、移动语义 —— 性能的免费午餐

3.1 以前的问题:不必要的复制

1
2
3
4
5
6
7
8
// C++98:返回一个大对象
std::vector<int> createLargeVector() {
    std::vector<int> v;
    // ... 往 v 里塞 100 万个元素 ...
    return v;   // 返回时:复制 100 万个元素!然后销毁原来的 v
}               // 白白浪费一次拷贝

std::vector<int> result = createLargeVector(); // 又一次拷贝

3.2 现在:移动而不是复制

1
2
3
4
5
6
7
8
9
10
11
12
13
// 移动语义的直觉:

// 拷贝 = 复印一份文件(耗时,占空间)
// 移动 = 把文件从一个文件夹拖到另一个文件夹(几乎免费)

std::vector<int> a = {1, 2, 3, 4, 5};

// 拷贝:a 的所有元素被复制到 b,a 不变
std::vector<int> b = a;            // 拷贝构造

// 移动:a 的内部指针直接交给 c,a 变为空
std::vector<int> c = std::move(a); // 移动构造,几乎零开销
// 此时 a 为空,c 拥有了原来 a 的数据

3.3 移动背后发生了什么

1
2
3
4
5
6
7
8
9
10
11
12
vector<int> 内部结构(简化):

拷贝前:
  a: [指针 → 堆上的数据: 1,2,3,4,5]  size=5  capacity=5

拷贝后(深拷贝,O(n)):
  a: [指针 → 堆上的数据: 1,2,3,4,5]  size=5  capacity=5
  b: [指针 → 新的堆数据: 1,2,3,4,5]  size=5  capacity=5   ← 分配新内存 + 复制

移动后(偷指针,O(1)):
  a: [指针 → nullptr]                size=0  capacity=0   ← 被掏空了
  c: [指针 → 堆上的数据: 1,2,3,4,5]  size=5  capacity=5   ← 直接拿走指针

3.4 右值引用(&&)

1
2
3
4
5
6
7
8
9
10
11
// 左值 vs 右值
int x = 42;          // x 是左值(有名字,有地址)
                     // 42 是右值(临时值,没名字)

// 右值引用:可以"绑定"到临时值
void process(std::string&& s) {
    // s 是一个右值引用,意味着"这个字符串即将被销毁,你可以偷它的数据"
    myString = std::move(s);  // 零拷贝接管
}

process("hello " + "world");  // 临时字符串直接移动进去,无需拷贝

3.5 你自己的类怎么支持移动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class MyBuffer {
    int* data;
    size_t size;
public:
    // 拷贝构造(以前就有)
    MyBuffer(const MyBuffer& other)
        : data(new int[other.size]), size(other.size) {
        std::copy(other.data, other.data + size, data);  // 真正复制数据
    }

    // 移动构造(C++11 新增)
    MyBuffer(MyBuffer&& other) noexcept
        : data(other.data), size(other.size) {  // 偷指针
        other.data = nullptr;                    // 把原对象置空
        other.size = 0;
    }

    // 移动赋值运算符
    MyBuffer& operator=(MyBuffer&& other) noexcept {
        if (this != &other) {
            delete[] data;          // 释放自己的资源
            data = other.data;      // 偷对方的
            size = other.size;
            other.data = nullptr;   // 把对方置空
            other.size = 0;
        }
        return *this;
    }

    ~MyBuffer() { delete[] data; }
};

3.6 实际影响

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
现代 C++ 中,以下操作都已经很高效(不再有额外拷贝):

// 返回大对象 —— 编译器 RVO + 移动语义
std::vector<int> createVector() {
    std::vector<int> v(1000000);
    return v;   // 零拷贝(RVO 直接构造到调用方,连移动都省了)
}

// 往容器里插入临时对象
std::vector<std::string> v;
v.push_back("hello");          // 临时 string 被移动进容器
v.emplace_back("hello");       // 更好:直接在容器内部构造,连移动都省了

// std::move 在容器操作中
std::string s = "very long string...";
v.push_back(std::move(s));     // s 被移动进容器,不拷贝
// s 现在为空

四、Lambda 表达式 —— 函数也能当变量传

4.1 以前:函数指针和 functor 的痛苦

1
2
3
4
5
6
7
8
9
10
11
12
13
// C++98:要传一个"自定义行为"给算法,选择很少

// 方式 1:函数指针
bool compare(int a, int b) { return a > b; }
std::sort(v.begin(), v.end(), compare);

// 方式 2:函数对象(functor),写一个完整的类
struct Compare {
    bool operator()(int a, int b) const { return a > b; }
};
std::sort(v.begin(), v.end(), Compare());

// 要写的东西太多了,就为了一个简单的"从大到小排序"

4.2 现在:Lambda 一行搞定

1
2
3
// 现代 C++:Lambda 表达式
std::sort(v.begin(), v.end(), [](int a, int b) { return a > b; });
//                             ↑ 就地定义一个匿名函数

4.3 Lambda 的完整语法

1
2
3
4
5
6
7
8
9
10
11
12
// Lambda 的组成部分:
// [捕获列表](参数列表) -> 返回类型 { 函数体 }

auto greet = [](const std::string& name) -> std::string {
    return "Hello, " + name;
};
std::cout << greet("World");  // "Hello, World"

// 返回类型通常可以省略(自动推导)
auto greet = [](const std::string& name) {
    return "Hello, " + name;
};

4.4 捕获列表:Lambda 的核心特色

Lambda 可以”捕获”它外面的变量,这是函数指针做不到的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int threshold = 10;
std::string prefix = "Item: ";

// 按值捕获 [=]:复制一份外部变量
auto f1 = [=](int x) { return x > threshold; };

// 按引用捕获 [&]:直接引用外部变量
auto f2 = [&](int x) {
    threshold = x;  // 可以修改外部变量
};

// 指定捕获方式
auto f3 = [threshold, &prefix](int x) {
    // threshold 是副本(不能修改原始值)
    // prefix 是引用(可以修改原始值)
    prefix += std::to_string(x);
    return x > threshold;
};

// C++14:初始化捕获(可以移动对象进 Lambda)
auto ptr = std::make_unique<Widget>();
auto f4 = [p = std::move(ptr)]() {
    p->doSomething();  // unique_ptr 被移动进来了
};

4.5 Lambda 的实际应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// ① STL 算法配合 Lambda(最常见)
auto it = std::find_if(users.begin(), users.end(),
    [](const User& u) { return u.age > 18; });

// ② 回调函数
button.onClick([&]() {
    statusBar.setText("Button clicked!");
});

// ③ 自定义排序
std::sort(students.begin(), students.end(),
    [](const Student& a, const Student& b) {
        return a.gpa > b.gpa;  // 按 GPA 从高到低
    });

// ④ 立即执行(IIFE,用于复杂初始化)
const auto config = [&]() {
    Config c;
    c.loadFromFile("config.json");
    c.validate();
    return c;
}();  // 注意末尾的 (),表示立即调用

五、范围 for 循环 和 Range-based 操作

5.1 循环的进化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// C++98:下标循环
for (int i = 0; i < vec.size(); i++) {
    std::cout << vec[i] << std::endl;
}

// C++98:迭代器循环
for (std::vector<int>::iterator it = vec.begin(); it != vec.end(); ++it) {
    std::cout << *it << std::endl;
}

// C++11:范围 for 循环
for (const auto& item : vec) {
    std::cout << item << std::endl;
}
// 简洁、安全、不会越界

5.2 Ranges(C++20)—— 链式操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <ranges>

std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

// 以前:过滤偶数 → 乘以 2 → 取前 3 个
std::vector<int> result;
for (int n : numbers) {
    if (n % 2 == 0) {
        result.push_back(n * 2);
        if (result.size() == 3) break;
    }
}
// result = {4, 8, 12}

// C++20 Ranges:管道风格,像流水线一样
auto result = numbers
    | std::views::filter([](int n) { return n % 2 == 0; })  // 过滤偶数
    | std::views::transform([](int n) { return n * 2; })    // 每个乘 2
    | std::views::take(3);                                   // 取前 3 个
// result 是一个惰性视图:{4, 8, 12}
// 关键:中间没有创建任何临时容器!

六、字符串和容器的改进

6.1 字符串字面量和 string_view

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// C++98
const char* s1 = "hello";           // C 字符串
std::string s2 = "hello";           // 需要堆分配

// C++14 自定义字面量
using namespace std::string_literals;
auto s3 = "hello"s;                 // 直接是 std::string 类型
auto s4 = "hello"sv;                // std::string_view(C++17)

// string_view:只读的字符串引用,零拷贝
void printName(std::string_view name) {  // 不管传入什么类型都不拷贝
    std::cout << name << std::endl;
}
printName("hello");           // ✅ C 字符串
printName(std::string("hi")); // ✅ std::string
printName(someStringView);    // ✅ string_view

6.2 新增容器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// C++11 新增的容器

// 无序容器(哈希表实现,O(1) 查找)
std::unordered_map<std::string, int> hashMap;   // 以前只有 std::map(红黑树,O(log n))
std::unordered_set<int> hashSet;

// 固定大小数组
std::array<int, 5> arr = {1, 2, 3, 4, 5};     // 替代 C 数组 int arr[5]
// 支持 .size()、.begin()、越界检查等

// 元组
auto t = std::make_tuple(1, "hello", 3.14);
auto [i, s, d] = t;  // C++17 结构化绑定

// C++17 新增
std::optional<int> maybeValue;      // 可能有值,也可能没有(替代"用 -1 表示无效")
std::variant<int, std::string> v;   // 类型安全的 union
std::any a;                         // 可以存任何类型

6.3 optional —— 告别”魔术值”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 以前:用特殊值表示"没有结果"
int findIndex(const std::vector<int>& v, int target) {
    for (int i = 0; i < v.size(); i++) {
        if (v[i] == target) return i;
    }
    return -1;  // -1 表示没找到,但谁规定 -1 就是无效的?
}

// 现在:用 optional 明确表达"可能没有"
std::optional<int> findIndex(const std::vector<int>& v, int target) {
    for (int i = 0; i < v.size(); i++) {
        if (v[i] == target) return i;
    }
    return std::nullopt;  // 明确表示"没有值"
}

// 使用
if (auto idx = findIndex(v, 42)) {  // optional 可以隐式转 bool
    std::cout << "Found at " << *idx << std::endl;
} else {
    std::cout << "Not found" << std::endl;
}

七、constexpr —— 编译期计算

7.1 以前:运行时算 vs 宏

1
2
3
4
5
// C++98:编译期常量只能用宏或 enum
#define MAX_SIZE 1024
enum { BUFFER_SIZE = 256 };

// 宏的问题:没有类型检查、不受作用域限制、调试困难

7.2 现在:constexpr 让函数在编译时运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// C++11/14/17:constexpr 函数
constexpr int factorial(int n) {
    if (n <= 1) return 1;
    return n * factorial(n - 1);
}

// 编译期就算好了!不占运行时间
constexpr int result = factorial(10);  // 编译期结果 = 3628800
static_assert(factorial(5) == 120);    // 编译期断言

// constexpr if(C++17)—— 编译期条件分支
template<typename T>
auto process(T value) {
    if constexpr (std::is_integral_v<T>) {
        return value * 2;        // 整数走这个分支
    } else {
        return value + 0.5;      // 浮点数走这个分支
    }
    // 不满足条件的分支在编译期就被丢弃,不会产生代码
}

八、并发编程 —— 标准库终于有了线程支持

8.1 以前:平台相关的 API

1
2
3
4
// C++98:没有标准线程库
// Windows: CreateThread / WaitForSingleObject
// Linux:   pthread_create / pthread_join
// 写一份跨平台多线程代码非常痛苦

8.2 现在:标准线程库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <thread>
#include <mutex>
#include <future>

// 创建线程
std::thread t([]() {
    std::cout << "Hello from thread!" << std::endl;
});
t.join();  // 等待线程结束

// 互斥锁(用 lock_guard 自动管理)
std::mutex mtx;
void safeIncrement(int& counter) {
    std::lock_guard<std::mutex> lock(mtx);  // 构造时加锁,析构时自动解锁
    counter++;
    // 不需要手动 unlock,异常安全
}

// C++17:scoped_lock(可同时锁多个互斥量,防止死锁)
std::scoped_lock lock(mutex1, mutex2);

// async + future:异步执行并获取结果
auto future = std::async(std::launch::async, []() {
    // 在另一个线程中执行
    return computeExpensiveResult();
});

// 继续做其他事情...

auto result = future.get();  // 需要结果时再等待

8.3 原子操作

1
2
3
4
5
6
7
8
9
#include <atomic>

// 以前:手动加锁保护一个简单的计数器
// 现在:原子变量,无锁线程安全
std::atomic<int> counter{0};

// 多个线程可以安全地操作
counter++;            // 原子操作,无需加锁
counter.fetch_add(5); // 原子加 5

九、模板和泛型编程的进化

9.1 以前的模板:编译错误天书

1
2
3
4
// C++98 模板错误信息的噩梦:
// 你写了 std::sort(myList.begin(), myList.end())
// 编译器吐出 200 行错误,核心意思是 list 不支持随机访问迭代器
// 但你需要从天书般的错误信息中自己找出来

9.2 Concepts(C++20)—— 模板约束

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// C++20:用 Concepts 约束模板参数
template<typename T>
concept Sortable = requires(T a) {
    { a.begin() } -> std::random_access_iterator;
    { a.size() } -> std::convertible_to<size_t>;
};

template<Sortable Container>
void mySort(Container& c) {
    std::sort(c.begin(), c.end());
}

mySort(myVector);  // ✅ vector 支持随机访问
mySort(myList);    // ❌ 编译错误:清晰地告诉你 list 不满足 Sortable 约束

9.3 变参模板(C++11)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 以前:要写多个重载来处理不同数量的参数
void print(int a) { std::cout << a; }
void print(int a, int b) { std::cout << a << b; }
void print(int a, int b, int c) { std::cout << a << b << c; }
// ... 无穷无尽

// 现在:变参模板,任意数量、任意类型
template<typename... Args>
void print(Args... args) {
    (std::cout << ... << args) << std::endl;  // C++17 折叠表达式
}

print(1);                    // 1
print(1, " hello ", 3.14);  // 1 hello 3.14

9.4 类型特征(Type Traits)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <type_traits>

// 编译期检查类型属性
static_assert(std::is_integral_v<int>);           // int 是整数类型
static_assert(std::is_floating_point_v<double>);   // double 是浮点类型
static_assert(std::is_same_v<decltype(42), int>);  // 42 的类型是 int

// 条件编译
template<typename T>
void process(T value) {
    if constexpr (std::is_pointer_v<T>) {
        std::cout << "pointer: " << *value << std::endl;
    } else {
        std::cout << "value: " << value << std::endl;
    }
}

十、错误处理的进化

10.1 以前:错误码 + 异常混用

1
2
3
4
5
// C++98:错误处理方式不统一
int result = doSomething();
if (result == -1) { /* 错误 */ }     // 错误码
if (result == NULL) { /* 错误 */ }   // 空指针
// 还有 errno、throw、assert... 风格不统一

10.2 现在:更丰富的工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// ① optional:表示"可能没有值"
std::optional<User> findUser(int id);

// ② noexcept:承诺不抛异常
void fastOperation() noexcept;  // 编译器可以据此优化
// 移动构造函数应该标 noexcept

// ③ [[nodiscard]]:强制调用方处理返回值
[[nodiscard]] ErrorCode init();
init();  // ⚠️ 编译器警告:你忽略了返回值!

// ④ std::expected(C++23):返回值或错误
std::expected<User, ErrorCode> findUser(int id) {
    if (id <= 0) return std::unexpected(ErrorCode::InvalidId);
    return User{id, "Alice"};
}

auto result = findUser(42);
if (result) {
    std::cout << result->name;
} else {
    handleError(result.error());
}

十一、构建工具和项目管理

11.1 CMake 成为事实标准

1
2
3
4
5
你那个时代                    现在

手写 Makefile               → CMake(事实标准)
IDE 管理构建                → CMakeLists.txt + 任意 IDE
./configure && make         → cmake -B build && cmake --build build
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 现代 CMake(CMakeLists.txt)
cmake_minimum_required(VERSION 3.20)
project(MyApp LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 20)           # 使用 C++20
set(CMAKE_CXX_STANDARD_REQUIRED ON)

add_executable(myapp
    src/main.cpp
    src/widget.cpp
)

# 链接第三方库
find_package(fmt REQUIRED)
target_link_libraries(myapp PRIVATE fmt::fmt)

11.2 包管理器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
以前:手动下载源码、编译、配置路径
     或者把第三方库源码直接放进项目

现在:
├── vcpkg      → 微软出品,最流行
│   vcpkg install fmt nlohmann-json spdlog
│
├── Conan      → 分布式包管理器
│   conan install . --build=missing
│
└── CMake FetchContent → 直接在 CMakeLists.txt 里声明依赖
    FetchContent_Declare(fmt
        GIT_REPOSITORY https://github.com/fmtlib/fmt
        GIT_TAG 10.2.1
    )
    FetchContent_MakeAvailable(fmt)

11.3 必备的现代 C++ 第三方库

1
2
3
4
5
6
7
8
9
库名              用途                    替代了什么

fmt              格式化输出              printf / iostream
spdlog           日志                    手写日志或 log4cxx
nlohmann/json    JSON 解析               手动拼字符串
Catch2 / GTest   单元测试                手写测试
{fmt}            已进入 C++20 std::format
doctest          轻量级测试框架
abseil           Google 的 C++ 基础库

十二、编码风格的变迁

12.1 新旧风格对照表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// ═══════ 变量初始化 ═══════

// 以前
int x = 42;
Widget w(arg1, arg2);
int arr[] = {1, 2, 3};

// 现在(统一初始化 / 花括号初始化)
int x{42};
Widget w{arg1, arg2};
std::vector<int> v{1, 2, 3};
// 花括号初始化可以防止窄化转换
int x{3.14};  // ❌ 编译错误,double 到 int 会丢失精度


// ═══════ 空指针 ═══════

// 以前
int* p = NULL;
if (p == 0) { ... }

// 现在
int* p = nullptr;      // 类型安全的空指针
if (p == nullptr) { ... }


// ═══════ 类型别名 ═══════

// 以前
typedef std::vector<std::pair<std::string, int>> UserList;

// 现在(更清晰,支持模板)
using UserList = std::vector<std::pair<std::string, int>>;

// using 还能做模板别名(typedef 做不到)
template<typename T>
using Vec = std::vector<T>;
Vec<int> numbers;


// ═══════ 枚举 ═══════

// 以前(枚举值泄漏到外层作用域)
enum Color { RED, GREEN, BLUE };
int x = RED;  // RED 污染了全局命名空间

// 现在(强类型枚举)
enum class Color { Red, Green, Blue };
Color c = Color::Red;  // 不会污染命名空间
// int x = Color::Red;  // ❌ 不能隐式转换为 int


// ═══════ 函数返回多值 ═══════

// 以前(输出参数)
bool getSize(int& width, int& height);

// 现在(返回结构体或 tuple)
struct Size { int width; int height; };
Size getSize();

auto [w, h] = getSize();  // C++17 结构化绑定

十三、学习路径建议

13.1 分阶段学习

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
第一阶段:最核心的改变(1-2 周)
├── auto 和类型推导
├── 范围 for 循环
├── nullptr(替代 NULL)
├── 智能指针(unique_ptr / shared_ptr)
├── Lambda 表达式
└── 花括号初始化 / enum class / using

  → 学完这些,你的代码就已经"现代化"了

第二阶段:性能和设计(2-3 周)
├── 移动语义和右值引用
├── string_view
├── optional / variant
├── constexpr
└── 标准线程库

  → 学完这些,你能写出高效的现代代码

第三阶段:高级特性(按需)
├── 变参模板 + 折叠表达式
├── Concepts(C++20)
├── Ranges(C++20)
├── Coroutines(C++20)
├── Modules(C++20,编译速度革命)
└── std::format / std::expected(C++23)

  → 按实际需要挑选学习

13.2 推荐资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
入门书籍:
  《Effective Modern C++》—— Scott Meyers
    最经典的现代 C++ 指南,42 条实践建议
    覆盖 C++11/14,是你的第一本书

  《A Tour of C++》—— Bjarne Stroustrup(C++ 之父)
    快速概览整个现代 C++,薄且精

进阶:
  《C++ Templates: The Complete Guide》第 2 版
    模板元编程的圣经

在线资源:
  cppreference.com       —— 最权威的 C++ 参考手册
  godbolt.org            —— 在线编译器,看汇编输出
  cppinsights.io         —— 看编译器如何展开你的代码
  isocpp.org             —— C++ 标准委员会官网

13.3 从今天开始的最小改动清单

如果你有一个老项目,以下是投入最小、收益最大的改动:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
优先级 1(立即改):
  ├── new/delete   →  make_unique / make_shared
  ├── NULL         →  nullptr
  ├── typedef      →  using
  └── 传统 for     →  范围 for

优先级 2(重构时改):
  ├── 返回值      →  考虑移动语义
  ├── 回调函数    →  Lambda
  ├── enum        →  enum class
  └── 老式 cast   →  static_cast / dynamic_cast

优先级 3(新代码默认用):
  ├── auto(迭代器、工厂函数返回值)
  ├── constexpr(编译期常量)
  ├── optional(可能无值的返回)
  ├── string_view(只读字符串参数)
  └── [[nodiscard]](不该忽略的返回值)

总结

1
2
3
4
5
6
7
8
9
10
11
12
13
14
C++98                          现代 C++(C++11 → C++23)

new / delete                →  智能指针(make_unique)
手写类型                    →  auto 推导
for(i=0;i<n;i++)            →  for(auto& x : container)
函数指针 / functor           →  Lambda
手动内存拷贝                 →  移动语义(std::move)
NULL                        →  nullptr
typedef                     →  using
enum                        →  enum class
平台相关线程 API             →  std::thread / async
手写 Makefile               →  CMake + vcpkg
Boost 补缺                  →  标准库已吸收大部分 Boost 功能
模板错误天书                 →  Concepts(C++20)

一句话总结:现代 C++ 的核心哲学是——把资源管理交给编译器(RAII + 智能指针),把类型推导交给编译器(auto),把性能优化交给编译器(移动语义 + constexpr),你只需要清晰地表达意图。

从 C++11 到 C++23 的每一个版本,都在朝着”写更少的代码、犯更少的错、跑得更快”这个方向前进。你不需要一次学完所有特性——先掌握智能指针、auto、Lambda、移动语义这四样,你的代码就已经焕然一新了。


本文聚焦在最常用、影响最大的特性上,无法覆盖现代 C++ 的全部内容。如需深入某个特性,推荐 cppreference.com 作为参考手册,以及 Scott Meyers 的《Effective Modern C++》作为最佳实践指南。

本文由作者按照 CC BY 4.0 进行授权