C++智能指针详解(auto_ptr、unique_ptr、shared_ptr)

1. 智能指针的应用场景

int Div(int a, int b)
{
	if (b == 0)
	{
		throw invalid_argument("除0错误");
	}
	return a / b;
}

int main()
{
	try
	{
		int* ptr = new int[10];
		int a, b;
		cin >> a >> b;
		cout << Div(a, b) << endl;	//Div函数可能抛异常
		delete ptr;
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}
	catch (...)
	{
		cout << "Unknown exception!" << endl;
	}
	return 0;
}

 在上面这个场景下,当Div函数抛异常时,就会导致申请的空间没法释放,也就造成了内存泄漏 (没法执行到delete ptr这句代码)

由于C++没有GC (垃圾回收器),我们通过malloc/new出来的资源需要我们手动去释放,因此就可能出现如下情况:

①:忘记释放

②:由于触发异常而导致资源泄露 (异常安全问题)

 面对上面的这两种情况,C++提出了智能指针!

2. 智能指针的介绍

· RAII

 RAII (Resource Acquisition Is Initialization) 是一种利用对象的生命周期来控制程序资源的思想。

 RAII:在对象构造时去获取资源,接着控制对资源的访问使得在对象的生命周期内始终保持有效,最后在对象析构时去释放资源。  //本质是将资源托管给了一个对象

智能指针就是RAII思想的一个应用。 (unique_lock、lock_guard也是RAII的体现)

· 智能指针的类型

  1. auto_ptr //管理权转移, C++98
  2. unique_ptr //防拷贝, C++11
  3. shared_ptr //共享拷贝, C++11

注意:shared_ptr有时会配合weak_ptr一起使用,但weak_ptr不是智能指针

3. 智能指针的使用与原理

· 智能指针的使用

 智能指针就是一个类,我们可以定义该类的对象,然后像使用指针一样的去使用该对象。

//auto_ptr和unique_ptr也是这样用的
#include <memory>	//C++库中的智能指针都是定义在memory头文件中的
int main()
{
    //int* p = new int;
    //shared_ptr<int> sp(p);
	shared_ptr<int> sp(new int);	//等价于上面两行代码
	*sp = 1;
	std::cout << *sp << srd::endl;
	return 0;
}

· 智能指针的原理

//下面是按照RAII的思想写出来了smart_ptr类
template <class T>
class smart_ptr
{
public:
	smart_ptr(T* ptr)
		:_ptr(ptr)
	{}
    
	~smart_ptr()
	{
		if (_ptr)
        {
            std::cout << "delete _ptr: " << _ptr << std::endl;	//方便显示出它自动释放资源了
            delete _ptr;
        }
	}
    
	T& operator*()	{return *_ptr;}
	T* operator->()	{return _ptr;}
private:
	T* _ptr;
};

int main()
{
    smart_ptr<int> sp1(new int);
    smart_ptr<std::pair<int, int>> sp2(new std::pair<int, int>);

    *sp1 = 1;
	sp2->first = 5;	//这里实际上省略了一个->,sp2->访问到的是_ptr,_ptr->才能访问first。编译器对此进行了优化
	sp2->second = 10;

	std::cout << "sp1: " << *sp1 << std::endl;
	std::cout << "sp2: " << sp2->first << ":" << sp2->second << std::endl;
	return 0;
}

image-20211226203741126

 看起来这个类好像没有什么问题,实际上它有个巨大的缺陷:当发生拷贝构造拷贝赋值时就会出现问题!

image-20211226205957864

 为了解决上述的问题,C++98中提出了auto_ptr、C++11中提出了unique_ptr和shared_ptr。

3.1 auto_ptr

 auto_ptr使用了一种"管理权转移"的思想。结合下面的代码,我们定义ap1后,用ap1去拷贝构造ap2,此时会将ap1的指针置为空nullptr,不让你再使用ap1了,相当于把这块空间的管理权转交给了ap2。

int main()
{
	auto_ptr<int> ap1(new int(1));
	auto_ptr<int> ap2(ap1);	//此时会将ap1中的指针置为nullptr
	return 0;
}

· 简单模拟实现auto_ptr

//简单的模拟实现auto_ptr (与库中的不一样),了解一下原理
namespace wqj	//防止与库中的冲突
{
	template <class T>
	class auto_ptr
	{
	public:
		auto_ptr(T* ptr)
			:_ptr(ptr)
		{}

		~auto_ptr()
		{
			if (_ptr)
				delete _ptr;
		}

		auto_ptr(auto_ptr<T>& ap)	//不能是const对象
			:_ptr(ap._ptr)
		{
			ap._ptr = nullptr;	//将ap置空
		}

		auto_ptr<T>& operator=(auto_ptr<T>& ap)	//不能是const对象
		{
			if (this != &ap)	//防止自己给自己赋值
			{
                  //释放_ptr的资源
				if (_ptr)
					delete _ptr;
				_ptr = ap._ptr;		//将ap中的资源转移到_ptr中
				ap._ptr = nullptr;	//将ap置空
			}
			return *this;
		}
        
		T& operator*()	{return *_ptr;}
		T* operator->()	{return _ptr;}
	private:
		T* _ptr;
	};
}

3.2 unique_ptr

 unique_ptr禁止进行拷贝和赋值的操作。 (一旦进行拷贝或赋值就会报错)

· 简单模拟实现unique_ptr

namespace wqj
{
    //unique_ptr的模拟实现
	template <class T>
	class unique_ptr
	{
	public:
		unique_ptr(T* ptr)
			:_ptr(ptr)
		{}

		~unique_ptr()
		{
			if (_ptr)
				delete _ptr;
		}
        
		T& operator*()	{return *_ptr;}
		T* operator->()	{return _ptr;}
        
		//C++11中提供的delete关键字,禁止生成指定的默认成员函数 (也可以将它们设为private属性)
		unique_ptr(const unique_ptr<T>& up) = delete;
		unique_ptr<T>& operator=(const unique<T>& up) = delete;
	private:
		T* _ptr;
	};
}

3.3 shared_ptr

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

注意:

  1. shared_ptr内部维护了一个引用计数变量,该变量是指针类型int*,只有指针类型才能保证拷贝自同一对象的不同对象享有相同的引用计数变量。 //int类型、int&类型(会暴露在外面)、static int类型都无法保证
  2. 当对象被销毁时,会将对象的引用计数减一
  3. 引用计数为0时,释放所申请的资源;不为0就不释放

· 简单模拟实现shared_ptr

namespace wqj
{
	template <class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr)
			:_ptr(ptr),
			 _pcount(new int(1))	//初始化引用计数为1
		{}

		~shared_ptr()
		{
			if(--(*_pcount) == 0)
			{
				delete _ptr;
				_ptr = nullptr;
				//引用计数的资源也要释放!
				delete _pcount;
				_pcount = nullptr;
			}
		}

		T& operator*() { return *_ptr; }
		T* operator->() { return _ptr; }
		
         //返回当前引用计数的值
		int use_count()
		{
			return *_pcount;
		}

		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr),
			 _pcount(sp._pcount)
		{
			++(*_pcount);
		}

		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			if (this != &sp)
			{
                 //先将_pcount的引用计数--,然后判断是否需要释放资源
				if (--(*_pcount) == 0)
				{
					delete _ptr;
					_ptr = nullptr;

					delete _pcount;
					_pcount = nullptr;
				}

				_ptr = sp._ptr;
				_pcount = sp._pcount;
				++(*_pcount);
			}
			return *this;
		}
	private:
		T *_ptr;
		int* _pcount;
	};
}

3.4 shared_ptr与线程安全

 上面的shared_ptr的代码是存在线程安全问题的,下面是一份测试代码,我们创建了2个线程,这两个线程都会去sp对象中的引用计数_pcount进行++,而这种++是存在线程安全问题的,它的汇编代码是3条,因此这个操作不能保证原子性。面对这种情况,我们要对临界资源进行加锁保护(也可以设为原子类型)

int main()
{
	int n = 10000;
	wqj::shared_ptr<int> sp(new int);
	std::cout << sp.use_count() << std::endl;

	std::thread t1([&]() {
		for (int i = 0; i < n; ++i)
		{
			wqj::shared_ptr<int> copy(sp);
		}
	});
	std::thread t2([&]() {
		for (int i = 0; i < n; ++i)
		{
			wqj::shared_ptr<int> copy(sp);
		}
	});

	t1.join();
	t2.join();
	std::cout << sp.use_count() << std::endl;
    return 0;
}

出错原因:举其中一种情况:t1线程创建出一个copy时,t2线程也创建出了一个copy对象,t1此时还没将count的值从寄存器写入内存, t2此时再去内存当中取count的值,就拿到了t1未更改的值。此时当t1将count的值再写入内存,count就++了,而t2再将count的值写入内存,count的值就不会发生变化了 。

//线程安全版本 --- shared_ptr的简单模拟实现
//在shared_ptr中,临界资源就是_pcount,因为它可能会被多个线程同时访问,所以我们需要对_pcount进行加锁保护
//1. 我们将引用计数++的过程封装成了Add_Ref_count()函数,里面自带了加锁和解锁
//2. 我们将引用计数--并判断是否需要释放资源的过程封装成了Release()函数,里面自带了加锁和解锁
namespace wqj
{
	template <class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr)
			:_ptr(ptr),
			 _pcount(new int(1)),
			 _pmtx(new std::mutex)
		{}
		//引用计数++
		void Add_Ref_count()
		{
			_pmtx->lock();
			++(*_pcount);
			_pmtx->unlock();
		}

		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr),
		 	 _pcount(sp._pcount),
			 _pmtx(sp._pmtx)
		{
			Add_Ref_count();
		}

		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			if (this != &sp)
			{
				//让当前引用计数--,并判断是否需要释放资源
				Release();

				//赋值
				_ptr = sp._ptr;
				_pcount = sp._pcount;
				_pmtx = sp._pmtx;

				//增加引用计数
				Add_Ref_count();
			}
			return *this;
		}
		//引用计数--,并判断是否需要释放资源
		void Release()
		{
			_pmtx->lock();
			bool flag = false;

			if (--(*_pcount) == 0)
			{
				delete _ptr;
				_ptr = nullptr;

				delete _pcount;
				_pcount = nullptr;

				flag = true;
				std::cout << "释放当前资源!" << std::endl;
			}
			_pmtx->unlock();

			//锁资源也要释放
			if (flag)
				delete _pmtx;
		}

		~shared_ptr()
		{
			Release();
		}
		
		T& operator*() { return *_ptr; }
		T* operator->() { return _ptr; }

		int use_count() 
		{
			return *_pcount; 
		}
	private:
		T* _ptr;
		int* _pcount;
		std::mutex* _pmtx;
	};
}

注意:

 在C++库中,shared_ptr是线程安全的智能指针! (其它的就不一定了!)

3.5 shared_ptr的循环引用

· 什么是循环引用?

 我们先来看下面这段代码

struct ListNode
{
	~ListNode()
	{
		std::cout << "~ListNode()" << std::endl;
	}

	//在使用智能指针的时候,为了让智能指针能够使用_prev/_next指向某一个智能指针的对象,
	//这里需要把它们定义为2个智能指针类型的对象
	shared_ptr<ListNode> _prev;
	shared_ptr<ListNode> _next;
	int _data;
};

int main()
{
	shared_ptr<ListNode> sp1(new ListNode);
	shared_ptr<ListNode> sp2(new ListNode);
	
	sp1->_next = sp2;	//sp2的引用计数++
	sp2->_prev = sp1;	//sp1的引用计数++
    std::cout << "~~~开始调用析构函数~~~" << std::endl;
	return 0;
}

image-20211228105502108

· 循环引用的原因

image-20211228105820339

count = 2的原因:
 对于sp1而言,sp2->中的prev成员,在进行sp2->_prev = sp1操作时,进行了拷贝赋值,而拷贝赋值会对sp1的引用计数++
 在最后进行析构的时候,sp1对象调用它的析构函数,在析构函数中会将引用计数- -,但是此时引用计数只是从2变为1,并没有变成0,这是因为sp2中的prev对象还指向着这块资源!所以sp1并不会释放这块资源。
 对于sp2也是如此,sp2最后析构的时候,sp2依然认为sp1中的next还指向这块资源(即便sp1对象已经销毁),所以sp2不会释放!

总结:由于sp1中的next指向sp2使得sp2的引用计数++,而当sp2资源释放的时候,引用计数减不到0,因此不会释放。sp2的next指向sp1使得sp1的引用计数++,当sp1资源释放的时候,引用计数同样减不到0,也不会释放。

· 关于ListNode中的prev和next对象:

 由于prev和next对象是shared_ptr类型 — 自定义类型,所以它们必须在ListNode的初始化列表阶段进行初始化。而它们初始化的话,首先就会去调用shared_ptr的构造函数进行初始化。所以shared_ptr的构造函数就决定了你需不需要在ListNode的构造函数阶段传参以及传什么样的参数!

 针对当前情况,我们只需要在shared_ptr中初始化一下成员变量,ptr成员初始化为nullptr就行。所以可以让shared_ptr提供缺省值就行,这样prev和next对象就能匹配到合适的构造函数*(不然的话,你就要在ListNode的构造函数中传shared_ptr构造函数需要的参数了)*。

image-20211228105632831


3.5.1 weak_ptr解决循环引用

 循环引用的关键就在于sp1->_next = sp2; 这句中的拷贝赋值。因为就是这句中的拷贝赋值使得sp2的引用计数++的。所以为了解决这句话中的循环引用问题,我们可以想办法让它无法引起sp2的引用计数++。在库中给出了weak_ptr类,它就是专门用来解决循环引用问题的,我们将next和prev的类型换成weak_ptr,并且在weak_ptr类中不提供引用计数机制,这样在使用next或prev的时候,就不会出现引用计数++的情况了。

struct ListNode
{
	~ListNode()
	{
		std::cout << "~ListNode()" << std::endl;
	}
	//这里使用weak_ptr去包裹prev和next,也就不会出现循环引用问题了
	weak_ptr<ListNode> _prev;
	weak_ptr<ListNode> _next;
	int _data;
};

int main()
{
	shared_ptr<ListNode> sp1(new ListNode);
	shared_ptr<ListNode> sp2(new ListNode);
	
	sp1->_next = sp2;
	sp2->_prev = sp1;
    std::cout << "~~~开始调用析构函数~~~" << std::endl;
	return 0;
}

image-20211228132030488

 在使用weak_ptr后,就没有出现引用计数中的count值变为2的情况,因为在sp1->_next = sp2的时候,next中由于不存在引用计数变量count,所以不会使得sp2的引用计数++。sp1也是同理。

总结:weak_ptr的存在使得智能指针shared_ptr在进行next/prev的拷贝赋值时,去调用了weak_ptr类中的拷贝赋值函数,而该函数中没有引用计数机制,所以不会引起引用计数增加。

· 简单模拟实现weak_ptr

 weak_ptr不是智能指针,它的目的只是为了解决shared_ptr在循环引用上的缺陷,所以它没有RAII的思想,也就是在weak_ptr中,并不会存在引用计数成员变量、互斥锁。我们需要的weak_ptr,它能够"像指针一样"就足够了。

namespace wqj    
{
	//weak_ptr并不是智能指针,它没有RAII思想,它只是能"像指针一样"
	template <class T>
	class weak_ptr
	{
	public:
		//这里必须给一个默认构造函数!!!
		//在ListNode类中,有自定义类型weak_ptr<ListNode> _prev存在,
		//该对象会在ListNode的构造函数中的初始化列表阶段进行初始化,而初始化就会去调用weak_ptr的构造函数
		//如果没有合适的构造函数就会报错了! 所以这里给了默认构造函数, 如果不给就不会生成(因为后面我们写了拷贝构造, 它就不会自动生成构造了)
		weak_ptr() = default;

		//为了能让weak_ptr类能够访问shared_ptr中的私有成员sp,以下给出2种方法
         //1. shared_ptr中提供Get_Ptr()的接口,返回ptr成员
         //2. 将weak_ptr变为shared_ptr的友元类
        
         //方法1:
        /* 在shared_ptr中的Get_Ptr()函数:
		T* Get_Ptr() const
		{ return _ptr; }
        */
		weak_ptr(const shared_ptr<T>& sp)
			:_ptr(sp.Get_Ptr())
		{}
		weak_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			_ptr = sp.Get_Ptr();
			return *this;
		}

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

· 关于模板友元类

 上面我提到了访问私有成员的第2种方法就是将weak_ptr设为shared_ptr的友元类,这样weak_ptr就能访问shared_ptr的私有成员变量了。但是有个细节需要注意,友元类如果是模板的话,需要前置声明一下,不然编译器在编译的期间,会因为模板还没实例化而导致识别不出来friend class B<T>

//前置声明
template <class T>
class B;

template <class T>
class A
{
    friend class B<T>;	//定义B类为A类的友元类
private:
    T _a;
};

template <class T>
class B
{
public:
    void func()
    {
        A<T> a;
        a._a;	//访问a的私有成员
    }
private:
    T _b;
};

int main()
{
	B<int>().func();
    return 0;
}

· weak_ptr的友元类写法

namespace wqj
{
    //前置声明
    template <class T>
    class weak_ptr;
    
    template <class T>
	class shared_ptr
    {
      //...
    private:
        friend class weak_ptr<T>;	//将weak_ptr作为shared_ptr的友元类
    };
    
	template <class T>
	class weak_ptr
	{
	public:
		weak_ptr() = default;
        
		//方法2: 使用了友元类语法(需要前置声明+友元类的定义语法)
		weak_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
		{}
		weak_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			_ptr = sp._ptr;
			return *this;
		}

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

4. 智能指针与定制删除器

 我们在前面测试的时候,new出来的都是1个对象,那么当new [ ]多个对象的时候,使用智能指针能够正常释放资源吗?如果不使用new去是申请资源而是使用其它的方式比如: malloc、fopen,那么在智能指针的析构函数中继续使用delete去释放资源就会出问题了! 解决方法↓↓↓

class A
{
public:
	A()	{ std::cout << "A()" << std::endl; }
	~A() { std::cout << "~A()" << std::endl; }
public:
	int _a;
};

int main()
{
	shared_ptr<A> sp1(new A[10]);
	shared_ptr<A> sp2((A*)malloc(sizeof(A)));
	shared_ptr<FILE> sp3(fopen("test.txt", "w"));
	return 0;
}
image-20211228153853181

 在C++的非官方库boost库中,很早就提出了智能指针,C++11中引入的智能指针也是参考了boost库中的实现。

在boost库中有下面种智能指针:

scoped_ptr / scoped_array //对应unique_ptr

shared_ptr / shared_array //对应shared_ptr

weak_ptr

我们注意到,在C++11中并没有xxx_array的版本,这个xxx_array对应的就是new [ ]或不是new出来的空间。C++11中如何处理这种问题?

解决方法:

 C++中提供了"定制删除器"的方法,你可以向shared_ptr中传递一个仿函数对象,这个仿函数中提供了释放资源的方法

//解决方法示例
using namespace std;
class A
{
public:
	A()	{ std::cout << "A()" << std::endl; }
	~A() { std::cout << "~A()" << std::endl; }
public:
	int _a;
};

template <class T>
struct DeleteAFunc
{
	void operator()(T* ptr)
	{
		cout << "delete[] ptr" << endl;
		delete[] ptr;
	}
};

template <class T>
struct FreeAFunc
{
	void operator()(T* ptr)
	{
		cout << "free ptr" << endl;
		free(ptr);
	}
};
//文件类型就不需要使用模板了
struct FcloseFunc
{
	void operator()(FILE* fp)
	{
		cout << "fclose fp" << endl;
		fclose(fp);
	}
};

int main()
{
	shared_ptr<A> sp1(new A[10], DeleteAFunc<A>());	//传一个仿函数对象(自定义析构的方法)
	shared_ptr<A> sp2((A*)malloc(sizeof(A)), FreeAFunc<A>());
	shared_ptr<FILE> sp3(fopen("test.txt", "w"), FcloseFunc());
	return 0;
}

5. lock与RAII

 RAII思想的应用不止有智能指针,还可以用来设计锁,在C++库中,lock_guard和unique_lock都是RAII的锁。lock_guard的作用就是,在锁被定义的时候上锁,锁的生命周期到了解锁,lock_guard无法手动加锁/解锁!unique_lock是除了具备lock_guard的功能之外,还允许用户自己选择上锁的时间和解锁的时间。

//lock_guard的简单模拟
template <class Lock>
class Lock_Guard
{
public:
	Lock_Guard(Lock& lk)
		:_lk(lk)
	{
		_lk.lock();
	}
    
	~Lock_Guard()
	{
		_lk.unlock();
	}
	//锁是禁止进行拷贝的
	Lock_Guard(const Lock_Guard<Lock>&) = delete;
private:
    //这里必须加上引用,否则你在外面是传不进来lock锁的
    //因为在构造函数期间,一定会进行lock锁的拷贝构造,去初始化_lk成员变量,然而锁的拷贝是禁止的!
    //因此,为了保证外面传来的锁和类中的成员变量_lk锁是同一个锁,我们需要将_lk改为引用.
	Lock& _lk;
};

mutex mtx;
int cnt = 0;

int main()
{

	thread t1([&] {
		Lock_Guard<mutex> lg(mtx);
		for (int i = 0; i < 100000; ++i)
		{
			++cnt;
		}
	});

	thread t2([&] {
		Lock_Guard<mutex> lg(mtx);
		for (int i = 0; i < 100000; ++i)
		{
			++cnt;
		}
	});

	t1.join();
	t2.join();	
	std::cout << cnt << '\n';
	return 0;
}

6. 内存泄漏

问题:什么是内存泄漏?

 内存泄漏是指我们向内存申请了资源,当资源使用完成后,我们忘记对其进行释放回收工作或者因为异常安全等问题而导致无法进行释放。

问题:内存泄漏的危害是什么?

 当我们申请内存没有释放,进程正常退出 (没变成僵尸进程),那么申请的内存也会被释放。

 一般的程序遇到内存泄漏问题,重启一下就好了。但是对于一些长期运行的,不能随时重启的程序,内存泄漏的危害就会很大了。比如:操作系统、服务器上的服务。当这些程序长期运行,不用的内存没有释放,这会使得可用的内存越来越少,最终会导致系统速度变慢、操作的失败(创建套接字、打开文件、发送数据都是需要内存的)

问题:如何解决内存泄漏问题?

  1. 在写C/C++代码时要谨慎
  2. 遇到不好处理资源的申请与释放的地方,要多使用智能指针去管理。 (事前预防)
  3. 如果内存泄漏问题可能已经出现了,那么可以使用内存泄漏工具去检测。 (事后解决), 如valgrind工具(Linux的)
  • 32
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 19
    评论
评论 19
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值