03 智能指针

说明

在学 C 语言的过程中,我们知道普通指针管理内存容易造成内存泄漏:

T* p = new T[n];
delete[] p;

由于手动 delete 容易遗忘而造成内存泄漏,因此 C++ 提供了智能指针用于内存的管理。
智能指针的实现基于 RAII(Resource Acquisition Is Initialization)思想,即将资源的生命周期与对象的生命周期绑定:对象创建时获取资源,对象销毁时自动释放资源,从而避免手动管理内存带来的错误。本质上来说,智能指针是一个对象。
智能指针的相关内容包含在头文件 <memory> 中。
标准库中支持的智能指针类型有三种:

  • unique_ptr
  • shared_ptr
  • weak_ptr

unique_ptr

std::unique_ptr 只允许这个指针来管理对应的资源, 不允许进行指针的赋值和拷贝 ,但是可以用 move 方法来移动指针的所有权 ,比如 unique_ptr<int> p2 = move(p1);

所有权

若某个对象A拥有另一个对象B的所有权,那么A就负责B的生命周期管理,即B的创建、使用和销毁。换句话说,A有权决定B什么时候被创建,什么时候被销毁。

auto p1 = std::make_unique<int>(42);
auto p2 = p1;      // 编译错误
auto p1 = std::make_unique<int>(42);
auto p2 = std::move(p1); // 编译成功

如果希望转移所有权,则需要使用移动语义(Move Semantics),std::move():

auto p1 = std::make_unique<int>(42);
auto p2 = std::move(p1); 

此时对象的所有权从 p1 转移给 p2,而 p1 会变为空指针(nullptr)。

shared_ptr

std::shared_ptr 是非独占的指针,可以多个指针管理一个对象。内部使用 引用计数。shared_ptr内部的引用计数是 线程安全 的,但是对象的读取需要 加锁。

引用计数

引用计数记录当前有多少个 shared_ptr 正在管理同一个对象。

  • 创建第一个 shared_ptr 时,引用计数为 1。
  • 每拷贝一个 shared_ptr,引用计数 加 1。
  • 每析构一个 shared_ptr,引用计数 减 1。

当引用计数减为 0 时,说明已经没有任何 shared_ptr 指向该对象,对象会被自动释放。

#include <iostream>
#include <memory>

void foo() {
    auto ptr = std::make_shared<int>(42); // 创建一个 shared_ptr,指向整数 42
    {
        auto another = ptr;               // another 与 ptr 共享所有权
        std::cout << *another << std::endl; // 输出 42
    } // another 超出作用域,但整数不会释放,因为 ptr 仍然指向它

    std::cout << *ptr << std::endl;       // 仍然可以输出 42
} // ptr 超出作用域,最后一个 shared_ptr 被销毁,整数自动释放

void bar() {
    auto ptr = std::make_shared<int>(100); // 创建一个 shared_ptr,指向整数 100
    auto copy = ptr;                       // 再创建一个 shared_ptr,共享所有权

    std::cout << *ptr << std::endl;        // 输出 100
    std::cout << *copy << std::endl;       // 输出 100
} // ptr 和 copy 同时离开作用域,对象自动释放

可以用 shared_ptr可以使用use_count 函数来查看某个对象拥有的指针的个数

注意

不能用一个原始指针初始化多个 shared_ptr 否则会造成内存多次释放。

#include <iostream>
#include <memory>
int main() {
	int* raw = new int(42);
	std::shared_ptr<int> ptr1(raw);
	std::shared_ptr<int> ptr2(raw); // 错误:再次用同一个裸指针构造
	std::cout << *ptr1 << std::endl;
	std::cout << *ptr2 << std::endl;
}

shared_ptr通过拷贝已有的 shared_ptr才能够实现多个shared_ptr管理同一个对象。而同一个裸指针只能用于构造一个 shared_ptr。

weak_ptr

std::weak_ptr 是一种弱引用(Weak Reference)。
它可以观测一个由 std::shared_ptr 管理的对象,但并不拥有对象的所有权。因此,weak_ptr 不会增加引用计数,也不会影响对象的生命周期。
与 shared_ptr 不同,weak_ptr 不能直接访问对象,而需要先转换为 shared_ptr。
假设已经存在一个 shared_ptr:

auto ptr = std::make_shared<int>(42);

如果只是想判断对象是否仍然存在:

std::weak_ptr<int> observer = ptr;

weak_ptr不能直接使用 * 或 ->。而且必须调用 lock() 获得一个 shared_ptr 后才能访问对象。

Example
auto ptr = std::make_shared<int>(42);
std::weak_ptr<int> observer = ptr;
if (auto temp = observer.lock()) {
    std::cout << *temp << std::endl;
}