C++智能指针

文章目录

  • 为什么使用智能指针?
  • 智能指针的使用及原理
    • RAII技术
    • 智能指针基本特征
    • std::auto_ptr
    • std::unique_ptr
    • std::shared_ptr
      • 循环引用
    • weak_ptr
      • 定制删除器
  • C++11和boost中智能指针的关系

为什么使用智能指针?

因为程序设计错误有可能导致内存泄露.
例如: 以下代码中,当调用division函数抛出异常时,执行流直接跳到了main函数中的catch进行捕捉,此时,此时,new出来的数组array未能及时释放而造成内存泄露.

double division(int a, int b)
{
	// 当b == 0时抛出异常
	if (b == 0)
	{
		throw "division by zero condition!";
	}
	return (double)a / (double)b;
}
void func()
{
	// 这里可以看到如果发生除0错误抛出异常,另外下面的array没有得到释放。
	int* array = new int[10];
		int len, time;
		cin >> len >> time;
		cout << division(len, time) << endl;
		delete[] array;
	// ...
	cout << "delete []" << array << endl;
	delete[] array;
}
int main()
{
	try
	{
		func();
	}
	catch (exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

这里,我们可以通过抛异常的方式处理,调用divison函数时便对Division函数进行捕捉,捕捉时并不对该异常处理,而是先将Array处理,再重新抛出,让main函数中的catch捕捉处理.


double division(int a, int b)
{
	// 当b == 0时抛出异常
	if (b == 0)
	{
		throw "division by zero condition!";
	}
	return (double)a / (double)b;
}
void func()
{
	// 这里捕获异常后并不处理异常,而是先释放new出来的指针,异常还是交给外面处理,这里捕获了再重新抛出去。
	int* array = new int[10];
	try 
	{
		int len, time;
		cin >> len >> time;
		cout << division(len, time) << endl;
	}
	catch (...)
	{
		cout << "delete []" << array << endl;
		delete[] array;
		throw;
	}
	// ...
	cout << "delete []" << array << endl;
	delete[] array;
}

但是如果new出多个指针,每个指针都有可能都可能抛异常导致前面成功创建的指针不能成功释放,如果都使用抛异常处理未免过于繁琐,所以,智能指针油然而生.

智能指针的使用及原理

RAII技术

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

  • 首先,在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始保持有效.
  • 其次,在对象析构的时候释放资源.

RAII有两大好处:

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

智能指针基本特征

实现智能指针有三个主要特征:

  • RAII
  • 具有像指针一样的行为,可以进行解引用,可以通过->访问所指向空间的内容.
  • 能够灵活解决拷贝问题.

如何理解智能指针中的拷贝问题?

对于STL中的迭代器来说,浅拷贝对程序没什么问题,因为迭代器不参与管内存资源的释放,但是对于智能指来说,由于RAII机制,如果只是单纯的浅拷贝,那么在函数栈帧销毁时sp1和sp2会同时调用析构函数对同一块资源进行析构,这会造成程序崩溃.
C++智能指针_第1张图片

std::auto_ptr

auto_ptr的基本原理

auto_ptr会将资源管理权进行转移,这将导致被拷贝对象悬空(变为空指针),很多公司明确不能使用auto_ptr.
如图:当ap1拷贝构造ap2时,ap2指向原来ap1管理的内存资源,ap1悬空.
C++智能指针_第2张图片

auto_ptr的基本模拟实现

namespace yzh
{
	template < class T>
	class auto_ptr
	{
	public:

		//构造.
		auto_ptr(T* ptr = nullptr)
			:_ptr(ptr)
		{}
        //拷贝构造
		auto_ptr(auto_ptr<T>& ap)
			:_ptr(ap._ptr)                      //将管理资源转移.
		{
			cout << "拷贝构造" << endl;
			ap._ptr = nullptr;                  //将被拷贝指针悬空.
		}
        //析构
		~auto_ptr()
		{
			if (_ptr)
			{
				cout << "~auto_ptr()" << endl;
				delete _ptr;
			}
		}

		// ->操作符重载.
		T* operator->()
		{
			return _ptr;
		}
		// * 操作符重载.
		T& operator*()
		{
			return *_ptr;
		}
	private:
		T* _ptr;
	};

auto_ptr的打赋值重载

  • 如果被赋值对象已有指向对象,则必须先析构该资源.
  • 再将赋值对象的指针赋值给被赋值对象的指针.
  • 最后将赋值对象的指针置为nullptr.
auto_ptr1<T>& operator=(auto_ptr1<T>& ap)
		{
			if (&ap != this)          //对自己给自己赋值进行判断.
			{
				if (_ptr)
				{
					cout << "delete _ptr " << endl;

					delete _ptr;
				}
				_ptr = ap._ptr;
				ap._ptr = nullptr;
				return *this;
			}
		}

注意:
对于auto_ptr来讲,如果自己给自己赋值,由于赋值时会将原先的管理的资源销毁,并将指针置为空,这显然并不是用户所希望看到的,所以在赋值之前必须先进行判断将该情况排除.

std::unique_ptr

unique_ptr的基本原理

unnique_ptr就是在auto_ptr的基础上不写拷贝构造和赋值重载,再利用delete关键字可防止编译器默认生成拷贝构造和赋值重载.可以简单而粗暴的防拷贝.

unique_ptr(unique_ptr<T>& ap) = delete;
unique_ptr<T>& operator=(unique_ptr<T>& ap) = delete;

std::shared_ptr

shared_ptr的基本原理

通过引用计数的方式实现来实现多个shared_ptr对象共享资源.

  • shared_ptr在其内部,都维护了一份引用计数,用来记录该资源被多少个对象共享.
  • 当对象被销毁时(调用析构函数),此时该引用计数减一.
  • 如果该引用计数=0,就说明自身是最后一个管理该资源的智能指针,必须先将该资源销毁.(防止内存泄露).
    C++智能指针_第3张图片
    C++智能指针_第4张图片

那么该引用计数类型可以为静态变量嘛?

  • 不可以,因为静态变量是同一个类中所有对象共享的.
  • 如果当shared-ptr sp1,sp2指向同一内存资源A(对象)时,此时引用计数为2,可是如果当再创建一个sp3,但是指向的内存为数据类型int,此时对于所有智能指针而言,引用计数就变成了1,这完全与shared_ptr设计思想相违背.
    C++智能指针_第5张图片

shared_ptr的基本实现

namespace yzh
{
	template < class T>
	class shared_ptr
	{
	public:

		//构造
		shared_ptr(T* ptr = nullptr)
			:_ptr(ptr)
			, _pCount(new int(1))   //刚创建的引用计数值为1.
		{}

		//拷贝构造
		shared_ptr(shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			, _pCount(sp._pCount)
		{
			(*_pCount)++;        //拷贝构造引用计数加1.
		}

		~shared_ptr()           //智能指针销毁自动调用析构函数
		{
			if (--(*_pCount) == 0)   //当引用计数值为0时,说明该指针为最后一个指向目标资源的对象,可以对该
				                     //资源销毁.
			{
				if (_ptr != nullptr)
				{
					cout << "~shared_ptr()" << endl;
					delete _ptr;
					_ptr = nullptr;
				}
				delete _pCount;
				_pCount = nullptr;
			}
		}

		int* get_cPount()
		{
			return _pCount;
		}
 
        T* get()
        {
            return _ptr;
        }
        
		//像指针一样.
		T* operator->()
		{
			return _ptr;
		}

		T& operator*()
		{
			return *_ptr;
		}
		//	private:
		T* _ptr;
		int* _pCount;
	};
}

赋值重载的基本实现

预期实现:

shared_ptr<T>& operator=(shared_ptr& sp)
		{
			
			if (--(*_pcount == 0))
			{
				delete _ptr;
				delete _pcount;
			}
			_ptr = sp._ptr;
			_pcount = sp._pcount;

			return *this;
		}

在实现前,我们要考虑两种情况.

  • 自己给自己赋值.(同一个对象赋值),此时由于引用计数由1变0,此时p1指向的内存资源A和引用计数_pCount都已经被销毁了,这显然不符合赋值重载的理念.
    C++智能指针_第6张图片

  • sp1和sp2两个对象的智能指针赋值,但是它们指向同一块内存资源,此时相当于只对(*_pCount)–后,再++,并没有什么实质性的改变.
    C++智能指针_第7张图片
    所以,我们不能只针对情况一,判断两个智能指针是否为同一对象.我们要根据两个情况的共性来判断,不管为哪种情况,赋值的两个指针指针一定是在指向同一块内存资源A的条件下,所以,如果指向同一块内存资源,则不需要赋值,直接返回.

shared_ptr<T>& operator=(shared_ptr& sp)
		{
			if (_ptr == sp._ptr)   //如果指向相同资源就不做处理.
			{
				return *this;
			}
			if (--( * _pCount) == 0 ) //如果引用计数为0,要先将目标资源销毁.
			{
				delete _ptr;
				delete _pCount;
			}
			_ptr = sp._ptr;
			_pCount = sp._pCount;

			return *this;
		}

循环引用

我们知道main函数结束之后内存还是要释放的,但是在函数栈帧结束之前还是要尽可能的释放内存,因为很多项目都是长期运行的,防止有可能造成僵尸内存.
可是如果n1中的_next指向结点2,n2中的_prev指向结点1时,当函数栈帧结束的时候,n2先析构,n1后析构,引用计数变为1.
此时便会面临尴尬的情况:

  • 结点1的释放取于结点2调用自身的析构函数.
  • 结点2的释放取决于结点1调用自身的析构函数.
    由此,结点1和结点2结点的释放都与对方紧密相关,所以最后,编译器也无法判断到底先释放哪一个结点,进而导致了内存泄露.
    C++智能指针_第8张图片
    测试代码如下:
struct Node
{
	int _n1;
	int _n2;
	yzh::shared_ptr<Node> _prev;
	yzh::shared_ptr<Node> _next;

	~Node()
	{
		cout << "~Node()" << endl;
	}
};
void test_shared_ptr()
{
	yzh::shared_ptr<Node> n1(new Node);

	yzh::shared_ptr<Node> n2(new Node);

	n1->_next = n2;

	n2->_prev = n1;
}
int main()
{
  //test_auto_ptr();

	test_shared_ptr();
	
  return 0;
}

weak_ptr

在C++11中,为了解决shared_ptr所造成的循环引用问题,特地引入了weak_ptr,weak_ptr没有引用计数,并且不参与资源的管理.

weak_ptr的基本实现

  • weak_ptr提供一个无参的构造函数.
  • 支持能够用shared_ptr对象拷贝构造weak_ptr.
  • 能够像指针一样解引用( * ),->.
	template < class T >
	class weak_ptr
	{
	public:
	//无参构造.
		weak_ptr()
			:_ptr(nullptr)
		{}
   //shared_ptr的拷贝构造weak_ptr
		weak_ptr(const shared_ptr<T>& sp)
			:_ptr(sp.get())
		{}
		weak_ptr<T>& operator=( shared_ptr<T>& sp)
		{
			_ptr = sp.get();
			return *this;
	    }
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr;
	};
}

测试代码如下:
此时n1(智能指针)指向第一个结点,n2指向后一个结点,但是结点中的_prev,_next由原先的(shared_ptr转变为weak_ptr),而又因为weak_ptr不参与资源的管理,不决定结点的销毁,所以这时就不会造成两个结点之间的销毁由对方结点是否先销毁而决定的情况,由于结点2比结点1先创建,在函数栈帧销毁的过程中,结点2比结点1先销毁.

int main()
{
	//yzh::shared_ptr sp1(new Node);
	
	yzh::shared_ptr<Node> n1(new Node);

	yzh::shared_ptr<Node> n2(new Node);

	n1->_next = n2;
	
	n2->_prev = n1;
	
}

定制删除器

new和delete类型不匹配导致的问题

在visual编译器的条件下,如果new方式和delete方式不匹配,对于内置类型没有影响,对于自定义类型会导致程序的崩溃.

int main()
{
	std::shared_ptr<Node> n1(new Node);
	
	std::shared_ptr<Node> n2(new Node);

	//对于内置类型,new[]类型使用delete没有问题.
	std::shared_ptr<int> n3(new int[5]);

	//对于自定义类型Node,new[]类型使用delete会导致程序异常.
	std::shared_ptr<Node> n4(new Node[5]);
}

new和delete类型不匹配造成程序崩溃的原因

  • 对于内置类型,new Int[]调用的是operator new[],operator new[]调用的是operator new,operator new调用的是malloc. 使用delete释放,delete调用的是free.
  • 而对于自定义类型来说,例如:Node自定义类型需要调用的是构造函数和析构函数,但是,对于delete[],其并没有告知编译器调用几次析构函数,所以在vsual编译器的环境下,当new自定义类型[]时,会额外在之前生成自定义类型地址ptr前加上一个内置类型int的计数,此时返回的Node地址实际上就为ptr-4.然后delte该地地址,然后根据计数调用析构函数和free.
  • 此时,如果使用delete删除,它默认只删除一个自定义对象,所以只调用一次析构函数.并且delete实际上删除的地址ptr不是自定义对象本身的地址.此时就会导致程序崩溃,所以写代码一定要配合,严谨,规范.
  • C++智能指针_第9张图片

删除定制器的解决办法

一:传递特定的仿函数.

//传仿函数定制删除器
template <class T>
struct DeleteArray
{
	void operator()(T* ptr)
	{
		cout << "delete[]" << endl;
		delete[] ptr;
	}
};
template <class T>
struct Free
{
	void operator()(T* ptr)
	{
		cout << "delete[]" << endl;
		free(ptr);
	}
};
int main()
{
	//调用析构函数,delete.
	std::shared_ptr<Node> n1(new Node);

	//调用free.
	std::shared_ptr<int> n2(new int[5], Free<int>());

	//仿函数,delete[].
	std::shared_ptr<Node> n3(new Node[5], DeleteArray<Node>());
    
    std::shared_ptr<int> n4((int*)malloc(sizeof(5)),Free<int>());
}

二: 传lambda表达式

int main()
{
	//调用析构函数,delete.
	std::shared_ptr<Node> n1(new Node);

	//调用free.
	std::shared_ptr<int> n2(new int[5], [](int* ptr) { free(ptr); });
	//仿函数,delete[].
	std::shared_ptr<Node> n3(new Node[5], [](Node* ptr) { delete[] ptr; });
   
	std::shared_ptr<int> n4((int*)malloc(sizeof(5)), [](int* ptr) {free(ptr); });
}

C++11和boost中智能指针的关系

  • C++ 98 中产生了第一个智能指针auto_ptr.
  • C++ boost给出了更实用的scoped_ptr和shared_ptr和weak_ptr.
  • C++ TR1,引入了shared_ptr等。不过注意的是TR1并不是标准版。
  • C++ 11,引入了unique_ptr和shared_ptr和weak_ptr。需要注意的是unique_ptr对应boost
    的scoped_ptr。并且这些智能指针的实现原理是参考boost中的实现的。

你可能感兴趣的:(c++,开发语言)