第一个问题:什么是智能指针,它有什么作用?
智能指针是 C++ 标准模板库(STL)中的一种类模板,用于管理动态分配的内存,以确保资源的正确释放,防止内存泄漏。在 C++ 中,动态分配内存是通过 new 操作符完成的,而释放内存则需要使用 delete 操作符。但在复杂的程序中,确保每次 new 后都有对应的 delete 调用是困难的,尤其是在出现异常或早期返回时。
智能指针通过封装原始指针,并在其析构函数中自动调用 delete,帮助程序员自动管理内存。C++ 提供了几种类型的智能指针,主要包括:
- unique_ptr:它是一种独占式智能指针,意味着它对其所管理的对象具有唯一的所有权。一旦 unique_ptr 被销毁,它所指向的对象也会被删除。它不支持复制,但可以进行移动,从而转移所有权。使用:
cpp
unique_ptr<int> p1(new int(100));
unique_ptr<int> p2 = p1; //编译直接报错!独占所有权,不能复制
unique_ptr<int> p2 = move(p1); //只能移动,p1变空
- shared_ptr :这是一种共享所有权的智能指针。多个 shared_ptr 可以指向同一个对象,内部使用引用计数来追踪有多少个 shared_ptr 指向同一个对象。当最后一个这样的指针被销毁时,对象才会被删除。什么时候需要拷贝 shared_ptr
场景:一份堆上的数据,程序里面好几个地方、好几个函数、多个线程都要访问它。普通裸指针:你很难判断**什么时候可以 delete。你不知道还有多少地方正在用这块内存。删早了 → 悬垂指针;忘记删 →内存泄漏。
shared_ptr 用引用计数就是用来解决这个痛点:记录当前有多少使用者。只有全部使用者都不用了,计数归 0,才自动释放堆内存。
- weak_ptr :它是 shared_ptr 的伴侣,提供了一种不控制对象生命周期的智能指针。它主要用于解决 shared_ptr 可能引起的循环引用问题。
应用场景举例:
unique_ptr、shared_ptr 基础用法:
cpp
//unique_ptr独占,只能move,不能复制
unique_ptr<int> p1(new int(100));
unique_ptr<int> p2 = move(p1); //所有权转移,p1变成空
//shared_ptr共享,引用计数增加
shared_ptr<int> s1 = make_shared<int>(200);
////在堆上开辟一块内存,存整数 200;同时自动创建一块控制块,里面保存引用计数(ref count)。此时引用计数 = 1。`s1` 是第一个共享智能指针指向这块堆内存。
//控制块和数据内存一起分配。s1 拥有对这个堆上 200 的管理权。当 s1 生命周期结束(出作用域),引用计数减 1;计数变成 0 的时候,就执行 delete 释放堆内存。
shared_ptr<int> s2 = s1; //引用计数+1
//`s2`是新的 shared_ptr,拷贝构造,让 s2 也指向同一块堆上的 200。因为现在有两个指针都在用同一份堆对象,所以控制块里面的引用计数+1。此时引用计数 = 2。
//weak_ptr观察shared_ptr
weak_ptr<int> w1 = s1;//w1 只是 "旁观、记录地址",`s1`的`use_count()`仍然等于 1,不会变成 2
//w1 自己不能直接写 `*w1`访问数据!没有重载`*`、`->`,它没有使用权。
auto tmp = w1.lock(); //临时得到shared_ptr访问
//`lock()`干的事情:1. 先检查:这块堆上的 int 对象还活着吗?有没有已经被释放掉?
//2. 如果对象还存在:新建一个临时的 shared_ptr 叫做 tmp;引用计数临时 +1。现在 use_count 变成 2。现在你就靠 tmp 去读写数据。
//3. 如果对象已经销毁了,lock 返回空的 shared_ptr。
if(tmp)
//必须写这个 if!不能省略!因为有可能`s1`早就已经销毁,内存已经释放了,lock 就返回空。如果直接解引用就等于访问已经释放的内存,程序直接崩溃。
{
cout << *tmp << endl;//通过临时 shared‑ptr `tmp`读取堆里面的值。
}
//`tmp`是局部变量,离开`if{}`作用域,`tmp`就析构销毁!引用计数立刻减回去,又变回1。
关键点总结:
lock 带来的 shared_ptr 只是临时增加计数,用完马上消失。
weak_ptr 自始至终不会永久性增加引用计数!
这就是它为什么可以解决循环引用的根源。
-
unique_ptr:当你在一个函数中创建了一个对象,并且想在函数结束时自动销毁它,可以使用 unique_ptr。例如,在一个图形应用中创建一个图像对象,当处理完毕后自动释放。
-
shared_ptr:在需要多个指针共享同一个对象时,例如在实现一个树结构,多个节点可能共享相同的子节点。
-
weak_ptr:在创建复杂的数据结构如图或树时,weak_ptr 可以用来避免循环引用,从而避免内存泄漏。比如在树的节点中,父节点使用 shared_ptr 指向子节点,而子节点使用 weak_ptr 指向父节点。
简单完整小 demo,你可以跑一下观察计数:
cpp
#include <iostream>
#include <memory>
using namespace std;
int main()
{
shared_ptr<int> s1 = make_shared<int>(200);
cout << s1.use_count() << endl; //输出1
shared_ptr<int> s2 = s1;
cout << s1.use_count() << endl; //输出2,use_count查看引用计数
return 0;
}
use_count():可以查看当前的引用计数值。
💡注意后面的坑:不要用裸指针再去初始化 shared_ptr,容易出现两套独立的控制块,导致重复释放!优先使用 make_shared。
循环引用:
循环引用错误版本(内存泄漏,两个 shared_ptr 互相持有):
cpp
#include <iostream>
#include <memory>
using namespace std;
class B;
class A {
public:
shared_ptr<B> ptrB;
~A() { cout << "A被析构释放\n"; }
};
class B {
public:
shared_ptr<A> ptrA;
~B() { cout << "B被析构释放\n"; }
};
int main()
{
{
shared_ptr<A> spA = make_shared<A>(); //A计数 =1
shared_ptr<B> spB = make_shared<B>(); //B计数 =1
spA->ptrB = spB; //B的引用计数 +1 → B计数=2
spB->ptrA = spA; //A的引用计数 +1 → A计数=2
}
//离开内层作用域!spA、spB这两个局部shared_ptr销毁
//spA析构:A计数减1 → A计数 = 1(不是0!)
//spB析构:B计数减1 → B计数 = 1(不是0!)
//A还被B内部的ptrA持有;B还被A内部ptrB持有。
//两个对象谁都释放不了!不会打印析构信息,发生内存泄漏。
return 0;
}
修改,把 B (其中一方)里面改成 weak_ptr,用 lock () 访问(修复泄漏):
不要两边都用 shared_ptr。其中一条引用链路改用 weak_ptr(观察者,不增加引用计数)。
修改 B 里面的成员,`shared_ptr<A> ptrA;` → `weak_ptr<A> ptrA;`
cpp
#include <iostream>
#include <memory>
using namespace std;
class B;
class A {
public:
shared_ptr<B> ptrB;
~A() { cout << "A被析构释放\n"; }
};
class B {
public:
weak_ptr<A> ptrA; //✅改成弱指针!!不增加A的引用计数
void show()
{
auto temp = ptrA.lock(); //临时升级shared_ptr访问A
if(temp)
{
cout << "访问A对象成功\n";
}
}
~B() { cout << "B被析构释放\n"; }
};
int main()
{
{
shared_ptr<A> spA = make_shared<A>(); //A计数 =1
shared_ptr<B> spB = make_shared<B>(); //B计数 =1
spA->ptrB = spB; //B计数 +1 → B计数=2
spB->ptrA = spA; //⚠️这里赋值给weak_ptr,A计数!!仍然等于1,不会+1
spB->show();
}
//离开作用域,局部spA先销毁:A引用计数1‑1=0
//A对象立刻析构!A里面成员ptrB跟着销毁;B的引用计数2‑1=0;B对象随之析构释放。
return 0;
}
运行输出:
cpp
访问A对象成功
A被析构释放
B被析构释放
口述:当两个对象内部互相使用 shared_ptr 指向对方的时候,就会发生循环引用。双方的引用计数都无法降到 0,造成内存泄漏。
解决思路:把其中一方改成 weak_ptr。weak_ptr 作为观察者不会增加引用计数,打破循环。
weak_ptr 不能直接访问对象;访问的时候调用 lock () 得到临时 shared_ptr,访问结束后临时 shared_ptr 销毁,引用计数回落。
完整小测试代码,可以运行看计数变化:
cpp
#include <iostream>
#include <memory>
using namespace std;
int main()
{
shared_ptr<int> s1 = make_shared<int>(200);
cout << "s1引用计数:" << s1.use_count() << endl; //1
weak_ptr<int> w1 = s1;
cout << "赋值weak_ptr之后计数:" << s1.use_count() << endl; //仍然是1!
{
auto tmp = w1.lock(); //临时shared_ptr,计数+1
cout << "lock之后计数:" << tmp.use_count() << endl; //2
if(tmp)
{
cout << *tmp << endl;
}
}
//离开内层大括号,tmp销毁
cout << "tmp销毁之后计数:" << s1.use_count() << endl; //回到1
return 0;
}
口述版本(背诵)
weak_ptr 只是作为观察者,本身不增加引用计数,不能够直接访问对象。
需要调用 lock () 方法,lock 会去检查对象是否存活。
如果对象还存在,就返回一个临时的 shared_ptr,这时引用计数临时加 1,就可以访问资源;
如果对象已经释放,lock 返回空智能指针。
使用前必须判断返回的 shared_ptr 是否有效。
临时 shared_ptr 出作用域之后自动销毁,引用计数回落。
正是因为 weak_ptr 不会永久持有引用计数,所以可以打破 shared_ptr 之间的循环引用。
希望对你有所帮助