用 RAII 取代手写 new / delete

很多人对 C++ 的印象还停留在“手动 new、手动 delete”,但现代 C++(C++11 之后)的核心思想恰恰相反:让资源的生命周期绑定到对象的生命周期上。这个思想叫 RAII(Resource Acquisition Is Initialization)——构造时获取资源,析构时自动释放,函数无论从哪条路径退出,栈对象的析构函数都会被调用。

对照一下两段代码就能看出差别:手写指针时,一旦中途 return 或抛异常,delete 就容易被跳过;而用 std::vector 或智能指针,释放由编译器保证。这正是现代 C++ 能在保留性能的同时兼顾安全的根本原因。

不要问“这段代码在哪释放”,而要问“这个对象在哪析构”。把释放权交给析构函数,异常安全和泄漏问题会同时消失一大半。
本文核心知识点
  • RAII资源随对象构造获取、随析构释放,天然异常安全。
  • 智能指针unique_ptr 独占、shared_ptr 共享、weak_ptr 弱引用破环。
  • 移动语义右值引用让大对象“搬家”而非深拷贝。
  • 并发原语std::thread、mutex、condition_variable 组成标准并发工具箱。
现代 C++ 核心特性示意:嵌套堆叠的积木塔方块与环形链条智能指针,多条并行线程流线汇聚
模板像可嵌套的积木,智能指针像带锁的链条——现代 C++ 用类型系统把资源管理交给编译器。

智能指针三兄弟

标准库在 <memory> 中提供了三种智能指针,选错了会带来性能损耗或循环引用。判断的依据只有一个:这块资源到底有几个“主人”。

类型所有权拷贝典型用途
std::unique_ptr独占不可拷贝,可移动默认首选,工厂返回值、资源句柄
std::shared_ptr共享,引用计数可拷贝,计数加一多处共享同一对象生命周期
std::weak_ptr不增加计数可拷贝打破 shared_ptr 循环引用、观察者
#include <memory>
#include <iostream>

struct Node { int v; };

int main() {
    auto p = std::make_unique<Node>(Node{7});   // 独占所有权
    std::cout << p->v << "\n";                  // 7

    std::shared_ptr<Node> a = std::make_shared<Node>(Node{9});
    std::shared_ptr<Node> b = a;                 // 计数 = 2
    std::weak_ptr<Node>   w = a;                 // 不增加计数
    std::cout << a.use_count() << "\n";         // 2

    return 0;                                    // 全部自动释放
}

两个经验:优先用 make_unique / make_shared 而不是裸 new,既更短也更安全;函数参数在“只借用、不持有”时应传裸指针或引用,而不是 shared_ptr,否则会无意延长生命周期、影响计数语义。

移动语义与右值引用

拷贝一个大容器意味着复制它内部所有元素,代价高。移动语义的思路是:当源对象马上就要销毁时,何必复制?直接把它的内部指针“偷”过来,把源置空即可。右值引用 T&& 就是“可以安全移动的临时对象”的类型标记。

#include <string>
#include <utility>
#include <vector>

std::vector<int> make_big() {
    std::vector<int> v(1'000'000, 1);
    return v;                      // 返回值优化 / 移动,不会深拷贝
}

int main() {
    std::vector<int> a = make_big();   // 移动构造
    std::vector<int> b = std::move(a); // 显式移动后,a 处于有效但未指定状态
    // 此后不要再假定 a 里还有数据
    return 0;
}

要点:std::move 并不移动任何东西,它只是把表达式转型为右值引用,真正干活的是移动构造函数 / 移动赋值运算符。被移动过的对象仍然有效(可析构、可重新赋值),但内容不再可信,不要继续读取。编写自己的容器类时,一并提供移动构造与移动赋值,是让性能上一个台阶的关键。

Lambda 与标准算法

Lambda 让“就地写一个函数”变得自然,配合标准算法可以把原本十几行的循环压缩成一行。它的完整语法是 [捕获列表](参数){函数体},捕获列表决定它如何使用外部变量:[=] 按值、[&] 按引用、[x, &y] 混合。

#include <algorithm>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> v = {5, 2, 8, 1, 9};

    std::sort(v.begin(), v.end(),
              [](int a, int b) { return a > b; });   // 降序

    int threshold = 3;
    auto cnt = std::count_if(v.begin(), v.end(),
                             [threshold](int x) { return x > threshold; });

    for (int x : v) std::cout << x << " ";   // 9 8 5 2 1
    std::cout << "\n>" << threshold << ": " << cnt << "\n";  // >3: 3
    return 0;
}

注意“按引用捕获”的悬垂风险:如果 Lambda 逃出当前作用域(被存进容器、交给异步任务),而它捕获的是局部变量的引用,就会访问已销毁的栈内存。跨作用域使用时一律按值捕获或明确用智能指针传递。

auto、范围 for 与结构化绑定

现代 C++ 用 auto 让编译器推导类型,减少冗长的类型书写;范围 for(range-based for)消除了手写下标;结构化绑定(C++17)则让“拆解一组返回值”变得直观。三者合起来,能显著降低代码的噪声。

#include <map>
#include <string>
#include <iostream>

int main() {
    std::map<std::string, int> ages = {{"Amy", 20}, {"Bob", 31}};

    ages["Cara"] = 25;
    for (const auto &[name, age] : ages) {   // 结构化绑定
        std::cout << name << " = " << age << "\n";
    }
    return 0;
}

用 auto 时也要有边界感:涉及隐式转换、需要明确宽度的场景(如 size_t 与 int 混用),最好显式写出类型,避免 deduction 出意料之外的窄化或符号问题。const auto& 是遍历容器时最稳妥的写法——既避免拷贝,又防止误改。

并发编程:线程与同步原语

C++11 起,标准库直接提供了 std::thread、std::mutex、std::condition_variable,不再依赖平台 API。掌握三件事就够起步:线程怎么起、共享数据怎么保护、线程之间怎么通知。

  • 起线程:std::thread t(fn, args...);,退出前务必 join()(等待结束)或 detach()(分离),否则会 terminate。
  • 保护数据:用 std::mutex 配合 std::lock_guard / std::unique_lock,让加解锁也走 RAII,避免忘记解锁。
  • 线程通信:用 condition_variable 做等待唤醒,而不是忙轮询浪费 CPU。
#include <thread>
#include <mutex>
#include <vector>
#include <iostream>

int main() {
    std::mutex m;
    long total = 0;

    auto worker = [&](int lo, int hi) {
        long local = 0;
        for (int i = lo; i < hi; ++i) local += i;
        std::lock_guard<std::mutex> lk(m);   // 临界区
        total += local;                      // 保护共享累加
    };

    std::vector<std::thread> ts;
    for (int k = 0; k < 4; ++k) ts.emplace_back(worker, k * 250, (k + 1) * 250);
    for (auto &t : ts) t.join();            // 等待全部结束

    std::cout << total << "\n";             // 124500
    return 0;
}

“每个线程在本地累加、最后一次性加进共享变量”是并发求和的常见优化:把锁的粒度降到最低,既保证正确性,又减少竞争。