需要预先了解:


1. 原始指针的缺点

原始指针/裸指针:如int* p,需要手动管理内存,容易造成以下问题:

1. 忘记delete/free会导致内存泄露:

// 局部变量 ptr 指向的堆内存未释放,造成内存泄露
void func
{
    Type* ptr = new Type();
    ptr->do_something();
}

2.使用释放后的悬挂指针,导致未定义行为:

Type* ptr = new Type();
delete ptr;
ptr->do_something();

3. 重复释放同一块内存,导致未定义行为或程序崩溃

Type* ptr = new Type();
delete ptr;
delete ptr;       // 重复释放同一块内存

2. RAII(Resource Acquisition Is Initialization)

RAII(直译为“资源获取即初始化”):在 对象构造阶段获取资源,在 对象析构阶段释放资源;依靠 C++ 对象自动生命周期管理(离开作用域自动调用析构),保证资源一定会被释放。

一个实现RAII管理资源的简单设计:

// Array.h
class Array
{
private:
    int* data;
    size_t size;
public:
    Array(size_t size);
    ~Array();
    
    // 不允许浅拷贝,可以看需求决定是否允许深拷贝
    Array(const Array& other) = delete;
    Array& operator=(const Array& other) = delete;
    
    // 视情况决定是否允许移动
    Array(Array&& other);
    Array& operator=(Array&& other);
}

这里给出函数的具体实现供参考:

// Array.cpp
Array::Array(size_t size)
{
    data = new int[size]();
}

Array::~Array()
{
    std::cout << "析构函数被调用,在这里释放内存" << std::endl;
    delete[] data;
}

Array::Array(Array&& other)
{
    data = other.data;
    other.data = nullptr;
}

Array& Array::operator=(Array&& other)
{
    if(this != &other)
    {
        data = other.data;
        other.data = nullptr;
    }
    return *this;
}

3. 智能指针

智能指针是原始指针的封装,会通过RAII的模式自动管理内存,确保资源在不再使用时被正确释放,避免内存泄漏和指针悬空等问题。智能指针定义在头文件<memory>,常见的智能指针有:

  • std::unique_ptr
  • std::shared_ptr
  • std::weak_ptr

3.1 std::unique_ptr:独占指针

独占语义:同一时刻,只能有一个 unique_ptr 拥有这块内存

  • 禁止拷贝(拷贝构造与拷贝赋值)
  • 允许移动进行所有权的转移
  • 当std::shared_ptr被销毁时,资源会被自动释放

1. std::shared_ptr的创建:语法为std::shared_ptr<变量类型>

#include <memory>

// 通过 原始指针 或 new 进行创建
int* raw_ptr = new int(10);
std::unique_ptr<int> uptr1(raw_ptr);
std::unique_ptr<int> uptr2(new int(20));
raw_ptr = nullptr;    // 注意把 raw_ptr 置空,确保资源被 uptr1 独占

// 通过 make_unique 进行创建,最推荐最安全的方式
auto uptr3 = std::make_unique<int>(30);

2. 和裸指针一样,通过 -> 和 * 操作符访问资源:

std::unique_ptr<MyClass> uptr1 = std::make_unique<MyClass>();
uptr1->member_function();

auto uptr2 = std::make_unique<int>();
std::cout << *uptr2 << std::endl;

3. 可以通过 get() 方法获取地址/访问原始指针:

int *rawPtr = uniquePtr.get();

4. 允许移动:

std::unique_ptr<int> uniquePtr1 = std::make_unique<int>(10);
std::unique_ptr<int> uniquePtr2 = std::move(uniquePtr1);        // 转移所有权

if(!uniquePtr1) {
    std::cout << "uniquePtr1 is empty" << std::endl; // uniquePtr1 现在为空
}

5. 禁止拷贝:

std::unique_ptr<Data> a = std::make_unique<Data>();
std::unique_ptr<Data> b = a; // 编译报错!禁止拷贝

3.2 std::shared_ptr共享指针 与 std::weak_ptr弱指针

std::shared_ptr

  • 共享所有权:多个std::shared_ptr可以指向同一个对象
  • 控制块:在堆上分配,独立于管理对象;其中保存着强引用计数、弱引用计数与删除器
  • 规则:
    • 拷贝shared_ptr,则强引用计数+1
    • shared_ptr析构/被赋值覆盖,则强引用计数-1
    • 当强引用计数为0时,销毁托管对象
    • 当weak_ptr全部销毁后释放控制块

1. 创建std::shared_ptr:语法为std::shared_ptr<变量类型>

#include <memory>

// 通过原始指针创建
int *raw_ptr = new int(10);
std::shared_ptr<int> sptr1(raw_ptr);
raw_ptr = nullptr;

// 通过 new 创建
std::shared_ptr<int> sptr2(new int(20));

// 通过 make_shared 创建,最推荐的方式
std::shared_ptr<int> sptr3 = std::make_shared<int>(30);

2. 获取引用计数:通过use_count()方法获取持有当前资源的shared_ptr的数目。 3. 释放所有权:通过reset()方法释放shared_ptr

std::shared_ptr<int> sptr1 = std::make_shared<int>(10);
std::shared_ptr<int> sptr2 = sptr1;       // 共享资源,引用计数增加
std::cout << "引用计数: " << sptr1.use_count() << std::endl; // 输出 2

sptr2.reset(); // 释放 sptr2 的资源,引用计数减少
std::cout << "引用计数: " << sptr1.use_count() << std::endl; // 输出 1

sptr1.reset(); // 释放 sptr1 的资源,引用计数减少到 0,资源被释放

4. 获取裸指针:通过get()方法

auto sptr = std::make_shared<int>(10);
int* raw_ptr = sptr.get();

std::weak_ptr:

  • 不拥有所有权,不影响强引用计数
  • 不能直接访问对象,需要调用lock()方法提升为shared_ptr
    • 如果资源已经被释放,则返回一个空的shared_ptr
    • 如果资源未被释放,则返回一个指向资源的shared_ptr
  • 不能由裸指针构造,只能由shared_ptr/weak_ptr赋值得到
auto sptr = std::make_shared<Node>();
std::weak_ptr<Node> wptr = sptr;

if(auto temp_ptr = wptr.lock())
{
    temp_ptr->do_something();
}

循环引用问题

两个对象互相持有对方的shared_ptr,导致引用计数永远无法归零,进而使得内存泄漏。

下面是一个循环引用的示例:

struct Node 
{ 
    int val; 
    std::shared_ptr<Node> partner; 
    Node(int v) : val(v) {} 
}; 

void func()
{
    auto a = std::make_shared<Node>(1);
    auto b = std::make_shared<Node>(2);
    
    a->partner = b, b->partner = a;   // a,b互相持有对方
}

在函数func()调用时,堆上的两个Node对象的的shared_ptr的引用计数都是2,但是当func()结束后,a、b被销毁,引用计数从2变成了1,对象不会被析构,造成了内存泄漏。

解决方案:把其中一个shared_ptr换为weak_ptr:

struct Node 
{ 
    int val; 
    std::weak_ptr<Node> next; // 改成 weak_ptr 
    Node(int v) : val(v) {} 
};