C++——智能指针

目录

智能指针出现的原因

避免内存泄漏

智能指针的使用及原理

RAII

智能指针的原理

auto_ptr

unique_ptr

shared_ptr

weak_ptr

定制删除器


智能指针出现的原因

        在上一篇异常的介绍中就提到了,如果申请了一块空间,因为抛异常导致资源没有释放,这就会导致内存泄漏。

        内存泄漏就是因为疏忽或错误造成程序没有释放不再使用的内存。这并不是说内存真的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。

        上一段简单的说明就是,内存泄漏就是指针丢失了,但是空间是丢失不了的,造成空间浪费。

        对于短期运行的程序出现内存泄漏,没有什么影响,进程结束,内存就释放了;对于长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。

避免内存泄漏

  1. 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。
  2. 采用RAII思想或者智能指针来管理资源。这就是接下来要说的。
  3. 出问题了使用内存泄漏工具检测,检测工具的原理就是申请空间就用一个容器记录下来,释空间的时候就从容器中删除,这样就可以检查哪些是内存泄漏的资源。

智能指针的使用及原理

RAII

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

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

template<class T>
class SmartPtr
{
	SmartPtr(T* ptr = nullptr) // 使用指针构造一个对象
		:_ptr(ptr)
	{}
	~SmartPtr()
	{
		if (_ptr)
			delete _ptr; // 在对象生命周期结束后自动调用析构函数释放资源
	}
private:
	T* _ptr;
};

智能指针的原理

        上面的代码还不能算是智能指针,因为它还不具有指针的行为。指针可以解引用,也可以通过 -> 去访问所指空间中的内容,因此类中还得需要重载 * 和 -> ,这样才可以像指针一样去使用。

template<class T>
class SmartPtr
{
public:
	SmartPtr(T* ptr = nullptr) // 使用指针构造一个对象
		:_ptr(ptr)
	{}
	~SmartPtr()
	{
		if (_ptr)
			delete _ptr; // 在对象生命周期结束后自动调用析构函数释放资源
	}
	T& operator&()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}
private:
	T* _ptr;
};

auto_ptr

C++98版本的库中就提供了auto_ptr的智能指针。

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

        假如这里有一个A类,把它托管给auto_ptr管理,当我要赋值的时候,就像迭代器一样,我不需要让他深拷贝,他是一个指针,负责指向就好了,如果是浅拷贝,赋值之后两个对象指向同一块空间,析构就会析构两次,为了解决这个问题,auto_ptr会把原来对象的权限转移给新的对象,但是要是使用原来的对象就会出错,所以它使用的是管理权转移,说白了也就是把旧的指针赋值过去,再置为空,但是这种做法很不负责,平常是不用的。

unique_ptr

因为auto_ptr不是那么好用,C++11中就有了unique_ptr。

unique_ptr的实现原理:简单粗暴的防拷贝。下面就是简单实现一下。

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

	// 防止拷贝 C++11的方法
	unique_ptr(unique_ptr<T>& up) = delete;
	unique_ptr<T>& operator=(unique_ptr<T>& up) = delete;

	// C++98使用的是私有拷贝构造、赋值,只声明不实现

	~unique_ptr()
	{
		if (_ptr)
			delete _ptr;
	}
	T& operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}
private:
	T* _ptr;
};

但是这样简单的防止拷贝也不是办法,它只适用于不需要拷贝的。

shared_ptr

如果就是想要拷贝呢?C++11中开始提供更靠谱的并且支持拷贝的shared_ptr。

shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。有几个资源指向这块空间就是几,释放的时候,如果不是最后一个,引用计数就 - - ,如果是最后一个再释放

template<class T>
class shared_ptr
{
public:
	shared_ptr(T* ptr = nullptr)
		:_ptr(ptr)
		, _pCount(new int(1)) // 初始化为1
	{}

	~shared_ptr()
	{
		if (--(*_pCount) == 0)
		{
			delete _ptr;
			delete _pCount;
		}
	}

	// 拷贝构造
	shared_ptr(const shared_ptr<T>& sp)
		:_ptr(sp._ptr)
		, _pCount(sp._pCount)
	{
		(*_pCount)++;
	}

	// 赋值
	shared_ptr& operator=(const shared_ptr<T>& sp)
	{
		if (sp._ptr != _ptr) // 不需要自己给自己赋值,也不需要给指向同一块的赋值
		{
			if (--(*_pCount) == 0) // 原来的是不是最后一个,是的话就释放
			{
				delete _ptr;
				delete _pCount;
			}

			_ptr = sp._ptr;
			_pCount = sp._pCount;
			(*_pCount)++;
		}
		return *this;
	}
    T* get()
    {
        return _ptr;
    }
	T& operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}
private:
	T* _ptr;
	int* _pCount; // 引用计数
};

        shared_ptr看似很完美,但是它还有一个问题,那就是循环引用,接下来我们就来看看什么是循环引用。

struct Node
{
	int _data;
	std::shared_ptr<Node> _prev;
	std::shared_ptr<Node> _next;

	~Node()
	{
		cout << "~Node()" << endl;
	}
};

int main()
{
	// 这里不能使用=赋值,因为shared_ptr构造使用的explicit,不可以有隐式类型的转换
	std::shared_ptr<Node> n1(new Node);
	std::shared_ptr<Node> n2(new Node);
	// 如果只把next修改,最后会析构两次,如果把next和prev都修改,变成循环链表最后就没有析构
	n1->_next = n2;
	// n2->_prev = n1;
	return 0;
}

这就是循环引用,谁也释放不了,这样就出现了weak_ptr。

weak_ptr

        weak_ptr不是常规的智能指针,它没有RAII,不支持直接管理资源,weak_ptr主要用shared_ptr构造,用来解决shared_ptr循环引用问题。

        next和prev是weak_ptr时,他不参与资源释放管理,也不增加引用计数,但是可以访问和修改资源,所以就不存在循环引用。

struct Node
{
	int _data;
	std::weak_ptr<Node> _prev;
	std::weak_ptr<Node> _next;

	~Node()
	{
		cout << "~Node()" << endl;
	}
};

下面就简单实现一下weak_ptr。

// 为了解决shared_ptr循环引用问题
template<class T>
class weak_ptr
{
public:
	weak_ptr()
		:_ptr(nullptr)
	{}
	weak_ptr(const shared_ptr<T>& sp)
		:_ptr(sp.get())
	{}
	weak_ptr(const weak_ptr<T>& wp)
		:_ptr(wp.get())
	{}
	weak_ptr<T>& operator=(const shared_ptr<T>& sp)
	{
		_ptr = sp.get();
		return *this;
	}

	T* get()
	{
		return _ptr;
	}

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

shared_ptr线程安全

        如果是多线程的情况下,shared_ptr是线程安全的吗,如果不处理那一定不是线程安全的。

        第一,在shared_ptr拷贝的时候,引用计数是要改变的,不管是++还是--,如果不加锁是一定会出问题的。

#include <iostream>
#include <vector>
#include <thread>
#include <mutex>

using namespace std;

namespace dsh
{
    template<class T>
    class shared_ptr
    {
    public:
        shared_ptr(T* ptr = nullptr)
            :_ptr(ptr)
            , _pCount(new int(1)) // 初始化为1
            , _pMutex(new mutex)  // 初始化锁
        {}

        ~shared_ptr()
        {
            Release();
        }

        void AddPCount()
        {
            _pMutex->lock();
            (*_pCount)++;
            _pMutex->unlock();
        }

        void Release()
        {
            bool flag = false; // 标识是不是最后一个,是的话也要释放锁
            _pMutex->lock();
            if (--(*_pCount) == 0) // 原来的是不是最后一个,是的话就释放
            {
                delete _ptr;
                delete _pCount;

                flag = true;
            }
            _pMutex->unlock();
            if (false)
                delete _pMutex;
        }

        // 拷贝构造
        shared_ptr(const shared_ptr<T>& sp)
            :_ptr(sp._ptr)
            , _pCount(sp._pCount)
            , _pMutex(sp._pMutex)
        {
            AddPCount();
        }

        // 赋值
        shared_ptr& operator=(const shared_ptr<T>& sp)
        {
            if (sp._ptr != _ptr) // 不需要自己给自己赋值,也不需要给指向同一块的赋值
            {
                Release();

                _ptr = sp._ptr;
                _pCount = sp._pCount;
                _pMutex = sp._pMutex;
                AddPCount();
            }
            return *this;
        }
        int use_count()
        {
            return *_pCount;
        }
        T* get()
        {
            return _ptr;
        }
        T& operator*()
        {
            return *_ptr;
        }
        T* operator->()
        {
            return _ptr;
        }
    private:
        T* _ptr;
        int* _pCount; // 引用计数
        mutex* _pMutex; // 锁
    };
}

int main()
{
    dsh::shared_ptr<int> sp1(new int(1));
    dsh::shared_ptr<int> sp2(sp1);

    vector<thread> v(2);
    int n = 1000000;
    mutex mtx;

    for (auto& t : v)
    {
        t = thread([&]() {
            for (size_t i = 0; i < n; i++)
            {
                dsh::shared_ptr<int> sp(sp1);
            }
        });
    }
    v[0].join();
    v[1].join();
    cout << sp1.use_count() << endl;
    cout << sp1.get() << endl;

    return 0;
}

        第二,那shared_ptr维护的指针是线程安全的吗,那肯定也不是,所以想要在多线程下访问也是要加锁的。

int main()
{
    dsh::shared_ptr<int> sp1(new int(1));
    dsh::shared_ptr<int> sp2(sp1);

    vector<thread> v(2);
    int n = 1000000;

    mutex mtx;

    for (auto& t : v)
    {
        t = thread([&]() {
            for (size_t i = 0; i < n; i++)
            {
                // 拷贝是线程安全的
                dsh::shared_ptr<int> sp(sp1);

                mtx.lock();
                (*sp)++;
                mtx.unlock();
            }
        });
    }
    v[0].join();
    v[1].join();
    cout << sp1.use_count() << endl;
    cout << sp1.get() << endl;
    cout << *sp1 << endl;

    return 0;
}

所以换言之,STL都不是线程安全的,使用的时候也是要按需求加锁的。

定制删除器

        如果一个对象是通过new[]出来的,那么用delete就会出问题,一定要使用delete[]。对于自定义类型,new[]实际是malloc和n次构造函数,它会在开空间的时候会在头的位置多开一个指针,使用这个指针存放调用了几次构造函数,等到delete[]的时候,先向前偏移到该位置,再释放资源,如果继续使用delete,指针不会偏移,释放的位置就会出错。这在vs下是这样实现的,所以使用new和delete的时候一定要匹配。

        智能指针中提供了一个定值删除器,unique_ptr和shared_ptr也使用了default_ptr。

当然,它也可以是一个仿函数,使用的时候定义匿名对象就可以了。

template <class T>
struct DeleteArr
{
	void operator()(T* ptr)
	{
		delete[] ptr;
	}
};

template <class T>
struct FreeArr
{
	void operator()(T* ptr)
	{
		free(ptr);
	}
};

int main()
{
	std::shared_ptr<Node> n1(new Node[5], DeleteArr<Node>());
	std::shared_ptr<int> n2(new int[5], DeleteArr<int>());
	std::shared_ptr<int> n3((int*)malloc(sizeof(4)), FreeArr<int>());

	return 0;
}

既然可以使用仿函数,那么自然可以使用lambda表达式。

int main()
{
	std::shared_ptr<Node> n1(new Node[5], [](Node* ptr) { delete[] ptr; });
	std::shared_ptr<int> n2(new int[5], [](int* ptr) { delete[] ptr; });
	std::shared_ptr<int> n3((int*)malloc(sizeof(4)), [](int* ptr) { free(ptr); });

	return 0;
}

        库中的模板参数还有一个类型,这就是删除器的类型,在创建对象的时候就可以指定是哪个删除器, 可以写一个默认的,也可以自己实现一个。这里就是简单说一下,库里的实现方式还是很麻烦的。

	template <class T> // 默认的定值删除器
	struct Delete
	{
		void operator()(T* ptr)
		{
			delete ptr;
		}
	};

	template<class T, class D = Delete<T>>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr = nullptr)
			:_ptr(ptr)
			, _pCount(new int(1)) // 初始化为1
		{}

		void Release()
		{
			if (--(*_pCount) == 0)
			{
				// delete _ptr;
				D del;
				del(_ptr);
				delete _pCount;
			}
		}

		~shared_ptr()
		{
			Release();
		}

		// 拷贝构造
		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			, _pCount(sp._pCount)
		{
			(*_pCount)++;
		}

		// 赋值
		shared_ptr& operator=(const shared_ptr<T>& sp)
		{
			if (sp._ptr != _ptr) // 不需要自己给自己赋值,也不需要给指向同一块的赋值
			{
				Release();

				_ptr = sp._ptr;
				_pCount = sp._pCount;
				(*_pCount)++;
			}
			return *this;
		}

        //...
	}

        修改了一下代码,可以使用默认的删除器也可以自己写一个删除器,假如DeleteArr就是自己写的,因为它释放数组,默认的就让他直接释放指针。

int main()
{
	shared_ptr<Node, DeleteArr<Node>> n1(new Node[5]);
	shared_ptr<int, DeleteArr<int>> n2(new int[5]);

	return 0;
}
  • 21
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

微yu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值