All writing Notes & observations

C++ Features of Each Version

Organized the features of each version since C++11.

C++几个版本的特性

C++11的特性:史诗级变革(现代 C++ 的基石)

nullptr

在 C++ 的世界里,虽然 NULL 和 nullptr 都代表“空指针”,但它们在底层逻辑和类型安全上有本质的区别。简单来说,NULL 是过去(C 语言遗产),而 nullptr 是未来(现代 C++)。

本质区别:整数 vs 指针

  • NULL:在底层通常被定义为整数0或者 (void*)0。在 C++ 中,它本质上是一个宏。因为 C++ 允许将0隐式转换为指针,所以 NULL 一直被当作指针用。
  • nullptr:是 C++11 引入的关键字。它的类型是 std::nullptr_t,它可以隐式转换为任何指针类型,但不能隐式转换为整数。

nullptr是为了解决NULL的二义性问题而出现的。由于NULL本质上是一个整数,因此在函数重载时会导致意想不到的错误。举个例子,加入你有以下两个函数:

1
2
void func(int n);    // 函数 A
void func(char* s);  // 函数 B

如果你调用func(NULL):编译器会优先把它看作整数0,从而调用函数A。这通常不是你的本意。如果你调用func(nullptr):编译器明确知道这是一个指针,从而调用函数B。
因此,在编写现代C++(C++11及以上):请永远使用nullptr。它能帮你规避掉很多难以排查的重载Bug。

以下是它们的核心差异对比表:

特性 NULL nullptr
本质 宏定义(通常是 0) 关键字(std::nullptr_t)
类型安全 弱(容易与整数混淆) 强(只能代表指针)
引入版本 C 语言时代 / C++98 C++11 及以后
隐式转换 可转为整数或指针 仅可转为指针,不可转为整数
推荐用法 现代 C++ 中应避免使用 强烈建议在所有场景使用

类型推导

在现代 C++(C++11 及以后)中,auto 和 decltype 都是用来进行类型推导的利器。虽然它们看起来都在替你省事,但它们的工作原理和“脑回路”完全不同。

auto:由值推导类型

auto的核心逻辑是:“根据等号右边的初始化值,推断出变量应该是什么类型。”

  • 特点:变量必须初始化。
  • 规则:它会忽略顶层的 const 属性和引用属性(除非你显式加上 auto& 或 const auto)。
1
2
3
4
5
int x = 10;
auto a = x;       // a 是 int
const int b = x;
auto c = b;       // c 依然是 int (const 被忽略了)
auto& d = b;      // d 是 const int& (显式写了引用,const 就会保留)

decltype:由表达式推导类型

decltype(declared type)的核心逻辑是:“查询一个表达式或变量的声明类型,而不实际计算它。”

  • 特点:不需要初始化,它只看表达式的“户口本”。
  • 规则:它会完整保留所有的 const 和引用属性。
1
2
const int x = 0;
decltype(x) y = 1; // y 的类型精确地就是 const ints

协作场景:追踪返回类型

在C++11中,如果你写一个模板函数,返回值取决于参数的运算,你可能无法预先知道类型。这时需要结合两者:

1
2
3
4
template <typename T, typename U>
auto add(T t, U u) -> decltype(t + u) {
    return t + u;
}

Lambda表达式

Lambda表达式实际上就是提供了一个类似匿名函数的特性,而匿名函数则是在需要一个函数,但是又不想费力去命名一个函数的情况下去使用的。

智能指针

智能指针的核心思想是 RAII(资源获取即初始化):它本质上是一个类(封装了原始指针),利用栈上对象的生命周期来管理堆上的内存。当智能指针对象超出作用域被销毁时,它的析构函数会自动帮你 delete 掉底层的内存。

C++ 标准库(<memory>)主要提供了三种智能指针,它们各自对应不同的“所有权”模型:

1.std::unique_ptr:独占所有权(霸道总裁)

这是最常用、也最高效的智能指针。它的原则是:同一时刻,只能有一个unique_ptr指向同一块内存。

  • 特点:不允许复制(拷贝构造函数被禁用),只允许移动(转移所有权)。由于没有额外的性能开销,它的性能和裸指针几乎一模一样。
  • 适用场景:只要你不需要在多个地方共享这块内存,就无脑用它。
 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
#include <iostream>
#include <memory>

class MyClass {
public:
    MyClass() { std::cout << "创建对象\n"; }
    ~MyClass() { std::cout << "销毁对象\n"; }
    void doSomething() { std::cout << "执行任务\n"; }
};

int main() {
    {
        // 推荐使用 std::make_unique (C++14 引入) 来创建
        std::unique_ptr<MyClass> ptr1 = std::make_unique<MyClass>();
        ptr1->doSomething();

        // std::unique_ptr<MyClass> ptr2 = ptr1; // 错误!编译不通过,不能复制
        
        // 正确做法:使用 std::move 转移所有权
        std::unique_ptr<MyClass> ptr3 = std::move(ptr1); 
        // 此时 ptr1 变成空指针,内存由 ptr3 接管
    } 
    // 离开作用域,ptr3 被销毁,自动调用 MyClass 的析构函数!
    return 0;
}

输出:

1
2
3
创建对象
执行任务
销毁对象

2.std::shared_ptr:共享所有权(合伙人)

当你确实需要多个指针共同管理同一块内存时,就需要用到它。它的原则是:大家一起用,最后一个走的人负责关灯(释放内存)。

  • 核心机制:引用计数(Reference Counting)。系统会在内部维护一个计数器,记录有多少个 shared_ptr 指向这块内存。

    • 每多一个 shared_ptr 指向它,计数器 +1。
    • 每销毁一个 shared_ptr,计数器 -1。
    • 当计数器归 0 时,自动 delete 内存。
  • 代价:因为要维护引用计数(且计数操作是线程安全的),它的性能开销比 unique_ptr 稍大。

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

int main() {
    // 推荐使用 std::make_shared,只分配一次内存(控制块和数据一起),性能更好
    std::shared_ptr<int> sp1 = std::make_shared<int>(100);
    std::cout << "引用计数: " << sp1.use_count() << "\n"; // 输出 1

    {
        std::shared_ptr<int> sp2 = sp1; // 允许复制!
        std::cout << "引用计数: " << sp1.use_count() << "\n"; // 输出2,sp1.use_count()也是2
    } // sp2 离开作用域被销毁,计数器减 1

    std::cout << "引用计数: " << sp1.use_count() << "\n"; // 输出 1
    // sp1 离开作用域,计数变为 0,内存释放
    return 0;
}

输出:

1
2
3
引用计数: 1
引用计数: 2
引用计数: 1

3.std::weak_ptr:弱引用(旁观者)

weak_ptr 是为了解决 shared_ptr 的一个致命缺陷而生的:循环引用。

  • 什么是循环引用? 假设对象 A 里面有一个 shared_ptr 指向对象 B,对象 B 里面也有一个 shared_ptr 指向对象 A。这会导致它们的引用计数永远降不到 0,从而造成内存泄漏。代码演示如下:
 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
#include <iostream>
#include <memory>

// 因为 ClassA 中用到了 ClassB,所以需要先打个招呼(前置声明)
class ClassB; 

class ClassA {
public:
    // A 里面有一个共享管家,负责保管 B
    std::shared_ptr<ClassB> ptrToB; 

    ClassA() { std::cout << "ClassA 诞生了!\n"; }
    // 如果管家正常下班,会触发这个销毁函数
    ~ClassA() { std::cout << "ClassA 被销毁了!\n"; } 
};

class ClassB {
public:
    // B 里面也有一个共享管家,负责保管 A
    std::shared_ptr<ClassA> ptrToA; 
    // std::weak_ptr<ClassA> ptrToA; 

    ClassB() { std::cout << "ClassB 诞生了!\n"; }
    // 如果管家正常下班,会触发这个销毁函数
    ~ClassB() { std::cout << "ClassB 被销毁了!\n"; } 
};

int main() {
    std::cout << "--- 实验开始 ---\n";
    {
        // 1. 雇佣管家 a 保管 ClassA,雇佣管家 b 保管 ClassB
        // 此时,A 和 B 的引用计数都是 1
        std::shared_ptr<ClassA> a = std::make_shared<ClassA>();
        std::shared_ptr<ClassB> b = std::make_shared<ClassB>();

        // 2. 互相绑定(循环引用的罪魁祸首!)
        // a 里面的管家也去保管 B,导致 B 的引用计数变成 2
        a->ptrToB = b; 
        // b 里面的管家也去保管 A,导致 A 的引用计数变成 2
        b->ptrToA = a; 

        std::cout << "A 的引用计数: " << a.use_count() << "\n"; // 输出 2
        std::cout << "B 的引用计数: " << b.use_count() << "\n"; // 输出 2
        
    } // 3. 离开大括号,局部变量 a 和 b 下班被销毁
    
    std::cout << "--- 离开作用域,实验结束 ---\n";
    return 0;
}

输出:

1
2
3
4
5
6
--- 实验开始 ---
ClassA 诞生了!
ClassB 诞生了!
A 的引用计数: 2
B 的引用计数: 2
--- 离开作用域,实验结束 ---

~ClassA() 和 ~ClassB() 里的“被销毁了!”这两句话根本没有打印出来,这意味着内存泄漏了。所以,为了避免出现上述问题,我们需要引入weak_ptr。只需讲上面代码中的这行代码 // std::weak_ptr ptrToA; 取消注释即可,下面就不再赘述。

  • weak_ptr 的作用:它只能从 shared_ptr 或另一个 weak_ptr 构造。它不增加引用计数,仅仅是“观察”这块内存。当你要使用数据时,必须调用.lock() 方法将它提升为shared_ptr(如果内存还没被释放的话)。该方法会返回一个shared_ptr,但若没有被接收的话,则不会增加所指向对象的引用计数。
 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
#include <iostream>
#include <memory>

int main() {
    // 先创建一个旁观者(目前什么都没看)
    std::weak_ptr<int> weak_viewer;

    {
        // 创建一个共享管家,保管数字 42
        std::shared_ptr<int> shared_owner = std::make_shared<int>(42);
        
        // 旁观者开始观察这个管家手里的数据 (引用计数依然是 1)
        weak_viewer = shared_owner; 

        // 第一次尝试使用数据 (此时 shared_owner 还在)
        std::cout << "--- 第一次尝试 ---\n";
        // 核心用法:用 lock() 临时获取一个 shared_ptr
        if (std::shared_ptr<int> temp_sp = weak_viewer.lock()) {
            std::cout << "对象还活着!它的值是: " << *temp_sp << "\n";
            std::cout << "此时引用计数临时变成: " << temp_sp.use_count() << "\n";
        } else {
            std::cout << "对象已经被销毁了。\n";
        }
    } // 离开大括号,shared_owner 被销毁,引用计数归 0,数字 42 被释放

    // 第二次尝试使用数据 (此时数据已经被销毁)
    std::cout << "\n--- 第二次尝试 ---\n";
    if (std::shared_ptr<int> temp_sp = weak_viewer.lock()) {
        std::cout << "对象还活着!它的值是: " << *temp_sp << "\n";
    } else {
        std::cout << "对象已经被销毁了,无法访问。\n";
    }

    return 0;
}

输出:

1
2
3
4
5
6
--- 第一次尝试 ---
对象还活着!它的值是: 42
此时引用计数临时变成: 2

--- 第二次尝试 ---
对象已经被销毁了,无法访问。

基于范围的for循环:for(auto &i:arr)

thread类和mutex类

C++14的特性

泛型 Lambda

Lambda 的参数可以用 auto 推导了([](auto x, auto y) { return x + y; })。

返回值类型推导

普通函数也可以直接写 auto 作为返回值类型,让编译器去猜。

std::make_unique

补齐了 C++11 只有 std::make_shared 的遗憾。

C++17的特性

结构化绑定

可以直接解包结构体或 std::tuple:auto [x, y, z] = get_3d_point();。

if / switch 带初始化

把变量的作用域严格限制在判断语句内,例如:if (auto it = map.find(key); it != map.end()) { … }。

现代化词汇类型:

  • std::optional:完美替代以前用指针判空来表示“可能没有值”的场景。
  • std::variant:类型安全的 union。
  • std::string_view:字符串的“只读视图”,实现了零拷贝传递字符串,极大地提升了性能。

C++20的特性:四大支柱(第二次历史跨越)

C++20 是继 C++11 之后最庞大的一次更新,它引入的“四大支柱”彻底改变了现代 C++ 的编程范式。

Concepts (概念)

给模板编程加上了“类型约束”。以前写模板报错时,编译器会打印几百行天书;现在用 Concepts 约束后,错误信息终于变成了人类可读的文字。

Modules (模块)

正式告别 #include 历史!使用 import 引入模块,不仅解决了宏污染问题,还让大型项目的编译速度有了数量级的提升。

Coroutines (协程)

引入了无栈协程,新增了 co_await, co_yield, co_return 关键字,让异步编程(比如网络请求、高并发)可以用同步的方式写出来。

Ranges (范围库)

结合了函数式编程思想,允许用类似 Linux 管道符 | 的方式处理数据:data | views::filter(…) | views::transform(…),代码极其优雅。

其他亮点

三路比较运算符(<=>,也叫飞船运算符)、std::format(类似 Python 的高级格式化输出)。

C++23 (及以后):标准库的极致完善

C++23 进一步吸收了其他现代语言的优点,让标准库更加“开箱即用”。

std::print / std::println

大家终于可以抛弃老旧的 std::cout « 和不安全的 printf 了,这是一种更现代、更快速的控制台输出方式。

std::expected

引入了类似 Rust Result 的错误处理机制。函数可以返回一个“期望的值”或者“错误原因”,比抛出异常更轻量,比返回错误码更清晰。

显式 this 参数 (Deducing this)

允许在类成员函数中显式把 this 当作参数传递,极大简化了复杂的模板代码(如 CRTP 模式)。

多维数组操作:std::mdspan

对科学计算和高性能计算非常友好,提供了一种访问连续内存的多维视图。