C++11新特性(智能指针详细介绍)

目录

1. 智能指针的基本概念

2. 智能指针的使用

3. C++库中的智能指针

3.1 auto_ptr

3.2 unique_ptr

3.3 shared_ptr

3.3.1 多线程计数的安全问题:

3.3.2  定制删除器(仿函数、函数指针、lambda表达式):

3.3.4  shared_ptr模拟实现:

3.4 weak_ptr

3.4.1 weak_ptr的模拟实现:

3.4.2 循环引用问题:

4. 如何选择智能指针

5. 内存泄漏(补充)


1. 智能指针的基本概念

智能指针主要用于管理在堆上分配的内存,它将普通的指针封装为一个栈对象。当栈对象的生存周期结束后,会在析构函数中释放掉申请的内存,从而防止内存泄漏。

智能指针的实质是一个类对象,它是利用模板类对一般的指针进行封装,在类内的构造函数实现对指针的初始化,并在析构函数里编写delete语句删除指针指向的内存空间。这样在程序过期的时候,对象会被删除,内存会被释放,实现指针的安全使用。

作用:一是防止忘记调用delete,二是异常安全(在一段进行了 try/catch 的代码段里面,即使你写入了 delete,也有可能因为发生异常。程序进入 catch 块,从而没能释放内存)。

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

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

智能指针的实现要考虑三个方面的问题:

  • RAII(resource acquisition is initialization)资源获取即初始化
  • 像指针一样使用,即operate*和operate->
  • 智能指针对象拷贝问题

四种智能指针:

  1. auto_ptr C++98中,拷贝时,管理权转移,保持一个资源只有一个对象管理,同时导致拷贝后原对象悬空。(所以不推荐使用,甚至禁止使用)
  2. unique_ptr C++11中(就是boost中的scoped_ptr),简单粗暴,就是防拷贝。
  3. shared_ptr C++11中,允许拷贝,使用引用计数来允许多个智能指针对象管理一个资源。缺陷:循环引用。
  4. weak_ptr C++11中,主要是用来解决shared_ptr的循环引用问题。

2. 智能指针的使用

template
class SmartPtr
{
public:
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{}
	~SmartPtr()
	{
		//cout << "delete:" << _ptr << endl;
		delete _ptr;
	}
	// 可以像指针一样使用
	T& operator*(){
		return *_ptr;
	}
	T* operator->(){
		return _ptr;
	}
private:
	T* _ptr;
};
int main() {
	SmartPtr sp(new int);
	*sp = 10;
	cout << *sp << endl;
}

在上面这个简单实现的类中没有定义拷贝构造函数和赋值重载函数,那么只能调用类中原生的拷贝构造函数和赋值重载函数。那么就会程序就会出现崩溃的问题,如下: 

int main(){
    SmartPtr ptr1(new int(0));
    SmartPtr ptr2(ptr1);
    retrun 0;}

 ptr2和ptr1指向的同一块空间,当ptr2被销毁时,它会调用它的析构函数去delete该资源对象,当ptr1被销毁时,也会去调用它的析构函数去释放ptr1所指向的资源。所以,当程序结束时,ptr2被先被销毁,同时释放ptr2所指向的资源,然后ptr1被销毁,也去释放该资源对象,那么如下的资源对象同时被释放两次,所以程序就会被崩溃掉。(资源对象被释放后,如果再去释放该资源,程序就会崩溃)。

综上所述,不能使用原生的拷贝构造函数和赋值重载函数,且定义的拷贝构造函数和赋值重载函数需要考虑只能释放一次资源对象。也不能使用深拷贝,因为拷贝时我们想实现的就是p2与p1指向同一块地址。因此下面c++库中几种智能指针都是围绕如何解决拷贝这个问题来进行的。

3. C++库中的智能指针

3.1 auto_ptr

auto_ptr是c++98版本库中提供的智能指针,该指针解决上诉的问题采取的措施是管理权转移的思想,也就是原对象拷贝给新对象的时候,原对象就会被设置为nullptr,此时就只有新对象指向一块资源空间。

但是会有新问题,比如int* p2 = p1; 再通过p1就访问不了了 。很多公司都明确的规定了,不能使用auto_ptr。

C++11新特性(智能指针详细介绍)_第1张图片

 模拟实现:

	template
	class auto_ptr
	{
	public:
		auto_ptr(T* ptr)
			:_ptr(ptr)
		{}
		~auto_ptr()
		{
			if (_ptr)
			{
				cout << "delete:" << _ptr << endl;
				delete _ptr;
				_ptr = nullptr;
			}
		}

		// 拷贝构造并不能用深拷贝,因为int * p2 = p1;要的就是p2和p1指向同一块地址
		// 所以auto_ptr中通过管理权转移来实现,但是又会有新的问题
		// sp2(sp1);
		auto_ptr(auto_ptr& ap)
			:_ptr(ap._ptr)
		{
			ap._ptr = nullptr;
		}

		// ap1 = ap3,也是资源转移
		// ap1 = ap1
		auto_ptr& operator=(auto_ptr& ap)
		{
			if (this != &ap)
			{
				delete _ptr;
				_ptr = ap._ptr;
				ap._ptr = nullptr;
			}
			return *this;
		}

		// 可以像指针一样使用
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr;
	};

3.2 unique_ptr

简单粗暴的解决拷贝问题:禁止拷贝。

模拟实现:

	template
	class unique_ptr
	{
	public:
		unique_ptr(T* ptr)
			:_ptr(ptr)
		{}
		~unique_ptr()
		{
			cout << "delete:" << _ptr << endl;
			delete _ptr;
		}
		// 可以像指针一样使用
		T& operator*(){
			return *_ptr;
		}
		T* operator->(){
			return _ptr;
		}
		unique_ptr(const unique_ptr&) = delete;
		unique_ptroperator=(const unique_ptr&) = delete;
	private:
		T* _ptr;
	};

3.3 shared_ptr

  • 采用引用计数的方式解决拷贝问题。
  • 但是,在shared_ptr的使用中,多线程时,count的计数会不安全,因此需要加上互斥锁。
  • 同时,使用定制删除器 -- 可以控制释放资源的方式。
  • 会有循环引用的问题 -- 后面的weak_ptr可以解决。

引用计数的原理:

  • shared_ptr在内部会维护着一份引用计数,用来记录该份资源被几个对象共享。
  • 当一个shared_ptr对象被销毁时(调用析构函数),析构函数内就会将该计数减1。
  • 如果引用计数减为0后,则表示自己是最后一个使用该资源的shared_ptr对象,必须释放资源。
  • 如果引用计数不是0,就说明自己还有其他对象在使用,则不能释放该资源,否则其他对象就成为野指针。

C++11新特性(智能指针详细介绍)_第2张图片

3.3.1 多线程计数的安全问题:

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

		//这里智能指针访问管理的资源,不是线程安全的。
		//所以我们看看这些值两个线程++了2n次,但是最终看到的结果,并一定是加了2n
        //为什么不需要锁对其进行保护?因为*ptr返回的对象有可能被读或者被写,这个不是指针内部所考虑的,而是由调用者进行考虑的。
		(*copy)++;
	}
}
void test_multithread_shared_ptr()
{
	std::shared_ptr p(new int(0));
	const size_t n = 10000;
	thread t1(SharePtrFunc, p, n);
	thread t2(SharePtrFunc, p, n);
	t1.join();
	t2.join();
	cout << p.use_count() << endl;//这里应该是安全的
	cout << *p << endl;//这里可能不安全
}

3.3.2  定制删除器(仿函数、函数指针、lambda表达式):

 当我们释放一个指向数组的指针的时候delete[]后面的空方括号是必须存在(如下),它指示编译器此指针指向的是一个对象数组的第一个元素,如果我们在delete一个指向数组的指针中忽略了方括号,我们的程序可能在执行过程中在没有任何警告下行为异常。

我们如果在动态内存中创建出一个数组,用一个shared_ptr对象去指向该数组,当shared_ptr使用完后,就会去调用析构函数,由于shared_ptr默认的删除方式是 delete ptr,后面没有带方括号,那么程序就会崩掉。

因此,shared_ptr 类中提供了一个构造函数可以自定义一个删除器去指定析构函数的删除方式。这个自定义删除器可以是函数指针仿函数lamber,包装器

	template
	struct DelArr//仿函数
	{
		void operator()(const T* ptr){
			cout << "delete[]:" << ptr << endl;
			delete[] ptr;
		}
	};
	// 定制删除器 -- 删除器控制释放资源的方式
	void test_shared_ptr_deletor()
	{
		std::shared_ptr spArr(new int[10], DelArr());
		std::shared_ptr spfl(fopen("test.txt", "w"), [](FILE* ptr) {
			cout << "fclose:" << ptr << endl;
			fclose(ptr);
			});
	}

3.3.4  shared_ptr模拟实现:

	template
	struct Delete
	{
		void operator()(const T* ptr)
		{
			delete ptr;
		}
	};
	//引用计数
	template>
	class shared_ptr
	{
	private:
		void AddRef()
		{
			_pmutex->lock();//要保证引用计数的线程安全
			++(*_pcount);
			_pmutex->unlock();
		}

		void ReleaseRef()
		{
			_pmutex->lock();
			bool flag = false;
			if (--(*_pcount) == 0)
			{
				if (_ptr)
				{
					cout << "delete:" << _ptr << endl;
					//delete _ptr;
					_del(_ptr);
				}

				delete _pcount;
				flag = true;
			}
			_pmutex->unlock();

			if (flag == true)
			{
				delete _pmutex;//这里锁的释放要注意
			}
		}
	public:
		shared_ptr(T* ptr = nullptr)
			:_ptr(ptr)
			, _pcount(new int(1))
			, _pmutex(new mutex)
		{}

		shared_ptr(T* ptr, D del)
			: _ptr(ptr)
			, _pcount(new int(1))
			, _pmutex(new mutex)
			, _del(del)
		{}

		~shared_ptr()
		{
			ReleaseRef();
		}

		shared_ptr(const shared_ptr& sp)
			:_ptr(sp._ptr)
			, _pcount(sp._pcount)
			, _pmutex(sp._pmutex)
		{
			AddRef();
		}

		// sp1 = sp1// sp1 = sp2// sp3 = sp1;
		shared_ptr& operator=(const shared_ptr& sp)
		{
			if (_ptr != sp._ptr)
			{
				ReleaseRef();
				_pcount = sp._pcount;
				_ptr = sp._ptr;
				_pmutex = sp._pmutex;
				AddRef();
			}
			return *this;
		}

		// 可以像指针一样使用
		T& operator*(){
			return *_ptr;
		}
		T* operator->(){
			return _ptr;
		}
		int use_count(){
			return *_pcount;
		}
		T* get() const{
			return _ptr;
		}
	private:
		T* _ptr;
		int* _pcount;//计数
		mutex* _pmutex;
		D _del;//模拟实现定制删除器
	};

3.4 weak_ptr

不参与资源管理,不增加shared_ptr管理资源的引用计数,可以像指针一样使用

3.4.1 weak_ptr的模拟实现:

	template
	class weak_ptr
	{
	public:
		weak_ptr()
			:_ptr(nullptr)
		{}

		weak_ptr(const shared_ptr& sp)
			:_ptr(sp.get())
		{}

		weak_ptr& operator=(const shared_ptr& sp)
		{
			_ptr = sp.get();
			return *this;
		}
		// 可以像指针一样使用
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr;
	};

3.4.2 循环引用问题:

	//对list节点使用new delete,可能会忘记delete
    struct ListNode
	{
		ListNode* _next;
		ListNode* _prev;
		int _val;
		~ListNode()
		{
			cout << "~ListNode()" << endl;
		}
	};
	void test1()
	{
		ListNode* node1 = new ListNode;
		ListNode* node2 = new ListNode;
		node1->_next = node2;
		node2->_prev = node1;
		//...
		//delete node1;
		//delete node2;//为了防止遗忘delete,可以采用智能指针来管理节点
	}
	// 假设我们想用智能指针来进行管理上述节点
	// 若采用shared_ptr,会引发循环引用问题,weak_ptr可以解决
	struct ListNode
	{
		//std::shared_ptr _next;
		//std::shared_ptr _prev;//这个不行,下面的对象不能正常析构
		std::weak_ptr _next;
		std::weak_ptr _prev;//这样才可以

		int _val;
		~ListNode()
		{
			cout << "~ListNode()" << endl;
		}
	};
	void test_shared_ptr_cycle_ref()
	{
		std::shared_ptr node1(new ListNode);
		std::shared_ptr node2(new ListNode);

		cout << node1.use_count() << endl;//1
		cout << node2.use_count() << endl;//1
		// 循环引用
		node1->_next = node2;
		node2->_prev = node1;
		// ...
		cout << node1.use_count() << endl;//1
		cout << node2.use_count() << endl;//1
	}

 循环引用分析:

  • 1. node1和node2两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动delete。
  • 2. node1的_next指向node2,node2的_prev指向node1,引用计数变成2。
  • 3. node1和node2析构,引用计数减到1,但是_next还指向下一个节点。但是_prev还指向上一个节点。
  • 4. 也就是说_next析构了,node2就释放了。
  • 5. 也就是说_prev析构了,node1就释放了。
  • 6. 但是_next属于node的成员,node1释放了,_next才会析构,而node1由_prev管理,_prev属于node2成员,所以这就叫循环引用,谁也不会释放。(类似死锁)

C++11新特性(智能指针详细介绍)_第3张图片

4. 如何选择智能指针

在了解 STL 的四种智能指针后,大家可能会想另一个问题:在实际应用中,应使用哪种智能指针呢?下面给出几个使用指南。

  1. 如果程序要使用多个指向同一个对象的指针,应选择 shared_ptr。这样的情况包括:
    将指针作为参数或者函数的返回值进行传递的话,应该使用 shared_ptr;
    两个对象都包含指向第三个对象的指针,此时应该使用 shared_ptr 来管理第三个对象;
    STL 容器包含指针。很多 STL 算法都支持复制和赋值操作,这些操作可用于 shared_ptr,但不能用于 unique_ptr(编译器发出 warning)和 auto_ptr(行为不确定)。如果你的编译器没有提供 shared_ptr,可使用 Boost 库提供的 shared_ptr。
  2. 如果程序不需要多个指向同一个对象的指针,则可使用 unique_ptr。如果函数使用 new 分配内存,并返还指向该内存的指针,将其返回类型声明为 unique_ptr 是不错的选择。这样,所有权转让给接受返回值的 unique_ptr,而该智能指针将负责调用 delete。可将 unique_ptr 存储到 STL 容器中,只要对容器元素不使用拷贝操作的算法即可(如 sort())。
  3. 虽然说在满足 unique_ptr 要求的条件时,使用 auto_ptr 也可以完成对内存资源的管理,但是因为 auto_ ptr 不够安全,不提倡使用,即任何情况下都不应该使用 auto_ptr。
  4. 为了解决 shared_ptr 的循环引用问题,我们可以祭出 weak_ptr。
  5. 在局部作用域(例如函数内部或类内部),且不需要将指针作为参数或返回值进行传递的情况下,如果对性能要求严格,使用 scoped_ptr 的开销较 shared_ptr 会小一些。

5. 内存泄漏(补充)

5.1 什么是内存泄漏?

内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。

5.2 内存泄漏的危害

长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。

void MemoryLeaks()
 {
     // 1.内存申请了忘记释放
     int* p1 = (int*)malloc(sizeof(int));
     int* p2 = new int;
 
     // 2.异常安全问题
     int* p3 = new int[10];
 
     Func(); // 这里Func函数抛异常导致 delete[] p3未执行,p3没被释放.
 
     delete[] p3;
 }

5.3 内存泄漏的分类

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

5.4 如何避免内存泄漏

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

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

5.5 如何检测内存泄漏

在linux下内存泄漏检测: Linux下几款C++程序中的内存泄露检查工具_CHENG Jian的博客-CSDN博客_linux查看内存泄漏
在windows下使用第三方工具:
VS编程内存泄漏:VLD(Visual LeakDetector)内存泄露库_波波在学习的博客-CSDN博客
其他工具:
内存泄露检测工具比较 - 默默淡然 - 博客园

你可能感兴趣的:(C++学习,c++,开发语言,数据结构)