C++(17)

新星杯·14天创作挑战营·第18期 10w+人浏览 215人参与

一.为什么需要智能指针

        下面我们先分析一下下面这段程序有没有什么内存方面的问题?

int div()
{
   int a, b;
   cin >> a >> b;
   if (b == 0)
   		throw invalid_argument("除0错误");
   return a / b;
}
void Func()
{
   // 1、如果p1这里new 抛异常会如何?无问题
   // 2、如果p2这里new 抛异常会如何?p1内存泄漏
   // 3、如果div调用这里又会抛异常会如何?p1,p2都内测泄漏
   int* p1 = new int;
   int* p2 = new int;
   cout << div() << endl;
   delete p1;
   delete p2;
}
int main()
{
   try
   {
       Func();
   }
   catch (exception& e)
   {
       cout << e.what() << endl;
   }
}

        之所以会发生这样的问题,因为异常它会打破原来程序线性执行的特性,导致有些代码没有实现(因此可能会产生内存泄漏)

二.智能指针的使用及原理

        2.1 RAII

        RAII是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。

        在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处

  • 不需要显式地释放资源。

  • 采用这种方式,对象所需的资源在其生命期内始终保持有效

// 使用RAII思想设计的SmartPtr类
template<class T>
class SmartPtr 
{
public:
    SmartPtr(T* ptr = nullptr)
    	: _ptr(ptr)
    {}
    ~SmartPtr()
    {
        if(_ptr)
    	    delete _ptr;
    }	

private:
	T* _ptr;
};
int div()
{
    int a, b;
    cin >> a >> b;
    if (b == 0)
        throw invalid_argument("除0错误");

    return a / b;
}
void Func()
{
    SmartPtr<int> sp1(new int);
    SmartPtr<int> sp2(new int);
    cout << div() << endl;
}
int main()
{
    try 
    {
    	Func();
    }
    catch(const exception& e)
    {
    	cout<<e.what()<<endl;
	}
}

        2.2 智能指针的原理
       

        上述的SmartPtr还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可以通过->去访问所指空间中的内容,因此:SmartPtr模板类中还得需要将* 、->重载下,才可让其像指针一样去使用

//在原来类的基础上补上这两代码
T& operator*() {return *_ptr;}
T* operator->() {return _ptr;}

        总结一下智能指针的原理:

  1. RAII特性

  2. 重载operator*和opertaor->,具有像指针一样的行为。

三.智能指针的种类

        3.1 auto_ptr

        auto_ptr的实现原理:管理权转移的思想

template<class T>
class auto_ptr
{
public:
    auto_ptr(T* ptr)
        :_ptr(ptr)
    {}

    auto_ptr(auto_ptr<T>& sp)
        :_ptr(sp._ptr)
    {
       // 管理权转移
       sp._ptr = nullptr;
    }

    auto_ptr<T>& operator=(auto_ptr<T>& ap)
    {
       // 检测是否为自己给自己赋值
       if (this != &ap)
       {
           //把当前指针进行析构
           delete _ptr;
           // 转移ap中资源到当前对象中,然后给ap置空
           _ptr = ap._ptr;
           ap._ptr = NULL;
        }
        return *this;     
    }

    ~auto_ptr()
    {
       if (_ptr)
       {
           cout << "delete:" << _ptr << endl;
           delete _ptr;
       }
    }
    // 像指针一样使用
    T& operator*()
    {
        return *_ptr;
    }

    T* operator->()
    {
        return _ptr;
    }
private:
	T* _ptr;
};

        总结一下auto_ptr,首先它面对拷贝和赋值采用的是置空法,就是我拿B赋值和构造A对象,那么势必两个元素会指向同一空间,然后auto_ptr的处理办法就是把B直接析构了(置空),这是一个很严重的问题,因此它很早就被淘汰了!

        3.2 unique_ptr

        unique_ptr的实现原理:简单粗暴的防拷贝

template<class T>
class unique_ptr
{
public:
    unique_ptr(T* ptr)
        : _ptr(ptr)
    {}

    ~unique_ptr()
    {
        
        delete _ptr;
    }

    // 像指针一样使用
    T& operator*()
    {
        return *_ptr;
    }

    T* operator->()
    {
        return _ptr;
    }

    unique_ptr(const unique_ptr<T>& sp) = delete;
    unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;

    // 移动语义
    unique_ptr(unique_ptr&& sp) 
        : _ptr(sp._ptr) 
    {
        sp._ptr = nullptr;
    }

    // 移动赋值
    unique_ptr& operator=(unique_ptr&& sp)
    {
        if (this != &sp)
        {
            delete _ptr;
            _ptr = sp._ptr;
            sp._ptr = nullptr;
        }
        return *this; 
    }

private:
    T* _ptr;
};

        总结一下unique_ptr,首先它面对拷贝和赋值采用的是禁止法,就是我不允许用B构造或赋值A,如果要改只允许右值引用(这是之前的内容)。

        3.3 shared_ptr

        shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源        

  1. shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享

  2. 对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。

  3. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源

  4. 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。

template<class T>
class shared_ptr
{
public:
	//一个指针只能调用一次,不然就会出现两个_ptr指向同一个地方
	// 但是_pcount却指向不同地方,从而出现问题
	shared_ptr(T* ptr)
		:_ptr(ptr)
		,_pcount(new int(1))
	{}
	
	~shared_ptr()
	{
		if (--(*_pcount) == 0)
		{
			delete _ptr;
			delete _pcount;
		}
	}
	
	T& operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}
	
	shared_ptr(const shared_ptr<T>& sp)
		:_ptr(sp._ptr)
		, _pcount(sp._pcount)
	{
		++(*_pcount);
	}
	
	shared_ptr<T>& operator=(const shared_ptr<T>& sp)
	{
        //这里为什么不用this == &sp,因为这只能处理A=A
        //但是不能处理A=B,可是B可能指向的内容和A相同的
		if (_ptr == sp._ptr)
		{
			return *this;
		}

		if (--(*_pcount) == 0)
		{
			delete _ptr;
			delete _pcount;
		}
		_ptr = sp._ptr;
		_pcount = sp._pcount;
		++(*_pcount);
		return *this;
	}
private:
	T* _ptr;
	int* _pcount;//引用计数

};

        关于这个代码需要了解的知识:

  1. 首先为什么引用计数要用指针,因为如果不用指针那么就会导致同一个指针对象却有不同的引用计数!
  2. 为什么每一个指针只能初始化一次,因为不然就会出现两个_ptr指向同一个地方但是_pcount却指向不同地方,从而出现问题。
  3. 这里为什么不用this == &sp,因为这只能处理A=A,但是不能处理A=B,可是B可能指向的内容和A相同的,就会导致冗余步骤。

        总结一下shared_ptr,就是我们使用最多的智能指针,因为它在面临拷贝和赋值交出来99分答卷,可是为什么不是100呢?请看下面分析!

         循环引用的希尔智能指针的弊端,解决方式是内部使用weak指针解决,weak指针没有RAII它是由希尔指针来构建的。
template<class T>
class weak_ptr 
{
public:
    weak_ptr() 
        : _ptr(nullptr)
        , _pcount(nullptr) 
    {}

    // 从 shared_ptr 构造:只“观察”,不增加引用计数
    weak_ptr(const shared_ptr<T>& sp)
        : _ptr(sp._ptr)
        , _pcount(sp._pcount)
    {}

    // 检查被管理的对象是否已被销毁
    bool expired() const 
    {
        return _pcount == nullptr || *_pcount == 0;
    }

private:
    T* _ptr;      // 观察的对象指针
    int* _pcount; // 共享的引用计数(来自 shared_ptr)
};

        weak指针解决的办法就是把prev弄成weak,那么就不会出现node1要析构要经过node2同意!

        还有一个问题,如果weak不支持*和->,那么如何查询该对象内容,就是给weak一个转成shared的方法。

         关于智能指针我们主要学习了3种类型,其中常用的只有2,3两种类型,其中3是最常用的也是最容易考的需要重点掌握,关于希尔指针还需要知道它的缺点是什么,它是这么处理的,原理是什么,最后其实智能指针这里还没有结束,智能指针还需要和锁结合,那这个就是后面的内容了,锁的内容我会在Linux章节讲到!
       
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值