c++智能指针详解

看到“智能指针”这个名字,脑海里浮现出什么?反正开始时我的脑海里出现了几个名字,shared_ptr/unique_ptr/weak_ptr,然后就没有然后了,至于如何使用脑子里一片空白,我相信很多小伙伴面临着跟我一样的窘境。产生的这样的问题究其根因还是对知识的掌握流于表面,经不起推敲。本文在这里进行总结,并不能让面前的读者看完达到完全融会贯通的程度,因为那需要大量的联系,如果对智能指针有着深刻理解的大神,请略过本文。

先说说为什么需要智能指针。下面几种罗列几种情况说明在程序中使用普通类型可能带来的问题。

  • 用户通过new在堆上申请一片内存,如果忘记delete就会引起内存泄漏。

  • 如果申请的内存,在程序结束前异常退出,也会引起内存泄漏。

  • 用户申请的内存如果重复释放,造成程序崩溃。

  • 悬空指针问题,即两个指针指向同一片内存,其中一个指针调用delete释放了该内存,然后另一个指针却发生再次使用指针的情况。

普通指针引起的问题必须通过用户在代码中通过做一些保护才能避免问题发生。这是一件很头疼的问题并且总是会出现疏漏。那有没有一种机制可以实现用户申请的内存自动释放呢?这时候就引入了智能指针。

智能指针就是为了解决用户申请的动态内存释放的问题,动态内存的释放达到一定条件时自动释放。能够大大的减少内存泄漏的发生。所以建议在c++程序中在动态申请内存指针时尽量使用智能指针。

下面将主要对4中智能指针进行介绍。

  1. atuo_ptr

存在安全隐患,类似unique_ptr,目前已经禁止使用。这里不在进行过多赘述。

  1. unique_ptr

这是一个独占性智能指针,即这个指针只能通过构造函数的形式初始化,无法通过其他的方式进行赋值。例如:

std::unique_ptr<int> uptr(new int(10));  //正确
std::unique_ptr<int> uptr = new int(10);  //非法,无对应的构造函数
std::unique_ptr<int> uptr1 = uptr;  //错误,unique_ptr模板类禁用了拷贝构造函数
std::unique_ptr<int> uptr2;  //合法,允许建立空对象但是没有意义
uptr2 = uptr; //错误,unique_ptr禁用了赋值构造函数函数

至于unique_ptr如何实现禁用赋值或者拷贝功能就是通过私有化或者利用关键字delete修饰拷贝构造函数或者赋值构造函数来实现的。

  1. shared_ptr

shared_ptr是c++11引入的非常重要的一种强引用指针。区别于unique_ptr,多个shared_ptr可以指向同一片内存资源,通过引用计数的方式来实现内存的释放。每增加一个shared_ptr指向该内存资源,计数+1,;每当析构一个shared_ptr,则计数-1。当计数为0时,表示没有任何shared_ptr指向该内存资源,该内存资源就可以进行释放。

shared_ptr我个人认为最重要的是要从源码的角度去理解,即shared_ptr模板类中包含两个指针,一个指针指向被管理对象,另外一个是指向管理者对象(引用计数对象),这两个对象都是在shared_ptr的普通构造函数中生成的。我个人总结的要理解shared_ptr就要知晓谁是被管理对象,这个被管理对象被哪个shared_ptr引用了,shared_ptr生成的对象与被管理者对象是独立的,当shared_ptr中引用计数对象的计数为0时,会触发释放被管理者对象

shared_ptr一般与make_shared配合使用,make_shared有好处。

shared_ptr模板类提供了一些接口方法,这里不一一进行描述。可以通过度娘得到很多的介绍。

需要注意的是shared_ptr可以通过*或者->来访问数据(从这一点看shared_ptr是强引用指针),例如

class A{
public:
int a;
public:
A(int value)
{
    a = value;
}
}
std::shared_ptr<A> ptr(new A(6));
cout<<"a value =" <<ptr->a<<endl;

shared_ptr错误的使用方法:

错误一:

int *p = new int(10);
std::shared_ptr<int> ptr1(p);  //正确
std::shared_ptr<int> prt2(p);  //用法错误,这里ptr1和ptr2虽然指向了同一片内存,但是这两
个shared_ptr智能指针分别申请了各自的引用计数对象空间,造成了一个内存对象p有两个引用计数对象进行
管理,会造成重复释放的问题。
//如何避免出现上面的问题呢
std::shared_ptr<int> ptr2 = ptr1;  //正确,通过调用拷贝构造函数或者赋值函数来获取,只有这样
才会不同的shared_ptr对象共享同一个引用计数对象空间。

错误二(循环引用):

using namespace std;
class Node{
public:
int data;
shared<Node> pNext;
shared<Node> pPre;
}
int main()
{
    Node* node1 = new Node;
    Node* node2 = new Node;
    node1->data = 10;
    node2->data = 20;
    shared_ptr<Node> ptr1 = node1;
    shared_ptr<Node> ptr2 = node2;
    ptr1->pNext = ptr2;
    ptr2->pPre = ptr1;
    return 0;
}

上述会造成node1和node2对应的内存无法释放,原因如下:

  1. node1指向的对象被ptr1引用,此时use_count为1;

  1. node2指向的对象被ptr2引用,此时use_count为1;

  1. node2指向的对象又被ptr1中的pNext引用,此时use_count变为2;

  1. node1指向的对象又被ptr2中的pPre引用,此时use_count变为2;

  1. main函数结束,由于ptr1和ptr2是局部变量,作用域在函数内部,函数接收,这两个shared_ptr对象会释放,此时引用node1和node2的use_count变为1。

  1. node1和node2指向的对象内存泄漏

原因分析:由于node1的中pNext也指向了node2,node2无法释放,而若想要释放node1,因为node2同样存在pPre指向了node1,造成node1也无法释放,node1和node2在互相等待对方的释放,造成死锁的现象。故内存泄漏。

解决方法:只要使这两个类中的shared_ptr任意一个修改为weak_ptr就可以了(思考一下为什么^-^)。

4 weak_ptr

weak_ptr是一种弱引用指针,这里是相对shared_ptr而言的,weak_ptr不能进行*或者->操作,因为没有weak_ptr没有重载这两中操作符,所以才称他为弱引用指针,不能通过weak_ptr对象访问数据成员或者方法。

weak_ptr是为了配合shared_ptr出现的,是为了解决shared_ptr的问题出现的,例如循环引用。weak_ptr不能独立使用。他必须从shared_ptr对象或者其他weak_ptr对象取值。

weak_ptr的原理是在管理者对象(引用计数对象)中新增一个数据成员标记当前weak_ptr引用有多少个,该数据成员的数量不能决定其指向的对象是否释放,只有shared_ptr对应的计数数量才会决定是否释放被管理者对象。

5 思考:如果一个类实例通过函数接口返回一个shared_ptr对象该如何实现?

先看下下面的代码:

class A{
public:
A(){
count<<"A construct"<<endl;
}
~A(){
cout << "A dedeconstruct"<<endl;
}
shared_ptr<A>  getSharedPtr()
{
    return shared_ptr<A>(this);
}

}
int main()
{
    shared_ptr<A> ptr1(new A);
    shared_ptr<A> ptr = ptr1->getSharedPtr();
    cout<<"ptr.use_cont = "<<ptr.use_count()<<endl;
    cout<<"ptr1.use_cont = "<<ptr1.use_count()<<endl;
    return 0;
}

输出结果:

上面的代码有什么问题呢?如果仔细的分析话就会发现这里会触发A对象的实例被释放两次。这个问题的发生还是跟本文前面讲的错误一样的原因,一个被管理者对象有虽然有两个shared_ptr对象引用,但是引用计数对象只记录了一次。(这里有个疑问,为什么第一次deconstruct在打印之后,而不是在第一次打印之前?

要实现类对象通过函数接口的方式需要通过enable_shared_from_this和shared_from_this来实现,实现方式时继承enable_shared_from_this,通过调用基类中的shared_from_this方法返回对象,代码如下:

class A:public enable_shared_from_this{
public:
A(){
count<<"A construct"<<endl;
}
~A(){
cout << "A dedeconstruct"<<endl;
}
shared_ptr<A>  getSharedPtr()
{
/*通过调用基类的shared_from_this方法得到一个指向当前对象的智能指针*/
    return shared_from_this();
}

}
int main()
{
    shared_ptr<A> ptr1(new A);
    shared_ptr<A> ptr = ptr1->getSharedPtr();
    cout<<"ptr.use_cont = "<<ptr.use_count()<<endl;
    cout<<"ptr1.use_cont = "<<ptr1.use_count()<<endl;
    return 0;
}

为什么会这样呢?请看下面的分析,先看看enable_shared_from_this基类的成员变量有什么。

template<class _Ty>
    class enable_shared_from_this
    {    // provide member functions that create shared_ptr to this
public:
    using _Esft_type = enable_shared_from_this;

    _NODISCARD shared_ptr<_Ty> shared_from_this()
        {    // return shared_ptr
        return (shared_ptr<_Ty>(_Wptr));
        }
    // 成员变量是一个指向资源的弱智能指针
    mutable weak_ptr<_Ty> _Wptr;
};

也就是说,如果一个类继承了enable_shared_from_this,那么它产生的对象就会从基类enable_shared_from_this继承一个成员变量_Wptr,当定义第一个智能指针对象的时候shared_ptr<A> ptr1(new A()),调用shared_ptr的普通构造函数,就会初始化A对象的成员变量_Wptr,作为观察A对象资源的一个弱智能指针观察者(在shared_ptr的构造函数中实现,有兴趣可以自己调试跟踪源码实现)。

然后代码如下调用shared_ptr<A> ptr2 = ptr1->getSharedPtr(),getSharedPtr函数内部调用shared_from_this()函数返回指向该对象的智能指针,这个函数怎么实现的呢,看源码:

shared_ptr<_Ty> shared_from_this()
{    // return shared_ptr
return (shared_ptr<_Ty>(_Wptr));
}

shared_ptr<_Ty>(_Wptr),说明通过当前A对象的成员变量_Wptr构造一个shared_ptr出来,看看shared_ptr相应的构造函数:

shared_ptr(const weak_ptr<_Ty2>& _Other)
{    // construct shared_ptr object that owns resource *_Other
if (!this->_Construct_from_weak(_Other)) // 从弱智能指针提升一个强智能指针
    {
    _THROW(bad_weak_ptr{});
    }
}

接着看上面调用的_Construct_from_weak方法的实现如下:

template<class _Ty2>
bool _Construct_from_weak(const weak_ptr<_Ty2>& _Other)
{    // implement shared_ptr's ctor from weak_ptr, and weak_ptr::lock()
// if通过判断资源的引用计数是否还在,判定对象的存活状态,对象存活,提升成功;
// 对象析构,提升失败!之前的博客内容讲过这些知识,可以去参考!
if (_Other._Rep && _Other._Rep->_Incref_nz())
    {
    _Ptr = _Other._Ptr;
    _Rep = _Other._Rep;
    return (true);
    }

return (false);
}

综上所说,所有过程都没有再使用shared_ptr的普通构造函数,没有在产生额外的引用计数对象,不会存在把一个内存资源,进行多次计数的过程;更关键的是,通过weak_ptr到shared_ptr的提升,还可以在多线程环境中判断对象是否存活或者已经析构释放,在多线程环境中是很安全的,通过this裸指针进行构造shared_ptr,不仅仅资源会多次释放,而且在多线程环境中也不确定this指向的对象是否还存活。

智能指针与多线程的关系?

一般而言谈论多线程的时候谈到的是shared_ptr,因为其他智能指针要么是弱引用指针,要么是独占型指针,不具备多线程的环境。

那么shared_ptr到底是否具有线程安全性呢?虽然shared_ptr中的引用计数对象的中的计数类型为原子类型,但是在多线程环境中使用shared_ptr也是需要互斥锁来保证安全性的。简单来说shared_ptr作为一个类对象,是有两个数据成员的,都是指针,其中一个指针指向被管理者对象,另外一个指针指向引用计数对象,一个赋值语句例如:

shared_ptr<int> ptr1(new int(10));
shared_ptr<int> ptr2(new int(20));
ptr1 = ptr2; //这个赋值语句会发生什么参考第7小节的shared_ptr基本实现

从上面的代码行可以喊道虽然ptr1=ptr2;仅有一行,但是在逻辑功能上有两项,其实拷贝被管理者对象,其二是拷贝引用计数对象。而这两行若是换成汇编指令则是会有更多项,线程运行的时候可能在这其中任何一条汇编指令切换线程。这也是多线程问题本质,若代码设计不好,则会造成错误。

至于为什么要是使用锁来保证安全性,请参考https://blog.csdn.net/solstice/article/details/8547547

智能指针在继承场景下的使用

在继承场景下只能智能也能正常使用,首先考虑单继承的情况,如下代码:

class base{
...
}
class derived:pubic base{
...
}
shared_ptr<derived> ptr1(new derived);
shared_ptr<base> ptr2 = ptr1;  //正确
ptr1.reset();

上面的代码第8行可以直接赋值,执行完毕后ptr2依然管理derived,并正常释放。

多继承查看如下引用

2. shared_ptr<void> 可以指向并安全地管理(析构或防止析构)任何对象;muduo::net::Channel class 的 tie() 函数就使用了这一特性,防止对象过早析构,见书 7.15.3 节。
shared_ptr<Foo> sp1(new Foo); // ref_count.ptr 的类型是 Foo*
shared_ptr<void> sp2 = sp1; // 可以赋值,Foo* 向 void* 自动转型
sp1.reset(); // 这时 Foo 对象的引用计数降为 1
此后 sp2 仍然能安全地管理 Foo 对象的生命期,并安全完整地释放 Foo,不会出现 delete void* 的情况,因为 delete 的是 ref_count.ptr,不是 sp2.ptr。
3. 多继承。假设 Bar 是 Foo 的多个基类之一,那么:
shared_ptr<Foo> sp1(new Foo);
shared_ptr<Bar> sp2 = sp1; // 这时 sp1.ptr 和 sp2.ptr 可能指向不同的地址,因为 Bar subobject 在 Foo object 中的 offset 可能不为0。
sp1.reset(); // 此时 Foo 对象的引用计数降为 1
但是 sp2 仍然能安全地管理 Foo 对象的生命期,并安全完整地释放 Foo,因为 delete 的不是 Bar*,而是原来的 Foo*。换句话说,sp2.ptr 和 ref_count.ptr 可能具有不同的值(当然它们的类型也不同)。
  1. 参考链接

https://www.cnblogs.com/S1mpleBug/p/16770731.html

https://www.cnblogs.com/cnhk19/p/15628579.html

https://blog.csdn.net/coolsuperman/article/details/98472948

https://blog.csdn.net/YYY_77/article/details/123299662

shared_ptr简单实现,通过改代码可以辅助理解具体实现:

#include<iostream>
#include<mutex>
#include<thread>
using namespace std;

template<class T>  //模板类
class Shared_Ptr{
public:
    //以普通指针进行构造
    Shared_Ptr(T* ptr = nullptr)
        :_pPtr(ptr)
        , _pRefCount(new int(1))
        , _pMutex(new mutex)
    {}
    
    //析构函数
    ~Shared_Ptr()
    {
        Release();
    }
    
    //拷贝构造函数
    Shared_Ptr(const Shared_Ptr<T>& sp)
        :_pPtr(sp._pPtr)
        , _pRefCount(sp._pRefCount)
        , _pMutex(sp._pMutex)
    {
        AddRefCount();
    }
    
    //重载赋值号,使得同一类型的shared_ptr智能指针可以相互赋值
    Shared_Ptr<T>& operator=(const Shared_Ptr<T>& sp)
    {
        if (_pPtr != sp._pPtr)
        {
            // 释放管理的旧资源
            Release();
            // 共享管理新对象的资源,并增加引用计数
            _pPtr = sp._pPtr;
            _pRefCount = sp._pRefCount;
            _pMutex = sp._pMutex;
            AddRefCount();
        }
        return *this;
    }
    
    //    重载 * 号,获取当前 shared_ptr 智能指针对象指向的数据
    T& operator*(){
        return *_pPtr;
    }
    
    //重载 -> 号,当智能指针指向的数据类型为自定义的结构体时,通过 -> 运算符可以获取其内部的指定成员
    T* operator->(){
        return _pPtr;
    }
    
    //返回同当前 shared_ptr 对象(包括它)指向相同的所有 shared_ptr 对象的数量
    int UseCount() { return *_pRefCount; }
    
    //返回 shared_ptr 对象内部包含的普通指针
    T* Get() { return _pPtr; }
    
    void AddRefCount()
    {
        _pMutex->lock();
        ++(*_pRefCount);
        _pMutex->unlock();
    }
private:
    void Release()
    {
        bool deleteflag = false;
        _pMutex->lock();
        if (--(*_pRefCount) == 0)
        {
            delete _pRefCount;
            delete _pPtr;
            deleteflag = true;
        }
        _pMutex->unlock();
        if (deleteflag == true)
            delete _pMutex;
    }
private:
    int *_pRefCount;  //定义一个引用计数指针
    T* _pPtr;   //定义一个存储指针
    mutex* _pMutex;  //定义一个锁指针,为了保证线程安全,防止资源未释放或程序崩溃
};
  1. 结语

  1. 使用智能指针不能算是绝对的安全,使用智能指针要在正确的使用方法基础之上才可以确保安全。

  1. 继承场景下的使用shared_ptr智能指针

  1. 智能指针的含义及内存构造

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
智能指针C++中起到了管理动态分配的对象内存的作用,它们通过封装原始指针并提供自动释放内存的机制来避免内存泄漏。智能指针可以跟踪对象的引用计数,并在没有引用时自动销毁对象。它们还可以提供异常安全,即在发生异常时能够正确地释放资源。在C++中,智能指针的分类包括unique_ptr、shared_ptr和weak_ptr。其中,unique_ptr表示独占所有权的指针,只能有一个指针可以指向对象;shared_ptr表示共享所有权的指针,可以有多个指针指向同一个对象,并且会自动释放对象内存;weak_ptr是对shared_ptr的一种扩展,它允许访问对象但不会增加引用计数,可以用于解决shared_ptr的循环引用问题。在C++中,使用智能指针能够提高代码的安全性和可维护性,减少内存泄漏的风险。例如,可以使用unique_ptr来管理动态分配的资源,确保在离开作用域时正确释放资源,避免忘记释放内存导致的内存泄漏。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [【C++智能指针详解](https://blog.csdn.net/qq_53268869/article/details/124551345)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 50%"] - *2* *3* [c++智能指针详解](https://blog.csdn.net/bitcarmanlee/article/details/124847634)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值