c++智能指针


前言


一、智能指针

1、为什么需要智能指针?

下面我们先分析一下下面这段程序有没有什么内存方面的问题?提示一下:注意分析Func函数中的问题。
下面的代码中会因为异常的抛出而造成内存泄漏。例如如果在p2的new中抛出异常,那么会直接跳到异常的处理catch的代码块中,这样就没有执行Func函数中的delete p1来释放p1的内存了。而如果在div()函数中抛出异常,那么不会执行delete p1和delete p2,这样就造成了p1和p2的内存泄漏。

int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
	{
		throw "除0错误";
	}
	return a / b;
}

void Func()
{
	//1.如果p1这里的new抛出异常会如何?
	//2.如果p2这里的new抛出异常会如何?
	//3.如果div调用这里抛出异常会如何?
	int* p1 = new int;
	int* p2 = new int;
	cout << div() << endl;
	
	delete p1;
	delete p2;
}

int main()
{
	try
	{
		Func();
	}
	catch (const char* str)
	{
		cout << str << endl;
	}

	return 0;
}

尽管我们可以通过下面的方法来避免p1和p2的内存泄漏,但是当Func函数中申请的内存越来越多时,这种解决办法就显得很笨重了。

int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
	{
		throw "除0错误";
	}
	return a / b;
}

void Func()
{
	//1.如果p1这里的new抛出异常会如何?
	//2.如果p2这里的new抛出异常会如何?
	//3.如果div调用这里抛出异常会如何?
	int* p1 = new int;
	int* p2 = nullptr;
	try
	{
		p2 = new int;
	}
	catch (...)
	{
		delete p1;
		throw;
	}
	try
	{
		cout << div() << endl;
	}
	catch (...)
	{
		delete p1;
		delete p2;
		throw;
	}
	
	delete p1;
	delete p2;
}

int main()
{
	try
	{
		Func();
	}
	catch (const char* str)
	{
		cout << str << endl;
	}

	return 0;
}

2、内存泄漏

2.1 什么是内存泄漏,内存泄漏的危害

什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。

2.2 内存泄漏分类

C/C++程序中一般我们关心两种方面的内存泄漏。

  • 堆内存泄漏(Heap leak)
    堆内存指的是程序执行中依据需要,通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。
  • 系统资源泄漏
    指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。

2.3 如何检测内存泄漏

在linux下内存泄漏检测:linux下几款内存泄漏检测工
在windows下使用第三方工具:VLD工具说明
其他工具:内存泄漏工具比较

2.4如何避免内存泄漏

  1. 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps:这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智能指针来管理才有保证。
  2. 采用RAII思想或者智能指针来管理资源。
  3. 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
  4. 出问题了使用内存泄漏工具检测。ps:不过很多工具都不够靠谱,或者收费昂贵。

总结一下:内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工具。

3、智能指针的使用及原理

在学习智能指针的使用之前我们需要先了解RAII思想。

3.1 RAII

RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:

  • 不需要显式地释放资源。
  • 采用这种方式,对象所需的资源在其生命期内始终保持有效。

如果我们实现一个SmartPtr模板类来充当指针。然后我们在使用指针时就将指针放到SmartPtr对象中,这样就不会出现上面说的内存泄漏现象了。因为当sp1或sp2或div函数抛异常时,程序就会跳到main函数中执行catch中的代码,而这时就出了Func函数的作用域,所以sp1对象和sp2对象就会自动销毁,而且在销毁前会调用自己的析构函数来将new int的内存进行释放。

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

	}

	~SmartPtr()
	{
		delete _ptr;
	}
private:
	T* _ptr;
};

int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
	{
		throw "除0错误";
	}
	return a / b;
}

void Func()
{
	//1.如果p1这里的new抛出异常会如何?
	//2.如果p2这里的new抛出异常会如何?
	//3.如果div调用这里抛出异常会如何?
	SmartPtr<int> sp1(new int);
	SmartPtr<int> sp2(new int);

	cout << div() << endl;
	
}

int main()
{
	try
	{
		Func();
	}
	catch (const char* str)
	{
		cout << str << endl;
	}

	return 0;
}

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

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

3.2 智能指针拷贝问题

当我们像下面一样调用SmartPtr的拷贝构造函数时会出现错误。这是因为上面实现的SmartPtr模板类中没有写拷贝构造,所以会使用编译器自动生成的浅拷贝构造函数。所以就造成了两次析构函数释放资源,就出现了错误。
那么我们能否自己写SmartPtr的拷贝构造函数呢?让SmartPtr类模板的拷贝构造函数为一个深拷贝的拷贝构造函数。这样也是不行的,因为我们需要的就是浅拷贝,即当sp2拷贝sp1时,就说明sp1和sp2指向了同一个资源,如果我们使用深拷贝的话,那么sp1和sp2就会指向不同的资源,这样就不和指针的作用相同了。
在这里插入图片描述

3.3 auto_ptr

在c++98中,使用了资源管理权限转移的方式来解决智能指针拷贝的问题。下面我们来了解auto_ptr智能指针的原理。
下面的代码中在auto_ptr的拷贝构造函数中将新的auto_ptr对象指向资源,而将原来的auto_ptr对象中保存的指针置为空。即将ap1的资源管理权转移给了ap2,这样的话就会造成ap1为空指针了。

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

	}
	~auto_ptr()
	{
		if (_ptr)
		{
			cout << "delete:" << _ptr << endl;
			delete _ptr;
		}
	}

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

	T& operator*()
	{
		return *_ptr;
	}

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

void test_auto()
{
	auto_ptr<int> ap1(new int(1));
	auto_ptr<int> ap2(ap1);
	*ap1 = 1;  //管理权转移以后导致ap1悬空,不能访问
	*ap2 = 2;
}

int main()
{
	test_auto();

	return 0;
}

在这里插入图片描述
上面是我们自己实现的auto_ptr智能指针。在c++的库中也提供了auto_ptr智能指针,并且库里面的auto_ptr也存在悬空问题,所以在实际中auto_ptr智能指针没有使用价值。
在这里插入图片描述

3.4 unique_ptr智能指针

boost库为c++的“准”标准库。c++11中的智能指针unique_ptr和shared_ptr/weak_ptr就是借鉴的boost库中的scoped_ptr和shared_ptr/weak_ptr。
Boost库是为C++语言标准库提供扩展的一些C++程序库的总称,由Boost社区组织开发、维护。Boost库可以与C++标准库完美共同工作,并且为其提供扩展功能。

下面我们就来了解c++11中的unique_ptr智能指针的原理。在c++11中直接使用delete将unique_ptr的拷贝构造函数和辅助运算符重载函数不自动生成。即不允许unique_ptr进行拷贝。
在c++98的语法中,想要将一个类不允许拷贝,那么可以将这个类的拷贝构造函数和赋值运算符重载函数声明为私有。
在c++11中可以直接将拷贝构造函数和赋值运算符重载函数为delete,即表示禁止生成默认拷贝构造函数和赋值运算符重载函数。

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

	}
	~unique_ptr()
	{
		if (_ptr)
		{
			cout << "delete:" << _ptr << endl;
			delete _ptr;
		}
	}
	//在c++11中可以直接将拷贝构造函数和赋值运算符重载函数为delete
	unique_ptr(const unique_ptr<T>& up) = delete;
	unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;
	T& operator*()
	{
		return *_ptr;
	}

	T* operator->()
	{
		return _ptr;
	}
private:
	//防拷贝
	//拷贝构造函数和赋值运算符重载函数是默认成员函数,我们不写会自动生成。
	//在c++98中:可以通过将拷贝构造函数和赋值运算符重载函数声明为私有来防止拷贝
	/*unique_ptr(const unique_ptr<T>& up);
	unique_ptr<T>& operator=(const unique_ptr<T>& up);*/
	
private:
	T* _ptr;
};


int main()
{
	unique_ptr<int> up1(new int(1));
	unique_ptr<int> up2(up1);   //因为防止拷贝,所以不能再进行unique_ptr的拷贝了。
	return 0;
}

3.5 shared_ptr

C++11中还提供了更靠谱的并且支持拷贝的shared_ptr。
shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。

  1. shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。
  2. 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
  3. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
  4. 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。

下面我们来模拟实现一个shared_ptr类模板。
我们需要先分析shared_ptr中应该使用什么类型的变量来记录引用计数。如果使用静态成员变量的话,那么通过shared_ptr类模板生成的对象都会共同使用这一个计数,这显然是不行的。因为有可能多个对象指向多个资源,我们需要的是让每个资源配对一个引用计数。
在这里插入图片描述
所以我们可以将记录引用计数的变量设为一个指针。
在这里插入图片描述
这样如果是第一个智能指针对象指向这个资源,那么就申请一个_pcount指针来记录引用计数,当调用析构函数时,先将引用计数减减,判断是否为0,如果为0那么就释放资源和引用计数。

template<class T>
class shared_ptr
{
public:
	shared_ptr(T* ptr)
		:_ptr(ptr),_pcount(new int(1))
	{

	}
	~shared_ptr()
	{
		if (--(*_pcount) == 0)
		{
			cout << "delete:" << _ptr << endl;
			delete _ptr;
			delete _pcount;
		}
	}
	shared_ptr(const shared_ptr<T>& sp)
		:_ptr(sp._ptr),_pcount(sp._pcount)
	{
		++(*_pcount);
	}

	T& operator*()
	{
		return *_ptr;
	}

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


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

	shared_ptr<int> sp3(new int(10));
	return 0;
}

我们看到sp1和sp2的引用计数为2,而sp3资源的引用计数为1。
在这里插入图片描述
下面我们来实现shared_ptr的赋值运算符重载函数。我们需要注意的是如果被赋值的对象指向的资源只有它自己,那么当对这个对象进行赋值后,需要将这个对象指向的资源和引用计数给释放掉。例如对sp3赋值时,需要将sp3原来指向的资源先释放掉。
在这里插入图片描述
在这里插入图片描述

3.6 std::shared_ptr的线程安全问题

智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时++或–,这个操作不是原子的,引用计数原来是1,++了两次,可能还是2.这样引用计数就错乱了。会导致资源未释放或者程序崩溃的问题。
在下面的代码中两个线程都对对象p的引用计数进行++或–操作,并且因为++或–操作不是原子性的,所以就可能会造成线程安全问题。
下面的代码中我们需要注意,在创建线程对象时,直接传引用是会报错的。需要使用ref函数来传引用。如果不加的话会认为是传值传参,而因为mtx锁不允许拷贝,所以传值传参会出错。

template<class T>
class shared_ptr
{
public:
   shared_ptr(T* ptr)
   	:_ptr(ptr),_pcount(new int(1))
   {

   }
   ~shared_ptr()
   {
   	Release();
   }
   void Release()
   {
   	if (--(*_pcount) == 0)
   	{
   		cout << "delete:" << _ptr << endl;
   		delete _ptr;
   		delete _pcount;
   	}
   }
   void AddCount()
   {
   	++(*_pcount);
   }
   shared_ptr(const shared_ptr<T>& sp)
   	:_ptr(sp._ptr),_pcount(sp._pcount)
   {
   	AddCount();
   }

   // sp1 = sp4
   shared_ptr<T>& operator=(const  shared_ptr<T>& sp)
   {
   	//如果两个对象管理的是同一个资源,那么就不需要赋值了
   	if (_ptr != sp._ptr)
   	{
   		Release();
   		_ptr = sp._ptr;
   		_pcount = sp._pcount;
   		AddCount();
   	}
   	return *this;
   }

   T& operator*()
   {
   	return *_ptr;
   }

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

   T* get()
   {
   	return _ptr;
   }

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

struct Date
{
   int _year = 0;
   int _month = 0;
   int _day = 0;
};

void SharePtrFunc(shared_ptr<Date>& sp, size_t n, std::mutex& mtx)
{
   cout << sp.get() << endl;
   for (size_t i = 0; i < n; ++i)
   {
   	//这里智能指针拷贝会++计数,智能指针析构会--计数,这里是线程安全的。
   	shared_ptr<Date> copy(sp);
   }
}

void test_shared_safe()
{
   shared_ptr<Date> p(new Date);
   cout << p.get() << endl;
   const size_t n = 10000;
   std::mutex mtx;
   std::thread t1(SharePtrFunc, std::ref(p), n, std::ref(mtx));
   std::thread t2(SharePtrFunc, std::ref(p), n, std::ref(mtx));

   t1.join();
   t2.join();

   cout << p.use_count() << endl;
}
int main()
{
   test_shared_safe();
   return 0;
}

在这里插入图片描述
所以我们就需要为每一份资源加一把锁,当多个对象指向同一个资源时,每个对象想要改变这份资源的内容,都需要先申请锁。

template<class T>
class shared_ptr
{
public:
	shared_ptr(T* ptr)
		:_ptr(ptr),_pcount(new int(1)),_pmtx(new std::mutex)
	{

	}
	~shared_ptr()
	{
		Release();
	}
	void Release()
	{
		_pmtx->lock();
		if (--(*_pcount) == 0)
		{
			cout << "delete:" << _ptr << endl;
			delete _ptr;
			delete _pcount;
		}
		_pmtx->unlock();
	}
	void AddCount()
	{
		_pmtx->lock();
		++(*_pcount);
		_pmtx->unlock();
	}
	shared_ptr(const shared_ptr<T>& sp)
		:_ptr(sp._ptr),_pcount(sp._pcount),_pmtx(sp._pmtx)
	{
		AddCount();
	}

	// sp1 = sp4
	shared_ptr<T>& operator=(const  shared_ptr<T>& sp)
	{
		//如果两个对象管理的是同一个资源,那么就不需要赋值了
		if (_ptr != sp._ptr)
		{
			Release();
			_ptr = sp._ptr;
			_pcount = sp._pcount;
			_pmtx = sp._pmtx;
			AddCount();
		}
		return *this;
	}

	T& operator*()
	{
		return *_ptr;
	}

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

	T* get()
	{
		return _ptr;
	}

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

struct Date
{
	int _year = 0;
	int _month = 0;
	int _day = 0;
};

void SharePtrFunc(shared_ptr<Date>& sp, size_t n, std::mutex& mtx)
{
	cout << sp.get() << endl;
	for (size_t i = 0; i < n; ++i)
	{
		//这里智能指针拷贝会++计数,智能指针析构会--计数,这里是线程安全的。
		shared_ptr<Date> copy(sp);
	}
}

void test_shared_safe()
{
	shared_ptr<Date> p(new Date);
	cout << p.get() << endl;
	const size_t n = 10000;
	std::mutex mtx;
	std::thread t1(SharePtrFunc, std::ref(p), n, std::ref(mtx));
	std::thread t2(SharePtrFunc, std::ref(p), n, std::ref(mtx));

	t1.join();
	t2.join();

	cout << p.use_count() << endl;
}
int main()
{
	test_shared_safe();
	return 0;
}

在这里插入图片描述
但是上面的代码中,我们虽然保证了线程的安全,但是我们在释放资源时并没有将锁释放。因为在Release中释放资源时,锁还处于使用状态,所以我们可以使用下面的方案来释放锁。即当判断这个资源的引用计数为0时,在释放资源后进行归还锁,然后再将锁资源释放。
在这里插入图片描述
我们还可以通过下面设置一个标志位的方法来进行锁的释放。这样就解决了锁的释放问题。
在这里插入图片描述
上面通过给智能指针中的引用计数操作进行加锁,我们保证了智能指针的引用计数是线程安全的。但是我们不能保证智能指针指向的资源是线程安全的。当在多线程情况下,我们看到智能指针指向的资源会出现线程安全问题。
在这里插入图片描述
在这里插入图片描述
当我们将通过智能指针修改资源的操作进行加锁后,这样就保证了智能指针指向的资源是线程安全的了。
在这里插入图片描述
我们使用库里面的shared_ptr智能指针时,如果不将通过智能指针修改资源的操作加锁,那么也会出现线程安全问题。
在这里插入图片描述
当我们加上锁之后就没有线程安全问题了。
在这里插入图片描述

3.7 shared_ptr的循环引用

当我们想要将两个结点连接起来时,我们可以使用下面的代码,但是下面的程序当在delete之前抛异常时,那么就会出现内存泄漏问题。
在这里插入图片描述
所以我们可以使用智能指针来实现。但是原生指针不能赋值给智能指针,所以我们需要将ListNode结构体中的_ next和_ prev也使用智能指针。
在这里插入图片描述
在这里插入图片描述
当我们运行时发现智能指针n1和n2指向的结点并没有调用析构函数来进行释放。在这里插入图片描述
但是当我们屏蔽一句两个结点的连接代码或者两句都屏蔽时,就会调用对应的析构函数。这就是shared_ptr的循环引用问题。
在这里插入图片描述
当下面的情况时,n1和n2在析构时因为引用计数为1,减减后为0,所以会执行delete _ ptr,而 _ ptr为ListNode * 类型的指针,所以会调用ListNode的析构函数来释放所指向的结点的资源。
在这里插入图片描述
下面的情况时。当n2调用析构函数时,发现引用计数为2,然后将引用计数减减,并不会进行资源释放。当n1调用析构时,将引用计数减减后为0,然后将自己指向的资源释放,在释放资源时会先调用_ next指向的资源,先将_ next指向的资源释放。而_ next指向的资源引用计数为1,减减后为0,所以会调用析构函数释放资源。
在这里插入图片描述
下面的情况中,当n2出作用域时会调用自己的析构函数,然后发现引用计数减减后还为1,所以不会delete释放结点资源。当n1出作用域时会调用自己的析构函数,然后发现引用计数减减后也为1,所以不会delete释放结点资源。然后就造成了_ next指向结点2,_ prev指向结点1。然后这两个智能指针的引用计数都为1,并且不再改变,所以就不会释放delete释放结点1和结点2了。
在这里插入图片描述
此时我们就可以使用weak_ptr来解决。weak_ptr不是常规的智能指针,不支持RAII思想。但是它支持像指针一样使用。它是专门设计出来辅助解决shared_ptr的循环引用问题的。weak_ptr可以指向资源,但是它不参与资源管理,不增加引用计数。
在这里插入图片描述
weak_ptr的拷贝构造函数中可以将一个shared_ptr智能指针赋值给weak_ptr智能指针。
在这里插入图片描述
我们看到当使用weak_ptr时就不会出现循环引用问题了。
在这里插入图片描述
下面我们来模拟实现一个weak_ptr智能指针。
在这里插入图片描述
然后我们看到循环引用问题就解决了。
在这里插入图片描述

3.8、定制删除器

当我们在智能指针中存放的是一个new出来的数组时,程序会崩溃。因为我们在shared_ptr的析构函数中释放内存时写的都是delete,所以当delete释放 new []时,会出现错误。
在这里插入图片描述
所以shared_ptr设计了一个删除器来解决这个问题。定制删除器为一个可调用对象,我们可以传函数指针,仿函数,lambda表达式等。即可以通过定制删除器来让用户自己定义delete时需要进行的操作。如果我们不写默认就是delete。
在这里插入图片描述
我们还可以传lambda表达式,并且将delete替换为关闭文件。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值