当前位置: 代码迷 >> 综合 >> C++智能指针:unique_ptr、shared_ptr、weak_ptr
  详细解决方案

C++智能指针:unique_ptr、shared_ptr、weak_ptr

热度:86   发布时间:2023-11-24 01:41:52.0

文章目录

    • 为什么需要智能指针
    • unique_ptr
      • unique_ptr常用操作
    • shared_ptr
      • share_ptr常用的操作
      • 引用计数变化的情况
      • move语法
      • 使用注意
      • 弊端:循环引用
    • weak_ptr
      • weak_ptr常用的操作
    • 如何选择智能指针

为什么需要智能指针

程序使用动态内存出于以下三种原因之一:

  1. 程序不知道自己需要使用多少对象
  2. 程序不知道所需对象的准确类型
  3. 程序需要在多个对象间共享数据

而在C++中动态内存的管理是通过一对运算符来完成的,即newdelete,但是动态内存的使用很容易产生以下问题:

  • 忘记delete内存,导致内存泄露问题,而且这种问题很难排查,往往出现在程序运行阶段而不是编译阶段。

  • 使用已释放的对象。

    通过释放内存后,将指针置为空,有时可以检测出这种错误

  • 同一块内存被释放两次。当两个指针指向相同的动态分配对象时,如果其中一个指针进行了delete操作,对象的内存就被归还给自由空间了,如果我们随后又delete第二个指针,自由空间就可能被破坏。
    在这里插入图片描述
    所以为了使开发人员能够更容易更安全的使用动态内存,就需要智能指针,智能指针的使用类似于指针,最主要的区别是它负责自动释放所指向的对象,目前有3种智能指针,分别是:

  • shared_ptr:允许多个指针指向同一个对象

  • unique_ptr:独占所指向的对象

  • weak_ptr:弱引用,指向shared_ptr所管理的对象

STL一共提供了四种智能指针:auto_ptr、unique_ptr、shared_ptr和weak_ptr

auto_ptr功能类似于unique_ptr,但是其在C++11中已经废弃,被unique_ptr替代,原因是auto_ptr不够安全,且支持拷贝和赋值,如p2= p1,p2会接管p1原来的内存管理权,p1会变为空指针,如果p2原来不为空,则它会释放原来的资源,基于这个原因,应该避免将auto_ptr放到容器中,因为算法对容器操作时,很难避免STL内部对容器实现了赋值传递操作,这样会使容器中很多元素被置为NULL。

在C++17中auto_ptr已被废除。

unique_ptr

一个unique_ptr拥有它所指向的对象。某个时刻只能有一个unique_ptr指向一个给定对象。当unique_ptr被销毁时,它指向的对象也被销毁。

由于unique_ptr独占的特点,所以unique_ptr不支持普通的拷贝或者赋值操作(拷贝或赋值一个将要被销毁的unique_ptr例外,比如函数返回一个unique_ptr),定义一个unique_ptr时,需要将其绑定到一个new返回的指针上,且也必须使用直接初始化方式

例如:unique_ptrp(new int(10))

unique_ptr常用操作

  • u.release():u放弃对指针的控制权,返回内置指针(即new出来的指针),并将u置为空

    class A {
          
    public:~A() {
          cout << "~A()" << endl;}
    };
    int main()
    {
          unique_ptr<A>p1(new A);//错误:release函数只是将p1置为空,并不会释放其指向的内存。而运行后并没有任何输出,说明没有调用A的析构函数,也说明release函数不会释放其指向的内存。p1.release();return 0;
    }
    

    注意:该操作通常用来初始化另一个智能指针,如果我们不用另一个智能指针保存其返回的指针,我们的程序就要负责资源的释放

  • u.reset():释放u指向的对象

  • u.reset(q):令u指向q,q必须是内置指针;

通过release或reset将指针的所有权从一个(非const)unique_ptr转移给另一个unique_ptr;

int main()
{
    unique_ptr<int>p1(new int(10));unique_ptr<int>p2(p1.release()); //release将p1置空,并指针的控制权交给p2p1.get() ? (cout << "p1的值为:" << *p1.get() << endl) : (cout << "p1为空指针" << endl);p2.get() ? (cout << "p2的值为:" << *p2.get() << endl) : (cout << "p2为空指针" << endl);unique_ptr<int>p3(new int(20));p2.reset(p3.release());//将所有权从p3转交给p2,并释放原先p2指向的对象p2.get() ? (cout << "p2的值为:" << *p2.get() << endl) : (cout << "p2为空指针" << endl);p3.get() ? (cout << "p3的值为:" << *p3.get() << endl) : (cout << "p3为空指针" << endl);return 0;
}

输出结果为:
在这里插入图片描述

shared_ptr

与一个对象只能被一个unique_ptr拥有不同,对象可被多个shared_ptr共享,在每个shared_ptr对象中,都有一个计数器,我们通常称为引用计数,该引用计数会记录有多少个其他shared_ptr指向相同的对象。

对于一块内存,shared_ptr类保证只要有任何shared_ptr对象引用它,即引用计数不为0,就不会被释放掉,相反,一旦一个shared_ptr对象的引用计数变为0,它就会自动销毁所管理的对象并自动释放相关联的内存。

因此通过该引用计数从而允许多个指针共享一个对象。

share_ptr常用的操作

  • make_shared<T>(args): 返回一个shared_ptr,指向一个动态分配的类型为T的对象。使用args初始化对象。该函数也是最安全的分配和使用动态内存的方法。
  • shared_ptr<T>p(q):p是shared_ptr q的拷贝
  • p.use_count():返回与p共享对象的智能指针数量;
  • p.get():返回p中保存的指针。要小心使用,若智能指针释放了其对象,返回的指针所指向的对象也就消失了,且返回的是内置指针
  • p.reset():若p是唯一指向其对象的shared_ptr,reset会释放此对象

注意,我们不能将一个内置指针隐式转换为一个智能制造,所以shared_ptr必须使用直接初始化形式,即

shared_ptrp1(new int(1024)); 正确

!!!shared_ptrp1=new int(1024); 错误(拷贝初始化)

引用计数变化的情况

情况1:用一个shared_ptr初始化另一个shared_ptr,引用计数增加

情况2:shared_ptr作为参数传给一个函数,引用计数增加

情况3:shared_ptr作为函数返回值,引用计数增加

情况4:shared_ptr的赋值操作,赋值号右边的shared_ptr引用计数增加,左侧的shared_ptr的引用计数减小

情况5:shared_ptr离开了其作用域,引用计数减小

void func1(shared_ptr<int>ptr) {
    cout << "p1作为参数传递给函数func1后,p1的引用计数:" << ptr.use_count() << endl;
}shared_ptr<int>func2(shared_ptr<int>ptr) {
    return ptr;
}
int main()
{
    shared_ptr<int>p1 = make_shared<int>(10); //p1指向的int只有一个引用者cout << "p1的引用计数:" << p1.use_count() << endl;cout << "=============================================" << endl;cout << "情况1" << endl;shared_ptr<int>p2(p1);//p1和p2都指向相同对象,cout << "p1的引用计数:" << p1.use_count() << endl;cout << "p2的引用计数:" << p2.use_count() << endl;cout << "=============================================" << endl;cout << "情况2" << endl;func1(p1);//拷贝p1会增加它的引用计数,在func1中引用计数为3cout << "程序执行完函数func1后,p1的引用计数:" << p1.use_count() << endl;cout << "=============================================" << endl;cout << "情况3" << endl;{
    shared_ptr<int>p3 = func2(p1);cout << "shared_ptr作为函数返回值给p3后,p1的引用计数:" << p1.use_count() << endl;}cout << "p3离开其作用域后,p1的引用计数:" << p1.use_count() << endl;cout << "=============================================" << endl;cout << "情况4" << endl;shared_ptr<int>p4 = make_shared<int>(20);shared_ptr<int>p5(p4);cout << "被赋值前p4的引用计数:" << p4.use_count() << endl;cout << "被赋值前p1的引用计数:" << p1.use_count() << endl;p5 = p1;cout << "被赋值后p4的引用计数:" << p4.use_count() << endl;cout << "被赋值后p1的引用计数::" << p1.use_count() << endl;cout << "=============================================" << endl;
}

输出结果:
在这里插入图片描述

从情况1中我们其实可以看到,当某个对象的引用计数增加时,所有指向该对象的shared_ptr的引用计数都会增加

在这里插入图片描述

从内存角度

在程序执行的过程中,p1、p2、ptr、p3、p5都指向过同一块内存单元,但是当执行到p4的时候,由于此时ptr和p3都已经不再其作用域,所以不再指向内存单元。从图中ptr和p3变灰可以看出。

上面的动态图中,在监视窗口的值中,strong refs 表示当前有多少指针指向同一个对象,当该值增加或减少时,会同时更改与该shared_ptr指向相同对象的其他shared_ptr的strong refs

当引用计数为0时,即指向一个对象的最后一个shared_ptr被销毁时,shared_ptr类会自动销毁次对象,并释放它所占的内存

int main()
{
    shared_ptr<int>p1 = make_shared<int>(10); //p1指向的int只有一个引用者shared_ptr<int>p2 = make_shared<int>(20);p1 = p2;return 0;
}

在这里插入图片描述
在这里插入图片描述

可以看到执行完p1=p2后,原先p1所指向的对象被销毁

move语法

将一个shared_ptr所指向对象的所有权转交给另一个shared_ptr,并将原先的shared_ptr置空;

int main()
{
    shared_ptr<int>p1=make_shared<int>(10);p1.get() ? (cout << "p1的值为:" << *p1.get() << endl) : (cout << "p1为空指针" << endl);shared_ptr<int>p2 = move(p1);p1.get() ? (cout << "p1的值为:" << *p1.get() << endl) : (cout << "p1为空指针" << endl);cout << "p1的引用计数:" << p1.use_count() << endl;cout << "p2的引用计数:" << p2.use_count() << endl;return 0;
}

在这里插入图片描述
可以看到执行完move操作后,p1的指针被置空,其所指向的对象所有权交付给了p2

输出结果为:
在这里插入图片描述

使用注意

  • 不要混合使用普通指针和智能指针
void process(shared_ptr<int>ptr) {
    }
int main()
{
    int *x = new int(10);process(shared_ptr<int>(x));//虽然不会出错,但是执行完后,内存会被释放int val = *x; //此时x是一个空悬指针,结果未知return 0;
  • 永远不要用get初始化另一个只能指针或者为另一个智能指针赋值
int main()
{
    shared_ptr<int>p1 = make_shared<int>(10); {
    shared_ptr<int>p2(p1.get());cout << "p1的引用计数:"<<p1.use_count() << endl;//输出为1cout << "p2的引用计数:"<<p2.use_count() << endl;//输出为1}return 0;
}

在这里插入图片描述

从上图可以看到虽然p1和p2指向相同的内存,但是它们是相互独立的,因为各自的引用计数都是1,当p2所在的程序块结束时,p2被销毁,这会导致p1所指向的内存被释放,从而p1变成空悬指针

弊端:循环引用

引用计数虽然允许多个shared_ptr指向同一个对象,但是会带来循环引用的问题,循环引用会导致堆里的内存无法正常回收,造成内存泄露。

在这里插入图片描述

weak_ptr

weak_ptr是不控制所指向对象生命周期的智能指针,也就是说即使有weak_ptr指向对象,对象还是会被释放,且weak_ptr指向由shared_ptr管理的对象,将weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数,当所有shared_ptr析构了之后,不管有没有weak_ptr引用该内存,内存还是会被释放,从而可以有效解决循环引用的问题

class B;
class A
{
    
public:shared_ptr<B>pA;~A() {
    cout << "~A()" << endl;}
};
class B
{
    
public:shared_ptr<A>pB;~B() {
    cout << "~B()" << endl;}
};
class BW;
class AW
{
    
public:shared_ptr<BW>pA;~AW() {
    cout << "~AW()" << endl;}
};
class BW
{
    
public:weak_ptr<AW>pB;~BW() {
    cout << "~BW()" << endl;}
};
void Test1() {
    shared_ptr<A>ta(new A());shared_ptr<B>tb(new B());cout << "ta的引用计数:" << ta.use_count() << endl;cout << "tb的引用计数:" << tb.use_count() << endl;ta->pA = tb;tb->pB = ta;cout << "ta的引用计数:" << ta.use_count() << endl;cout << "tb的引用计数:" << tb.use_count() << endl;
}
void Test2() {
    shared_ptr<AW>ta(new AW());shared_ptr<BW>tb(new BW());cout << "ta的引用计数:" << ta.use_count() << endl;cout << "tb的引用计数:" << tb.use_count() << endl;ta->pA = tb;tb->pB = ta;cout << "ta的引用计数:" << ta.use_count() << endl;cout << "tb的引用计数:" << tb.use_count() << endl;
}
int main()
{
    Test1();cout << "=======================" << endl;Test2();return 0;
}

输出结果为:
在这里插入图片描述

可以看到在Test1()中,pA和pB存在循环引用的问题,在调用完Test1()后,pA和pB并没有正常释放,因为没有调用相应的析构函数;而当采用weak_ptr后,有效解决了这个问题,而从Test2()中ta的引用计数为1,tb的引用计数为,也验证了weak_ptr具有不会改变shared_ptr的引用计数的特点。

weak_ptr常用的操作

  • w.reset():将w置空
  • w.use_count():与w共享对象的shared_ptr的数量
  • w.expired():若w.use_count()为0,返回true,否则返回false
  • w.lock():如果expired为true,返回一个空shared_ptr;否则返回一个指向w的对象的shared_ptr

由于weak_ptr不会改变shared_ptr的引用计数,所以weak_ptr所指向的对象可能被释放掉,所以我们不能使用weak_ptr直接访问对象,而必须调用lock(),从而检查weak_ptr指向的对象是否仍然存在,如果存在,则返回一个指向共享对象的shared_ptr.

如何选择智能指针

  • 如果程序要使用多个指向同一个对象的指针,应选择 shared_ptr。这样的情况包括:
    1. 将指针作为参数或者函数的返回值进行传递的话,应该使用 shared_ptr;
    2. 两个对象都包含指向第三个对象的指针,此时应该使用 shared_ptr 来管理第三个对象;
    3. STL 容器包含指针。很多 STL 算法都支持复制和赋值操作,这些操作可用于 shared_ptr,但不能用于 unique_ptr(编译器发出 warning)
  • 如果程序不需要多个指向同一个对象的指针,则可使用 unique_ptr。
  • 为了解决 shared_ptr 的循环引用问题,应该使用 weak_ptr。

参考:

《C++ Primer (第5版)中文版》

C++ STL 四种智能指针的用法详解
c++ 智能指针用法详解