**C++中的智能指针及其使用技巧**

在C++11之后,标准库提供了三种主要的智能指针:std::unique_ptrstd::shared_ptrstd::weak_ptr。它们的出现极大地简化了内存管理,避免了大量手写new/delete的错误,并让资源获取即初始化(RAII)理念得以充分实践。本文将逐一介绍这三种智能指针的特性、适用场景,并给出一些实用技巧。


1. std::unique_ptr

1.1 特性

  • 独占所有权:同一时间只能有一个unique_ptr指向同一个资源。
  • 移动语义:支持移动构造和移动赋值,但不支持拷贝。
  • 析构自动释放:当unique_ptr离开作用域时,自动调用对应的删除器释放资源。

1.2 用法示例

#include <memory>
#include <iostream>

struct Node {
    int value;
    Node(int v) : value(v) { std::cout << "Node(" << value << ") constructed\n"; }
    ~Node() { std::cout << "Node(" << value << ") destroyed\n"; }
};

int main() {
    std::unique_ptr <Node> ptr1 = std::make_unique<Node>(10); // 推荐使用 make_unique
    // std::unique_ptr <Node> ptr2(ptr1); // 编译错误:不能拷贝
    std::unique_ptr <Node> ptr2 = std::move(ptr1); // 转移所有权
    if (!ptr1) std::cout << "ptr1 is now empty\n";
}

1.3 小技巧

  • 自定义删除器:如果需要特殊释放逻辑(如文件句柄、网络连接),可以传入第二个模板参数或构造时提供删除器。
auto customDel = [](int* p){ std::cout << "Custom delete\n"; delete p; };
std::unique_ptr<int, decltype(customDel)> up(new int(5), customDel);
  • 非堆分配unique_ptr可以指向栈上对象,但需使用自定义删除器防止错误删除。
int arr[10];
auto arrPtr = std::unique_ptr<int[], decltype(&std::free)>(arr, [](int*){ /* no op */ });

2. std::shared_ptr

2.1 特性

  • 共享所有权:多个shared_ptr实例可以指向同一资源。
  • 引用计数:内部维护计数,计数为0时才释放资源。
  • 线程安全:对计数的增减操作是原子操作。

2.2 用法示例

#include <memory>
#include <iostream>
#include <vector>

struct User {
    std::string name;
    User(const std::string& n) : name(n) { std::cout << name << " created\n"; }
    ~User(){ std::cout << name << " destroyed\n"; }
};

int main() {
    std::shared_ptr <User> p1 = std::make_shared<User>("Alice");
    {
        std::shared_ptr <User> p2 = p1; // 计数+1
        std::cout << "ref count: " << p1.use_count() << '\n';
    } // p2离开作用域,计数-1
    std::cout << "ref count after block: " << p1.use_count() << '\n';
}

2.3 小技巧

  • 避免循环引用:若两个对象互相持有shared_ptr,会导致内存泄漏。此时使用std::weak_ptr打破循环。
class B; // 前向声明

class A {
public:
    std::shared_ptr <B> ptrB;
    ~A(){ std::cout << "A destroyed\n"; }
};

class B {
public:
    std::weak_ptr <A> ptrA; // 注意使用 weak_ptr
    ~B(){ std::cout << "B destroyed\n"; }
};
  • 自定义计数器:在性能敏感场景,可自定义内存分配器或计数实现。

  • 配合 std::make_shared:一次性分配对象和控制块,减少内存碎片。


3. std::weak_ptr

3.1 特性

  • 非拥有指针:不影响引用计数,不持有资源的所有权。
  • 避免循环引用:与shared_ptr配合使用,解除循环依赖。
  • 访问资源:通过 lock() 获取对应的shared_ptr,若资源已被释放返回空指针。

3.2 用法示例

std::shared_ptr <int> sp = std::make_shared<int>(42);
std::weak_ptr <int> wp = sp; // 只记录弱引用

sp.reset(); // 资源被释放

if (auto locked = wp.lock()) {
    std::cout << "value: " << *locked << '\n';
} else {
    std::cout << "resource expired\n";
}

3.3 小技巧

  • 常用在观察者模式:观察者持有weak_ptr,避免强引用导致生命周期被拉长。

  • 定期检查:在循环或定时器中使用weak_ptr检查资源是否已消亡,避免悬空指针。


4. 实用技巧与最佳实践

场景 推荐智能指针 说明
单一所有者,栈或堆资源 unique_ptr 自动释放,移动语义
多个所有者共享 shared_ptr 需要时使用 make_shared
循环引用场景 weak_ptr 打破循环,避免泄漏
自定义删除器 unique_ptr/shared_ptr 可自定义释放逻辑
大量短生命周期资源 unique_ptr 性能更好,计数开销低
线程共享资源 shared_ptr 计数线程安全,轻量
资源监控、观察者 weak_ptr 观察者不拥有资源,防止生命周期拉长

4.1 注意事项

  1. 不要混用:在同一对象上既用unique_ptr又用shared_ptr,会产生未定义行为。
  2. 避免裸指针:尽量使用智能指针,若必须使用裸指针,记得使用get()而不是operator->直接操作。
  3. 自定义删除器:若使用数组,需要传入std::default_delete<T[]>或自己实现。
  4. 拷贝与移动unique_ptr只能移动,shared_ptr可拷贝;使用std::move时注意变量状态。
  5. 计数溢出:在极端情况下,shared_ptr计数可能溢出;可通过weak_ptr或分配器进行管理。

5. 小结

C++智能指针是现代C++编程不可或缺的工具,正确使用可以大幅降低内存泄漏、悬空指针等错误的发生概率。熟悉它们的语义、生命周期以及最佳实践,是成为优秀C++开发者的必经之路。希望本文能帮助你快速掌握智能指针的核心概念,并在实际项目中灵活运用。祝编码愉快!

发表评论