1. 智能指针家族

  • C++ 标准库提供了三种主要的智能指针,定义在 头文件中:
智能指针 所有权 引用计数 可拷贝 核心特点
unique_ptr 独占 不可拷贝,只能移动 零开销,性能与裸指针相当
shared_ptr 共享 可拷贝 引用计数管理,最后一个销毁时释放
weak_ptr 非拥有 有(弱计数) 可拷贝 不增加引用计数,用于打破循环引用

2. 为什么需要智能指针

2.1 内存泄漏

  • 函数中途 return 抛出异常,delete 没执行,堆内存永久占用
void test()
{
    int* p = new int(10);
    if(true) return; // 直接退出,delete 永远执行不到,内存泄漏
    delete p;
}

2.2 重复释放

int* p = new int;
int* q = p;
delete p;
delete q; // 同一块内存释放两次,程序崩溃

2.3 野指针悬空

  • 释放后没有置空,再次访问非法内存
int* p = new int;
delete p;
*p = 10; // 野指针,未定义行为

2.4 异常安全漏洞

-异常抛出跳过 delete,内存泄漏。

核心解决方案:RAII。资源获取即初始化:把堆资源托管给栈对象,栈对象生命周期结束自动调用析构释放内存,无需手动 delete。智能指针就是 RAII 的标准实现。

3. std::unique_ptr 独占智能指针

3.1 基础语法

#include<menory>
//方式1:new初始化,不推荐
unique_ptr<int>p1(new int(666));
//方式2: C++14 make_unique,推荐
auto p2 = make_unique<int>(667);
//空指针
unique_ptr<int>empty_p;

3.2 禁止拷贝,仅支持移动

unique_ptr<int> p1(new int(100));
unique_ptr<int> p2 = p1; //  编译报错,禁止拷贝
unique_ptr<int> p3 = move(p1); //  std::move转移所有权,p1变空

3.3 常用接口

p.get();        // 返回裸指针,谨慎使用
p.release();    // 释放所有权,返回裸指针,unique_ptr置空
p.reset();      // 释放当前资源,置空
p.reset(new int(20)); // 释放旧资源,托管新内存
bool flag = p;  // 判断是否持有有效资源
*p; p->func();  // 解引用、访问成员,和裸指针用法一致

3.4 支持数组管理

unique_ptr<int[]> arr = make_unique<int[]>(10);
arr[0] = 10; // 数组下标访问,析构自动 delete[]

4. std::shared_ptr 共享智能指针

  • 多个 shared_ptr 可以托管同一块堆内存,内部维护原子引用计数器:
  • 拷贝一份,计数 + 1
  • 析构 /reset,计数 - 1
  • 计数归 0 时,自动释放堆内存

4.1 基础用法

// C++11 推荐 make_shared,内存分配一次(对象+计数器同一块内存)
auto p1 = make_shared<int>(999);
shared_ptr<int> p2 = p1; // 拷贝,计数+1,现在计数=2
cout << p1.use_count(); // 获取当前引用计数

4.2 拷贝、移动行为

auto p1 = make_shared<int>(10);
auto p2 = p1;        // 拷贝:计数+1
auto p3 = move(p1);  // 移动:p1置空,计数不变

4.3 自定义删除器(解决数组、特殊资源)

// 管理数组
shared_ptr<int> arr(new int[10], [](int* p){ delete[] p; });

4.4 缺陷

  • 两个对象互相持有对方的 shared_ptr,引用计数永远无法归 0,内存永远不释放。
struct Node
{
    int val;
    shared_ptr<Node> next;
    Node(int v):val(v){}
};

void loop_ref()
{
    auto n1 = make_shared<Node>(1);
    auto n2 = make_shared<Node>(2);
    n1->next = n2;
    n2->next = n1;
    // 函数结束,n1、n2析构,计数各减1,但互相持有,计数=1,内存泄漏!
}

解决方案:搭配 weak_ptr 打破循环引用。

4.5 性能开销

  • 引用计数是原子操作,多线程下有轻微性能损耗
  • make_shared 一次分配内存,比 shared_ptr(new) 更高效

5. std::weak_ptr 弱智能指针(配套 shared_ptr)

  • 不增加引用计数,仅作为 shared_ptr 的观察者,解决循环引用。
  • 不能单独管理资源,必须依附 shared_ptr。

5.1 接口

  • expired():判断指向的内存是否已经释放(计数 = 0)
  • lock():提升为 shared_ptr,若资源已销毁返回空 shared_ptr
  • use_count():查看关联的 shared_ptr 计数
  • 解决循环引用完整示例
#include <memory>
struct Node
{
    int val;
    weak_ptr<Node> next; // 改用weak_ptr,不增加计数
    Node(int v):val(v){}
};

void fix_loop()
{
    auto n1 = make_shared<Node>(1);
    auto n2 = make_shared<Node>(2);
    n1->next = n2; // weak_ptr,计数不变
    n2->next = n1;
    // 函数结束,计数直接归0,内存正常释放
}
  • weak_ptr 不能直接解引用,必须 lock () 转为 shared_ptr 再使用:
auto sp = make_shared<int>(10);
weak_ptr<int> wp = sp;
if(!wp.expired())
{
    auto temp = wp.lock();
    cout << *temp;
}