目录
为什么需要智能指针?
什么是智能指针?
智能指针的本质
三种智能指针(来自 头文件)
1. std::unique_ptr
3. std::weak_ptr
引用计数(reference counting)
什么是循环引用?为什么需要 weak_ptr?
什么时候用哪种智能指针?
第一性角度再总结一遍:
我们从一个根本问题开始:
C++ 中我们用 new
创建对象,释放时要用 delete
,你自己负责内存管理。但如果你忘了 delete
呢?
void func() {
int* p = new int(10);
// 忘了 delete p;
}
这段代码执行完后,堆上的内存没有被释放,内存泄漏了!
每次 new
都要记得 delete
多个函数共享一个指针,delete 一次还好,多了就崩溃
异常发生时,如果没进到 delete
,也会泄漏
于是,我们可以问一个第一性问题:
有没有一种机制,当对象不再需要时,自动帮我释放内存?
C++ 说:有!那就是——智能指针(smart pointers)。
通俗来说:智能指针就是**“带有脑子的指针”,它不仅能像普通指针一样指向对象,还能在合适的时候自动释放对象的内存**。
它封装了一个裸指针,但同时还管理这个指针的生命周期,防止你忘记释放资源。
template
class SmartPointer {
private:
T* ptr;
public:
SmartPointer(T* p) : ptr(p) {}
~SmartPointer() { delete ptr; }
T* operator->() { return ptr; }
T& operator*() { return *ptr; }
};
这个简化版就能说明问题了:
构造时接管一个裸指针
析构时自动 delete
重载 *
和 ->
,让它像普通指针一样使用
这就是智能指针的基本思想!
头文件)独占型智能指针,一个对象只能有一个主人。
#include
std::unique_ptr p = std::make_unique(42);
std::cout << *p << std::endl;
//转移所有权:
std::unique_ptr q = std::move(p); // p 被“清空”,资源被 q 拥有
特点:
只能有一个指针拥有这块资源
不可复制(copy),但可以移动(move)
离开作用域后自动 delete
底层实现:unique_ptr
持有一个裸指针,且该指针在 unique_ptr
析构时会被释放。unique_ptr
支持移动语义(std::move
),允许所有权转移,但禁止拷贝。
[unique_ptr] --> [资源]
共享型智能指针,多个指针可以共享一个对象。
#include
std::shared_ptr p1 = std::make_shared(42);
std::shared_ptr p2 = p1; // 引用计数 +1
特点:
多个 shared_ptr
可以共同管理一块内存
使用引用计数(reference counting)
最后一个 shared_ptr
被销毁时,资源才被释放
#include
auto p1 = std::make_shared(100);
auto p2 = p1; // 引用计数 +1
你可以用 .use_count()
查看当前有几个指针共享:
std::cout << p1.use_count(); // 输出 2
底层实现:shared_ptr
内部会维护一个引用计数,当一个 shared_ptr
被复制时,它会增加引用计
数;当一个 shared_ptr
被销毁时,它会减少引用计数。只有当引用计数为 0 时,资源才会被释
放。
[shared_ptr1] -->┐
[shared_ptr2] -->│--> [资源] + [引用计数]
[shared_ptr3] -->┘
弱引用,不参与引用计数,防止循环引用
#include
std::shared_ptr a = std::make_shared();
std::weak_ptr wa = a; // wa 不增加引用计数
特点:
不增加引用计数
不会影响资源释放
可用于解决 shared_ptr 循环引用的问题
不能直接使用,要用 .lock()
转成 shared_ptr
std::shared_ptr sp = std::make_shared(123);
std::weak_ptr wp = sp;
if (auto spt = wp.lock()) {
std::cout << *spt << std::endl;
}
底层实现:weak_ptr
内部持有一个指向 shared_ptr
的指针,但不增加引用计数。当你想访问
weak_ptr
所管理的对象时,需要通过 lock()
方法将它转换为一个有效的 shared_ptr
,如果资源已
被释放,lock()
返回一个空的 shared_ptr
。
[weak_ptr] -X-> [资源]
什么是引用计数?
引用计数是一种自动管理资源生命周期的机制。
它的核心思想是:
“有多少人正在用这块资源?我就记录一个‘计数器’。当没人用了,我就把它释放掉。”
这个“多少人”指的就是指向资源的智能指针(通常是 shared_ptr
)。
为什么需要引用计数?
比如你用 shared_ptr
管理一个对象:
auto p1 = std::make_shared(10);
auto p2 = p1;
这个资源(内存里的 10
)现在有两个指针在用它(p1 和 p2)。
引用计数的目标是:
每次有一个新指针指向资源,计数 +1
每次有一个指针销毁或离开作用域,计数 -1
当计数 = 0,说明没人用了,自动 delete
资源
怎么实现引用计数(背后的机制)?
通常情况下,一个 shared_ptr
实际上会维护一个 控制块(control block),里面包括:
┌─────────────────────────────┐
│ 控制块 Control Block │
├─────────────────────────────┤
│ 资源指针 ptr → new T(...) │
│ 引用计数 use_count │
│ 弱引用计数 weak_count │
└─────────────────────────────┘
use_count
:当前有几个 shared_ptr
正在使用资源
weak_count
:有几个 weak_ptr
正在“观察”资源
#include
#include
int main() {
std::shared_ptr p1 = std::make_shared(42);
std::shared_ptr p2 = p1; // use_count = 2
std::shared_ptr p3 = p2; // use_count = 3
std::cout << p1.use_count() << std::endl; // 输出 3
p2.reset(); // use_count = 2
std::cout << p1.use_count() << std::endl;
p3.reset(); // use_count = 1
std::cout << p1.use_count() << std::endl;
p1.reset(); // use_count = 0,资源自动释放 ✅
}
行为 | 引用计数变化 |
---|---|
拷贝 shared_ptr | +1 |
reset() 或销毁 | -1 |
计数为 0 | 自动释放资源 |
weak_ptr
?循环引用是指:当两个对象的 shared_ptr
相互指向对方时,它们的引用计数不会归零,因为它们各自持有对方的 shared_ptr
。这导致对象永远不能被销毁,从而产生内存泄漏。
解决办法是,使用 weak_ptr
代替其中一个 shared_ptr
,这样它就不会增加引用计数,防止了循环引用。
struct Node {
std::shared_ptr next;
};
std::shared_ptr a = std::make_shared();
std::shared_ptr b = std::make_shared();
a->next = b;
b->next = a; // ❌ 循环引用,永远不会释放!
正确写法:
struct Node {
std::weak_ptr next;
};
这样 b
指向 a
的引用就不会增加计数了,内存可以正常释放。
类型 | 适合场景 |
---|---|
unique_ptr |
默认首选,局部资源管理,函数返回,单一所有者 |
shared_ptr |
多个模块共享资源(如树状结构、观察者模式) |
weak_ptr |
避免 shared_ptr 的循环引用 |
智能指针的本质是:
RAII:Resource Acquisition Is Initialization
——资源获取即初始化,对象一创建就接管资源,一销毁就自动释放。
它通过:
构造函数负责获取资源
析构函数负责释放资源
实现了对象生命周期 = 资源生命周期