目 录CONTENT

文章目录

C++ 智能指针:unique_ptr、share_ptr、weak_ptr

TalentQ
2025-09-09 / 0 评论 / 0 点赞 / 4 阅读 / 0 字

1 为什么要使用智能指针

在传统的C++中,动态内存的管理完全由程序员手动管理(newdelete)。这会带来一个巨大的风险:内存泄露。

手动管理的痛点

  • 忘记 delete: 尤其是在函数提前返回或抛出异常时,很容易漏掉 delete

  • 重复 delete: 对同一个指针 delete 多次会导致未定义行为(通常是程序崩溃)。

  • 难以跟踪所有权: 很难清晰地知道哪个函数或对象负责释放一块内存。

解决方案:RAII (Resource Acquisition Is Initialization)
智能指针是 RAII 思想的典型应用:

  • 将资源(内存)的生命周期与对象的生命周期绑定。

  • 构造函数中获取资源(初始化)。

  • 析构函数中释放资源。

  • 这样,只要智能指针对象离开其作用域(无论是正常离开还是因为异常),它的析构函数就会被自动调用,从而确保资源被安全释放。

2 C++ 11 中的四种智能指针

C++11 在 <memory> 头文件中提供了四种智能指针:

  1. std::unique_ptr (C++11)

  2. std::shared_ptr (C++11)

  3. std::weak_ptr (C++11)

  4. std::auto_ptr (C++17 中移除,已废弃,不做讨论)

2.1 独占指针 std::unique_ptr

核心思想:

独占所有权。一个 unique_ptr 在任何时候都唯一地拥有其指向的对象。它不能被拷贝,只能被移动(std::move)。当 unique_ptr 被销毁时,它指向的对象也会被销毁。

  • 轻量高效: 开销很小,通常与裸指针相同。

  • 禁止拷贝: 拷贝构造函数和拷贝赋值运算符被标记为 = delete

  • 支持移动: 可以通过 std::move 转移所有权。

创建方式

#include <memory>

// 方式一:推荐使用 std::make_unique (C++14)
std::unique_ptr<int> u1 = std::make_unique<int>(42);
auto u2 = std::make_unique<std::string>("Hello");

// 方式二:直接构造(不推荐,可能涉及不必要的内存分配)
std::unique_ptr<int> u3(new int(100));

代码示例

{
  auto ptr = std::make_unique<MyClass>(); // 创建
  ptr->doSomething(); // 使用 -> 操作符
  (*ptr).doSomething(); // 使用 * 操作符解引用

  // std::unique_ptr<int> ptr2 = ptr; // 错误!不能拷贝
  std::unique_ptr<int> ptr2 = std::move(ptr); // 正确!转移所有权
  // 现在 ptr 变为 nullptr,ptr2 拥有资源

} // 作用域结束,ptr2 被销毁,它管理的 MyClass 对象自动被 delete

2.2 共享指针 std::shared_ptr

核心思想

共享所有权。多个 shared_ptr 可以指向同一个对象,并通过一个引用计数器来协同管理对象的生命周期。每当一个新的 shared_ptr 被创建来指向该对象时,计数器加1;每当一个 shared_ptr 被销毁或重置时,计数器减1。当计数器变为0时,对象被自动删除。

  • 支持拷贝和赋值

  • 有额外开销: 需要维护一个控制块(包含引用计数、弱计数、删除器等),内存和性能开销比 unique_ptr 大。

  • 不是线程安全的: 引用计数的增减是原子操作(线程安全的),但指向的对象本身不是线程安全的。

创建方式

// 方式一:推荐使用 std::make_shared (更高效,单次分配内存)
auto s1 = std::make_shared<MyClass>();
std::shared_ptr<int> s2 = std::make_shared<int>(42);

// 方式二:直接构造
std::shared_ptr<MyClass> s3(new MyClass);

// 方式三:通过另一个 shared_ptr 拷贝
auto s4 = s1;

代码示例

void func(std::shared_ptr<MyClass> sp) {
    // 引用计数+1
    sp->doSomething();
} // 函数结束,sp 析构,引用计数-1

auto mainSp = std::make_shared<MyClass>(); // 引用计数 = 1
func(mainSp); // 传入时引用计数+1(=2),函数返回后-1(=1)
std::cout << mainSp.use_count() << std::endl; // 输出 1

{
    auto anotherSp = mainSp; // 引用计数+1(=2)
} // anotherSp 析构,引用计数-1(=1)

} // mainSp 析构,引用计数-1(=0),对象被删除

2.3 弱共享指针 std::weak_ptr

核心思想:

弱引用。weak_ptr 是为了解决 shared_ptr循环引用问题而设计的。它指向一个由 shared_ptr 管理的对象,但不增加其引用计数。它不能直接访问对象,需要先转换为 shared_ptr

  • 不拥有对象,不控制生命周期。

  • 用于打破 shared_ptr 的循环引用。

  • 必须从 shared_ptr 或另一个 weak_ptr 创建。

循环引用问题

两个或多个对象通过 shared_ptr 互相持有,导致引用计数永远无法降为0。

class B;
class A {
public:
    std::shared_ptr<B> b_ptr;
    ~A() { std::cout << "A destroyed" << std::endl; }
};
class B {
public:
    std::shared_ptr<A> a_ptr; // 这里造成了循环引用
    ~B() { std::cout << "B destroyed" << std::endl; }
};

void cycle() {
    auto a = std::make_shared<A>();
    auto b = std::make_shared<B>();
    a->b_ptr = b; // A 引用 B,B 的引用计数=2
    b->a_ptr = a; // B 引用 A,A 的引用计数=2
} // 函数结束,a 和 b 析构,但 A 和 B 的引用计数都只减为1,无法释放!内存泄漏!

使用 weak_ptr 解决

分析对象关系,将其中一方持有另一方的 shared_ptr 改为 weak_ptr

class B;
class A {
public:
    std::shared_ptr<B> b_ptr;
    ~A() { std::cout << "A destroyed" << std::endl; }
};
class B {
public:
    std::weak_ptr<A> a_ptr; // 将 shared_ptr 改为 weak_ptr
    ~B() { std::cout << "B destroyed" << std::endl; }
};

void no_cycle() {
    auto a = std::make_shared<A>();
    auto b = std::make_shared<B>();
    a->b_ptr = b;
    b->a_ptr = a; // weak_ptr 不会增加 A 的引用计数,A 的计数仍为1
} // 函数结束,a 析构,A 计数减为0,A 被销毁。然后 b 析构,B 计数减为0,B 被销毁。

如何使用 weak_ptr 访问对象

auto shared_p = std::make_shared<int>(10);
std::weak_ptr<int> weak_p = shared_p;

// 方法一:使用 lock(),返回一个 shared_ptr,如果对象存在则有效
if (auto temp_sp = weak_p.lock()) {
    std::cout << *temp_sp << std::endl; // 对象存在,可以访问
} else {
    std::cout << "Object has been destroyed" << std::endl;
}

// 方法二:使用 expired(),检查对象是否已被销毁(不推荐,因为检查和使用非原子操作)
// if (!weak_p.expired()) { ... } // 可能刚检查完对象就被另一个线程释放了

lock() 返回的 shared_ptr 会暂时增加引用计数,从而锁定资源。当你不再需要访问该资源时,应该让这个临时的 shared_ptr 尽快离开作用域以减少引用计数,避免不必要的资源占用。

3 扩展

make_shared 和直接 new 的区别

  • 性能make_shared 通常只进行一次内存分配,同时分配对象和控制块。而 shared_ptr<T>(new T) 会进行两次分配(一次给对象,一次给控制块),效率更低。

  • 异常安全make_shared 是异常安全的。例如 func(std::shared_ptr<T>(new T), other_func()),如果 other_func() 抛出异常,new T 分配的内存可能会泄漏。而 func(std::make_shared<T>(), other_func()) 则不会。

智能指针的大小开销

  • unique_ptr: 通常与裸指针大小相同。

  • shared_ptr: 通常是裸指针的两倍大小(一个指向对象,一个指向控制块)。

  • weak_ptr: 通常与 shared_ptr 大小相同。

0

评论区