讲解智能指针之前,我们先看一个样例

样例引入

来看看这段代码

double dev(double input1, double input2)
{
	int* arr = new int[10];
	if (input1 == 0 || input2 == 0)
	{
		string s = "不能除以零";
		throw s;
		
	}
	else
	{
	    delete[] arr;
	    arr = nullptr;
		return input1 / input2;
	}

}

int main()
{
	double x, y;
		try
		{
			cin >> x >> y;
			double ret = dev(x, y);
			cout << ret << endl;
		}
		catch (const string& ret)
		{
			cout << ret << endl;
		}
	return 0;
}
上⾯程序中我们可以看到,new了以后,我们也delete了,但是因为抛异常导,后⾯的delete没有得到 执⾏,所以就内存泄漏了

当我们这个代码的除数为0的时候,我们就会抛异常,后面的delete[] arr就不会执行,显然这样就造成了内存泄漏,没有正确释放资源

那么应该怎么做?我们可以试试在throw s之前的时候就把资源给释放了

如果有多个内存资源的申请,我们就要释放多次,这样也麻烦,但是new本身当内存申请失败的时候也会抛异常,这时候我们就还需要一层逻辑

这样就非常麻烦,基于这个场景c++设计了应该东西叫智能指针。

RAII和智能指针的设计思路

RAII是Resource Acquisition Is Initialization的缩写,他是⼀种管理资源的类的设计思想,本质是
⼀种利⽤对象⽣命周期来管理获取到的动态资源,避免资源泄漏,这⾥的资源可以是内存、⽂件指
针、⽹络连接、互斥锁等等。RAII在获取资源时把资源委托给⼀个对象,接着控制对资源的访问,
资源在对象的⽣命周期内始终保持有效,最后在对象析构的时候释放资源,这样保障了资源的正常
释放,避免资源泄漏问题。
智能指针类除了满⾜RAII的设计思路,还要⽅便资源的访问,所以智能指针类还会想迭代器类⼀
样,重载 operator*/operator->/operator[] 等运算符,⽅便访问资源
      当有了智能指针之后,上面的代码就可以简化,我们可以把申请的资源交给智能指针管理,这样就把内存的生命周期给绑定到了一个局部对象的生命周期上了,当new失败之后,throw触发抛异常时会触发一个叫栈展开的机制,会沿着调用链往上回溯,当遇到要从栈上销毁的用智能指针构造的局部对象时,要销毁这个对象时会去调用他本身的析构函数,从而正确的释放资源,如果是普通指针不做处理的情况下,只会销毁栈上的int* arr指针,而不会去管堆上的内存这就会导致内存泄漏,这体现了智能指针的一个最大的优势。

C++智能指针的原理

讲使用之前我们先来讲讲原理,首先我们看看智能指针的构造这里我们主要模拟实现一个简单的share_ptr的场景,首先我们要知道share_ptr有一个关键就是引用计数,这个是什么意思我们期望我们有多个不同的指针对象来管理同一块资源,我们就需要引用计数,那么引用计数该怎么实现呢,我们可以用一个int* pcount的指针来代表计数多个shared_ptr指向资源时就++引⽤计 数,shared_ptr对象析构时就--引⽤计数,引⽤计数减到0时代表当前析构的shared_ptr是最后⼀ 个管理资源的对象,则析构资源,为什么不能用static int pcount来呢,那是因为我们期望的是多个指针对象管理同一块资源而不是由这个share_ptr类创建的所有指针对象来管理同一块资源,所以要用int* pcount。因为static是这个类创建的所有对象都可以使用这个静态变量。

share_ptr代码实现

下面这个是没有删除器的版本

template<class T>
class share_ptr
{
public:
	explicit share_ptr(T* input_ptr = nullptr)
		:_ptr(input_ptr)
		,_count(new int(1))
	{}

	share_ptr(const share_ptr<T>& input_ptr)
		:_ptr(input_ptr._ptr)
		,_count(input_ptr._count)
	{
		(*_count)++;
	}
	~share_ptr()
	{
		release();
	}
	void release()
	{
		if (--(*_count) == 0)
		{
			delete _count;
			delete _ptr;
			_ptr = nullptr;
			_count = nullptr;
		}
	}
	//p2 =                                p1;
	share_ptr<T>& operator=(const share_ptr<T>& input_ptr)
	{
		if (input_ptr._ptr != _ptr)
		{
			release();
			_ptr = input_ptr._ptr;
			_count = input_ptr._count;
			(*_count)++;
		}
		return *this;
	}
	T& operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return this->_ptr;
	}
	T* get() const
	{
		return _ptr;
	}
	int use_count() const
	{
		return *_count;
	}
private:
	T* _ptr;
	int* _count;
};

这个代码我们就实现了一个简单的可以供给单线程使用的智能指针,只能满足最基本功能是share_ptr 来挑几个重点的代码讲解

为什么赋值重载不用this != input_ptr来判断

我们的本意是看这两个智能指针所管理的对象一不一样,而不是看这两个指针类是不是一样的

为什么在赋值重载的时候要去判断--count等不等于0
因为需要去看看原来的那个智能指针是不是最后一个指向那一个资源的指针,如果是需要释放资源,再赋值,如果不这么做,那么就会导致内存泄漏
下面我们来讲解智能指针的使用

智能指针的使用

C++标准库中的智能指针都在<memory>这个头⽂件下⾯,我们包含<memory>就可以是使⽤了,
智能指针有好⼏种,除了weak_ptr他们都符合RAII和像指针⼀样访问的⾏为,原理上⽽⾔主要是解
决智能指针拷⻉时的思路不同。

auto_ptr

下面来说说auto_ptr,auto_ptr是c++98中设计的,他的设计非常不好他的特点是拷⻉时把被拷⻉对象的资源的管理权转移给拷⻉对象,这是⼀个⾮常糟糕的设计,因为他会到被拷⻉对象悬空,访问报错的问题,像是一个半成品,我们平时要避免使用这一类的指针,或者不要用他
我们可以看看他的模拟实现
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)
			{
				// 释放当前对象中资源
				if (_ptr)
					delete _ptr;
				// 转移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;
};

unique_ptr

是C++11设计出来的智能指针,他的名字翻译出来是唯⼀指针,他的特点的不⽀持拷 ⻉,只⽀持移动。如果不需要拷⻉的场景就⾮常建议使⽤他。我们来看看他的使用

为什么unique_pte不支持拷贝只支持移动?

因为unique_ptr 的核心思想是:同一时间只能有一个 unique_ptr 指向并管理某块资源。如果允许拷贝,就会导致多个 unique_ptr 指向同一块内存,这直接违背了“唯一”的初衷.

在转移过程中,源 unique_ptr 会主动放弃所有权(内部指针被置为 nullptr),而目标标 unique_ptr 接管资源。

那么move是可以的因为,move是c++的移动语义,移动不是拷贝,我的目的很明确,要求转移资源而不是拷贝资源,这样不会违背唯一性的初衷。

unique_ptr的模拟实现

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

	{
	if (_ptr)
	{
	cout << "delete:" << _ptr << endl;
	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<T>&& sp)
		:_ptr(sp._ptr)
	{
		sp._ptr = nullptr;
	}
	unique_ptr<T>& operator=(unique_ptr<T>&& sp)
	{
		delete _ptr;
		_ptr = sp._ptr;
		sp._ptr = nullptr;
	}
private:
	T* _ptr;
};

share_ptr

share_ptr是C++11设计出来的智能指针,他的名字翻译出来是共享指针,他的特点是⽀持拷⻉,
也⽀持移动。如果需要拷⻉的场景就需要使⽤他了。底层是⽤引⽤计数的⽅式实现的。
我们来看看使用
shared_ptr 除了⽀持⽤指向资源的指针构造,还⽀持 make_shared ⽤初始化资源对象的值
直接构造。make_shared的效率比share_ptr更高效,make_shared构造这个指针对象的时候就把空间给一次性申请好了,shared_ptr还需要进行两次第一次是new申请的第二次是shared_ptr指针对象申请的。
  shared_ptr的构造函数内部,为存储引用计数的“控制块”再次分配内存。
shared_ptr unique_ptr 都⽀持了operator bool的类型转换,如果智能指针对象是⼀个
空对象没有管理资源,则返回false,否则返回true,意味着我们可以直接把智能指针对象给if判断
是否为空。
shared_ptr unique_ptr 都得构造函数都使⽤explicit 修饰,防⽌普通指针隐式类型转换
成智能指针对象。

定制删除器

智能指针析构时默认是进⾏delete释放资源,这也就意味着如果不是new出来的资源,交给智能指
针管理,析构时就会崩溃。智能指针⽀持在构造时给⼀个删除器,所谓删除器本质就是⼀个可调⽤
对象,这个可调⽤对象中实现你想要的释放资源的⽅式,当构造智能指针时,给了定制的删除器,
在智能指针析构时就会调⽤删除器去释放资源。因为new[]经常使⽤,所以为了简洁⼀点,
unique_ptr和shared_ptr都特化了⼀份[]的版本,使⽤时 unique_ptr<Date[]> up1(new
Date[5]);shared_ptr<Date[]> sp1(new Date[5]); 就可以管理new []的资源。就像这样本质这里是一个模板特化这个特化版本析构时⽤的delete[]
先看看share_ptr
同样我们可以传删除器可调用对象,这里传递了一个lambda
还可以传递函数指针
还可以传递仿函数
说完了shared_ptr,我们来说说unique_ptr
unique_ptr相比于shared_ptr传递参数的时候更加复杂,因为unique_ptr是在函数模板处传递的,而shared_ptr是在构造函数中传递参数实现的
函数指针
仿函数
lambda做删除器

weak_ptr

weak_ptr是C++11设计出来的智能指针,他的名字翻译出来是弱指针,他完全不同于上⾯的智能指
针,他不⽀持RAII,也就意味着不能⽤它直接管理资源,weak_ptr的产⽣本质是要解决shared_ptr
的⼀个循环引⽤导致内存泄漏的问题。
我们来看一个样例

share_ptr的循环引用问题

我们来看下面的代码



struct ListNode
{
    int _data;
    std::shared_ptr<ListNode> _next;
    std::shared_ptr<ListNode> _prev;
    ListNode()
        :_data(0)
        ,_next(nullptr)
        ,_prev(nullptr)
    {}
    ~ListNode()
    {
        std::cout << "~ListNode()" << std::endl;
    }
};

void function()
{
	std::shared_ptr<ListNode> d1(new ListNode);
	std::shared_ptr<ListNode> d2(new ListNode);

	d1->_next = d2;
	d2->_prev = d1;

}

上面这个代码是一个典型的循环引用

shared_ptr⼤多数情况下管理资源⾮常合适,⽀持RAII,也⽀持拷⻉。但是在循环引⽤的场景下会
导致资源没得到释放内存泄漏
我们来看看是怎么构成循环引用的
    我们知道资源销毁的条件是没有指针再去指向它,右边的d2什么时候释放当左边的d1的next管着的,next析构之后右边的d2就析构了,next什么时候析构,next是左边的d1的成员,当左边d1销毁之后next就析构了,左边的d1什么时候析释放,右边的d2的prev管着的,prev析构后左边的就释放了右边的d2什么时候释放,这又回到开头了,这样就构成了循环引用。
因此:
把ListNode结构体中的_next和_prev改成weak_ptr,weak_ptr绑定到shared_ptr时不会增加它的
引⽤计数,_next和_prev不参与资源释放管理逻辑,就成功打破了循环引⽤,解决了这⾥的问题
weak_ptr的原理是什么
weak_ptr的本质就是不增加引用计数它只是在一旁默默观察,weak_ptr不参与资源的管理,但要参与引用计数的管理
两个share_ptr分别管理自己的资源他们之间通过weak_ptr进行连接,互相的引用计数不影响,这样就能把循环引用给打破。weak_ptr也要指向引用计数用来判断是否过期(指向的资源有无释放

内存泄漏

什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使⽤的内存,⼀般是忘记释 放或者发⽣异常释放程序未能执⾏导致的。内存泄漏并不是指内存在物理上的消失,⽽是应⽤程序分 配某段内存后,因为设计错误,失去了对该段内存的控制,因⽽造成了内存的浪费。

内存泄漏的危害:普通程序运⾏⼀会就结束了出现内存泄漏问题也不⼤,进程正常结束,⻚表的映射 关系解除,物理内存也可以释放。⻓期运⾏的程序出现内存泄漏,影响很⼤,如操作系统、后台服 务、⻓时间运⾏的客⼾端等等,不断出现内存泄漏会导致可⽤内存不断变少,各种功能响应越来越 慢,最终卡死。
我们需要去尽量规避内存泄漏,可以使用合理的工具像windows的vld和linux的dmalloc等这样我们的程序才能健壮,但最重要的是自己的编程习惯,要尽最大可能去规避内存泄漏

更多推荐