智能指针是什么?使用场景,循环引用解决办法,面试回答

第一个问题:什么是智能指针,它有什么作用?

智能指针是 C++ 标准模板库(STL)中的一种类模板,用于管理动态分配的内存,以确保资源的正确释放,防止内存泄漏。在 C++ 中,动态分配内存是通过 new 操作符完成的,而释放内存则需要使用 delete 操作符。但在复杂的程序中,确保每次 new 后都有对应的 delete 调用是困难的,尤其是在出现异常或早期返回时。

智能指针通过封装原始指针,并在其析构函数中自动调用 delete,帮助程序员自动管理内存。C++ 提供了几种类型的智能指针,主要包括:

  1. unique_ptr:它是一种独占式智能指针,意味着它对其所管理的对象具有唯一的所有权。一旦 unique_ptr 被销毁,它所指向的对象也会被删除。它不支持复制,但可以进行移动,从而转移所有权。使用:
cpp 复制代码
unique_ptr<int> p1(new int(100));
unique_ptr<int> p2 = p1; //编译直接报错!独占所有权,不能复制
unique_ptr<int> p2 = move(p1); //只能移动,p1变空
  1. shared_ptr :这是一种共享所有权的智能指针。多个 shared_ptr 可以指向同一个对象,内部使用引用计数来追踪有多少个 shared_ptr 指向同一个对象。当最后一个这样的指针被销毁时,对象才会被删除。什么时候需要拷贝 shared_ptr

场景:一份堆上的数据,程序里面好几个地方、好几个函数、多个线程都要访问它。普通裸指针:你很难判断**什么时候可以 delete。你不知道还有多少地方正在用这块内存。删早了 → 悬垂指针;忘记删 →内存泄漏。

shared_ptr 用引用计数就是用来解决这个痛点:记录当前有多少使用者。只有全部使用者都不用了,计数归 0,才自动释放堆内存。

  1. 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 自始至终不会永久性增加引用计数!

这就是它为什么可以解决循环引用的根源。

  1. unique_ptr:当你在一个函数中创建了一个对象,并且想在函数结束时自动销毁它,可以使用 unique_ptr。例如,在一个图形应用中创建一个图像对象,当处理完毕后自动释放。

  2. shared_ptr:在需要多个指针共享同一个对象时,例如在实现一个树结构,多个节点可能共享相同的子节点。

  3. 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 之间的循环引用。

希望对你有所帮助

相关推荐
ao-weilai1 小时前
Linux网络编程:Socket UDP
linux·服务器·网络·c++
OKkankan1 小时前
Python 基础进阶(三):从函数、类到 asyncio 与 FastAPI 后端开发实战
开发语言·python
泡海椒2 小时前
jquick-pdf 防止 PDF 内容分页断裂:keepTogether 属性妙用
java·开发语言·pdf
.YM.Z3 小时前
C++——【继承】的底层逻辑与【多态】实现机制
c++·多态·继承
汉克老师3 小时前
GESP2026年9月认证C++三级( 第一部分选择题(1~7题)精讲
c++·gesp·小学生·学c++编程
传奇开心果编程3 小时前
【Rust入门练中学】 第1课:从零开始
开发语言·学习·rust
Brilliantwxx4 小时前
【STM32】 SPI阻塞式通信与HAL源码(串口读ID实战)
开发语言·stm32·单片机·嵌入式硬件·ecmascript
吠品4 小时前
Nginx负载均衡配置与线上故障排查的一些经验
java·开发语言·数据库