C++ 智能指针

1、什么是智能指针

2、智能指针的实现

3、几种常见的智能指针

(1)auto_ptr

(2)unique_ptr

 (3)shared_ptr

(4)weak_ptr


1、什么是智能指针

    几乎每一个有分量的程序都需要“在相同时间的多处地点处理或使用对象”的能力。为此,我们必须在程序的多个地点指向(refer to)同一对象。虽然C++语言提供引用(reference)和指针(pointer),还是不够,因为我们往往必须确保当“指向对象”的最末一个引用被删除时该对象本身也被删除,毕竟对象被删除时析构函数可以要求某些操作,例如释放内存或归还资源等等。

    在C++中,为了确保指针的寿命和其所指向的对象的寿命一致是件棘手的事,特别是当多个指针指向同一对象时。例如,为了让多个集合拥有同一对象,你必须把指向该对象的指针放进那些集合内,而且当其中一个指针被销毁时不该出现问题,也就是不该出现所谓的空悬指针(也叫野指针)或多次删除被指向对象,最后一个指针被销毁时也不该出现资源泄露问题。为了避免上述问题的一个通用做法是使用智能指针,智能指针能够知道它自己是不是指向某物的最后一个指针,并且运用这样的知识,在它的确是该对象的最后一个拥有者而且它被删除时,销毁它所指向的对象。shared_ptr的目标就是,在其所指向的对象不再被使用之后(而非之前),自动释放与对象相关的资源。  

2、智能指针的实现(参考

    智能指针(smart pointer)是存储指向动态分配(堆)对象指针的类(智能指针自身保存在栈中),用于生存期控制,能够确保自动正确的销毁动态分配的对象,防止内存泄露。它的一种通用实现技术是使用引用计数(reference count)。智能指针类将一个计数器与类指向的对象相关联,引用计数跟踪该类有多少个对象共享同一指针。每次创建类的新对象时,初始化指针并将引用计数置为1;当对象作为另一对象的副本而创建时,拷贝构造函数拷贝指针并增加与之相应的引用计数;对一个对象进行赋值时,赋值操作符减少左操作数所指对象的引用计数(如果引用计数为减至0,则删除对象),并增加右操作数所指对象的引用计数;调用析构函数时,构造函数减少引用计数(如果引用计数减至0,则删除基础对象)。智能指针就是模拟指针动作的类。所有的智能指针都会重载 -> 和 * 操作符。智能指针还有许多其他功能,比较有用的是自动销毁。这主要是利用栈对象的有限作用域以及临时对象(有限作用域实现)析构函数释放内存。智能指针类的具体实现参考如下:

#include <iostream>
using namespace std;

template<class T>
class SmartPtr
{
public:
    SmartPtr(T *p);                                    // 构造函数
    ~SmartPtr();                                       // 析构函数
    SmartPtr(const SmartPtr<T> &orig);                 // 复制构造函数
    SmartPtr<T>& operator=(const SmartPtr<T> &rhs);    // 赋值运算操作符
    T& operator*();                                    // 解引用
    T* operator->();                                   // 取成员操作符

private:
    T *ptr;    
    int *use_count;            // 将use_count声明成指针是为了方便对其的递增或递减操作
};

template<class T>
SmartPtr<T>::SmartPtr(T *p) : ptr(p)                  //构造函数定义
{
    try
    {
        use_count = new int(1);
    }
    catch (...)
    {
        delete ptr;
        ptr = nullptr;
        use_count = nullptr;
        cout << "Allocate memory for use_count fails." << endl;
        exit(1);
    }
    cout << "Constructor is called!" << endl;
}

template<class T>
SmartPtr<T>::~SmartPtr()                             //析构函数定义
{ 
    // 只在最后一个对象引用ptr时才释放内存
    if (--(*use_count) == 0)
    {
        delete ptr;
        delete use_count;
        ptr = nullptr;
        use_count = nullptr;
        cout << "Destructor is called!" << endl;
    }
}

template<class T>
SmartPtr<T>::SmartPtr(const SmartPtr<T> &orig)       //复制构造函数定义
{
    ptr = orig.ptr;
    use_count = orig.use_count;
    ++(*use_count);
    cout << "Copy constructor is called!" << endl;
}

// 重载等号函数不同于复制构造函数,即等号左边的对象可能已经指向某块内存。
// 这样,我们就得先判断左边对象指向的内存已经被引用的次数。如果次数为1,
// 表明我们可以释放这块内存;反之则不释放,由其他对象来释放。
template<class T>
SmartPtr<T>& SmartPtr<T>::operator=(const SmartPtr<T> &rhs)   //赋值运算操作符定义
{
    // 《C++ primer》:“这个赋值操作符在减少左操作数的使用计数之前使rhs的使用计数加1,
    // 从而防止自身赋值”而导致的提早释放内存
    ++(*rhs.use_count);

    // 将左操作数对象的使用计数减1,若该对象的使用计数减至0,则删除该对象
    if (--(*use_count) == 0)
    {
        delete ptr;
        delete use_count;
        cout << "Left side object is deleted!" << endl;
    }

    ptr = rhs.ptr;
    use_count = rhs.use_count;
    
    cout << "Assignment operator overloaded is called!" << endl;
    return *this;
}

template<class T>
T& SmartPtr<T>::operator*()
{
    return *(this->ptr);
}

template<class T>
T* SmartPtr<T>::operator->()
{
    return this->ptr;
}

int main()
{
    SmartPtr<int> ptr(new int(10));
    std::cout << *ptr << std::endl;
    return 0;
}

3、几种常见的智能指针

(1)auto_ptr

    auto_ptr是C++98提供的,它不支持复制(拷贝构造函数)和赋值(operator =),此类模板已被弃用,unique_ptr是具有相似功能但具有改进的安全性的新工具。auto_ptr是一个智能指针,用于管理通过新表达式获取的对象,并在auto_ptr本身被销毁时删除该对象。当使用auto_ptr类描述一个对象时,它存储一个指向单个分配对象的指针,该对象可以确保当它超出范围时,它指向的对象必须被自动销毁。 它基于独占所有权模式,即同一类型的两个指针不能同时指向相同的资源。auto_ptr的拷贝构造函数和赋值运算符实际上并没有复制存储的指针,而是将它们传输出去,使第一个auto_ptr对象变为空。 这是实现严格所有权的一种方法,因此只有一个auto_ptr对象可以在任何给定时间拥有该指针,即在需要复制语义的情况下不应使用auto_ptr。

(2)unique_ptr

    std :: unique_ptr是在C ++ 11中开发的,用于替代std :: auto_ptr。unique_ptr是具有类似功能的新工具,但具有改进的安全性(无假拷贝分配),添加功能(删除器)和数组支持。 它是一个原始指针的容器。 它明确地防止复制其包含的指针,正如正常赋值那样会发生,即它只允许底层指针的一个所有者。所以,当使用unique_ptr时,在任何一个资源上最多只能有一个unique_ptr,当该unique_ptr被破坏时,该资源将被自动声明。 另外,由于任何资源只能有一个unique_ptr,所以任何创建unique_ptr副本的尝试将导致编译时错误。

 unique_ptr<A> ptr1 (new A);
 unique_ptr<A> ptr2 = ptr1;    // Error: can't copy unique_ptr

    但是,unique_ptr可以使用新的语义,即使用std :: move()函数将包含的指针的所有权转移到另一个unique_ptr。

unique_ptr<A> ptr2 = move(ptr1);  //此时ptr1为NULL,ptr2指向该对象

 (3)shared_ptr

    shared_ptr是原始指针的容器。 它是一个引用计数模型,即它与shared_ptr的所有副本合作维护其包含的指针的引用计数。 因此,每当一个新的指针指向资源时,计数器就会递增,当调用对象的析构函数时递减计数器。引用计数:这是一种将资源数量,指针或句柄存入资源(如对象,内存块,磁盘空间或其他资源)的技术。引用计数大于0,直到所有的shared_ptr副本都被删除,所包含的原始指针引用的对象将不会被销毁。因此,当我们要为一个原始指针分配给多个所有者时,我们应该使用shared_ptr。

#include<iostream>
#include<memory>
using namespace std;
 
class A
{
public:
    void show()
    {
        cout<<"A::show()"<<endl;
    }
};
 
int main()
{
    shared_ptr<A> p1 (new A);
    cout << p1.get() << endl;
    p1->show();
    shared_ptr<A> p2 (p1);
    p2->show();
    cout << p1.get() << endl;
    cout << p2.get() << endl; 

    cout << p1.use_count() << endl;
    cout << p2.use_count() << endl; 

    p1.reset();
    cout << p1.get() << endl;
    cout << p2.use_count() << endl;
    cout << p2.get() << endl;
 
    return 0;
}
输出:
0x1c41c20
A::show()
A::show()
0x1c41c20
0x1c41c20
2
2
0          // NULL
1
0x1c41c20

 关于shared_ptr的线程安全级别:

    (shared_ptr)的引用计数本身是安全且无锁的,但对象的读写则不是,因为 shared_ptr 有两个数据成员,读写操作不能原子化。根据文档(http://www.boost.org/doc/libs/release/libs/smart_ptr/shared_ptr.htm#ThreadSafety), shared_ptr 的线程安全级别和内建类型、标准库容器、std::string 一样,即:

  • 一个 shared_ptr 对象实体可被多个线程同时读取(文档例1);
  • 两个 shared_ptr 对象实体可以被两个线程同时写入(例2),“析构”算写操作;
  • 如果要从多个线程读写同一个 shared_ptr 对象,那么需要加锁(例3~5)。

    请注意,以上是 shared_ptr 对象本身的线程安全级别,不是它管理的对象的线程安全级别。

 

(4)weak_ptr

    虽然使用shared_ptr可以在对象的引用个数为0时自动销毁对象,但是shared_ptr也存在一些缺点,比如循环引用问题。“循环引用”简单来说就是:两个对象互相使用一个shared_ptr成员变量指向对方的会造成循环引用。导致引用计数失效。下面给段代码来说明循环引用:

#include <iostream>
#include <memory>
using namespace std;
 
class B;
class A
{
public:// 为了省去一些步骤这里 数据成员也声明为public
    //weak_ptr<B> pb;
    shared_ptr<B> pb;
    void doSomthing()
    {
//        if(pb.lock())
//        {
//
//        }
    }
 
    ~A()
    {
        cout << "kill A\n";
    }
};
 
class B
{
public:
    //weak_ptr<A> pa;
    shared_ptr<A> pa;
    ~B()
    {
        cout <<"kill B\n";
    }
};
 
int main(int argc, char** argv)
{
    shared_ptr<A> sa(new A());
    shared_ptr<B> sb(new B());
    if(sa && sb)
    {
        sa->pb=sb;
        sb->pa=sa;
    }
    cout<<"sa use count:"<<sa.use_count()<<endl;
    return 0;
}

输出:sa use count:2

    注意此时sa,sb都没有释放,产生了内存泄露问题。即A内部有指向B,B内部有指向A,这样对于A,B必定是在A析构后B才析构,对于B,A必定是在B析构后才析构A,这就是循环引用问题,违反常规,导致内存泄露。解决循环引用问题的方法有很多,最常用的是使用weak_ptr。将weak_ptr创建为shared_ptr的副本。 它提供对一个或多个shared_ptr实例拥有的对象的访问权限,但不参与引用计数。 weak_ptr的存在或破坏对shared_ptr或其他副本没有影响。

    对于强引用而言,当被引用的对象活着的话,这个引用也存在(就是说,当至少有一个强引用,那么这个对象就不能被释放),share_ptr就是强引用。对于弱引用而言,当引用的对象活着的时候不一定存在。仅仅是当它存在的时候的一个引用。弱引用并不修改该对象的引用计数,这意味这弱引用它并不对对象的内存进行管理,在功能上类似于普通指针,然而一个比较大的区别是,弱引用能检测到所管理的对象是否已经被释放,从而避免访问非法内存。

   

 

代码如下:

#include <iostream>
#include <memory>
using namespace std;
 
class B;
class A
{
public:// 为了省去一些步骤这里 数据成员也声明为public
    weak_ptr<B> pb;
    //shared_ptr<B> pb;
    void doSomthing()
    {
        shared_ptr<B> pp = pb.lock();
        if(pp)//通过lock()方法来判断它所管理的资源是否被释放
        {
            cout<<"sb use count:"<<pp.use_count()<<endl;
        }
    }
 
    ~A()
    {
        cout << "kill A\n";
    }
};
 
class B
{
public:
    //weak_ptr<A> pa;
    shared_ptr<A> pa;
    ~B()
    {
        cout <<"kill B\n";
    }
};
 
int main(int argc, char** argv)
{
    shared_ptr<A> sa(new A());
    shared_ptr<B> sb(new B());
    if(sa && sb)
    {
        sa->pb=sb;
        sb->pa=sa;
    }
    sa->doSomthing();
    cout<<"sb use count:"<<sb.use_count()<<endl;
    return 0;
}

注意: weak_ptr除了对所管理对象的基本访问功能(通过get()函数)外,还有两个常用的功能函数:expired()用于检测所管理的对象是否已经释放;lock()用于获取所管理的对象的强引用指针。不能直接通过weak_ptr来访问资源。那么如何通过weak_ptr来间接访问资源呢?答案是:在需要访问资源的时候weak_ptr为你生成一个shared_ptr,shared_ptr能够保证在shared_ptr没有被释放之前,其所管理的资源是不会被释放的。创建shared_ptr的方法就是lock()方法。

参考:https://blog.csdn.net/jxw167/article/details/72864554

          https://blog.csdn.net/daniel_ustc/article/details/23096229

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值