C++11——智能指针

0.前言

在C++异常处理时,当程序抛出异常时,程序会直接跳转到最近的捕获区域进行异常的处理,这种处理方式保证了程序的不崩溃,只针对一个区域内的某种错误进行处理。

这样的代码看似很正常,但往往会忽略掉某些内存的处理。例如我们在某个函数调用前向内存申请了部分空间,本来内存的释放逻辑是在函数调用之后进行完成,但是如果函数内部出现异常错误,函数将直接跳转,申请的内存无法释放,就造成了内存泄漏的风险

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

我们首先分析如下代码:

int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");
	return a / b;
}
void func()
{
	int* p1 = new int[10]; // 这里可能会抛异常,此时抛异常内存会分配失败,程序结束后不会造成内存泄漏。
	int* p2 = new int[10]; // 这里可能会抛异常,此时抛异常会导致p2以及后面的内存分配失败,退出程序后,
	                       //由于p1已经成功申请内存,但是C++没有内存回收机制,因此会造成内存泄漏。
	int* p3 = new int[10]; // 这里可能会抛异常

	try
	{
		div();
	}
	catch (...)
	{
		delete[] p1;
		delete[] p2;
		delete[] p3;
		throw;
	}
	delete[] p1;
	delete[] p2;
	delete[] p3;
}

int main()
{
	try
	{
		func();
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
		// ...
	}

	return 0;
}

为了避免出现内存泄漏的情况,一般对于出现异常问题的异常处理函数中,需要对目前进程中已经分配的内存进行内存释放,如上示代码中的div( )函数所示。但实际项目开发过程中很多人会忘记在适当地方加上delete语句。

在当前的运行环境中,函数调用完毕后进程也就结束了,即便我们没有释放的内存也会在进程结束时一起归还给操作系统,因此这种内存泄漏并没有什么大的风险。 但是,服务器上的程序是一旦开机几乎不会停止的进程,如果我们的内存一直在泄漏,那么我们的机器将会越来越慢,最终因为内存不够而导致进程崩溃

因此总结认为智能指针就是用来解决内存泄漏问题的。

2.什么是智能指针?

我们将指针托管给某个对象,在对象构造时获取资源,在对象析构时释放资源。这样我们的资源不用再手动进行释放,对象所需要的资源在生命周期内始终有效。智能指针是一种对普通指针进行构造的模板类,使用我们提供的类进行指针的托管。

实现方法可以参考下面类似代码:

template
class SmartPtr
{
public:
	SmartPtr(T* pt)//构造函数
		:_ptr(pt)
	{}
	~SmartPtr()//析构函数,释放内存空间
	{
        cout<<"delete "<() { return _ptr ;}
private:
	T* _ptr;
};

3.智能指针—auto_ptr

 先观察如下的赋值语句:

auto_ptr< string> p1 (new string("I reigned lonely as a auto_ptr.”);
auto_ptr<string> p2;
p2= p1;

如果p1p2是常规指针,则两个指针将指向同一个string对象。程序将试图删除同一个对象两次——一次是p1过期时,另一次是p2过期时。要避免这种问题,可以有多种方法:

  • 定义赋值运算符:执行深拷贝,使得两个指针指向不同的对象,缺点是浪费空间
  • auto_ptr智能指针:auto_ptr智能指针在进行赋值时,将p1指针的所有权转交给p2指针,p1指针指向nullptr

避免使用auto_ptr,因为当p1的指针所有权装给p2指针后,p1指针指向空,当再次访问指针时将会引发错误!!!一句话总结就是:避免潜在的内存崩溃问题

4.智能指针—unique_ptr

下面来看使用unique_ptr的情况:

unique_ptr< string> p1 (new string("I reigned lonely as a unique_ptr.”);   //#1
unique_ptr<string> p2;                                                                                 //#2
p2= p1;                                                                                                         //#3

编译器认为语句#3非法,避免了p1不再指向有效数据,留下危险的悬挂指针的问题。因此,unique_ptr auto_ptr更安全。

unique_ptr 还有更聪明的地方,可以将一个临时的智能指针赋值给另一个unique_ptr指针,举例代码如下:

unique_ptr<string> demo(const char * s)
{
    unique_ptr<string> temp (new string(s));
    return temp;
}
int main()
{
    unique_ptr<string> ps;
    ps = demo('Uniquely special");
}

demo()返回一个临时unique_ptr ,然后ps接管了原本归返回的unique_ptr 所有的对象,而返回时临时的 unique_ptr 被销毁,也就是说没有机会使用 unique_ptr 来访问无效的数据,换句话来说,这种赋值是不会出现任何问题的,即没有理由禁止这种赋值。

unique_ptr<string> pu1(new string ("hello world"));
unique_ptr<string> pu2;
pu2 = pu1;                                                         // #1 not allowed
unique_ptr<string> pu3;
pu3 = unique_ptr<string>(new string("You"));   // #2 allowed

其中#1留下悬挂的unique_ptr(pu1),这可能导致危害。而#2不会留下悬挂的unique_ptr ,因为它调用 unique_ptr 的构造函数,该构造函数创建的临时对象在其所有权让给 pu3 后就会被销毁。

您可能确实想执行类似于#1的操作,要安全的重用这种指针,可给它赋新值。C++有一个标准库函数std::move()使用move后,原来的指针仍转让所有权变成空指针,让你能够将一个unique_ptr赋给另一个

下面是一个使用前述demo()函数的例子,该函数返回一个unique_ptr 对象:

unique_ptr<string> ps1, ps2;
ps1 = demo("hello");
ps2 = move(ps1);
ps1 = demo("alexia");
cout << *ps2 << *ps1 << endl;

5.智能指针—share_ptr

shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。引用计数支持多个拷贝管理同一个资源,最后一个析构对象释放资源。

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

std::shared_ptr的循环引用

案例如下:

struct ListNode
{
	int _data;
	shared_ptr _prev;
	shared_ptr _next;
	~ListNode() 
	{
		cout << "~ListNode()" << endl;
	}
};
int main()
{
	shared_ptr node1(new ListNode);//此时node1引用计数为1
	shared_ptr node2(new ListNode);//此时node2引用计数为1

	cout << node1.use_count() << endl;
	cout << node2.use_count() << endl;

	node1->_next = node2;//此时node2引用计数为2
	node2->_prev = node1;//此时node1引用计数为2
	cout << node1.use_count() << endl;
	cout << node2.use_count() << endl;
    //到了此处node1和node2对象不再使用,调用析构函数释放node1、node2。
    //此时node1和node2的引用计数为1,但是还是无法释放
	return 0;
}

  • node1和node2两个智能指针对象指向两个节点,node1和node2的引用计数变成1。
  • node1的_next指向node2,node2的_prev指向node1,node1和node2的引用计数变成2。
  • node1和node2析构,引用计数减到1,但是node1->_next还指向node2节点。node2->_prev还指向node1节点。
  • 即是说node1->_next析构了,node2才能被释放,node2->_prev析构了,node1才能被释放。
  • 但是node1->_next属于node1的成员,node1释放了,node1->_next才会析构,而node1由node2->_prev管理,node2->_prev属于node2成员,所以这就叫循环引用,谁也不会释

解决方法是在引用计数的场景下,把节点中的_prev和_next成员改成weak_ptr。此时:

node1->_next = node2;
node2->_prev = node1;
weak_ptr的_next和 _prev不会增加node1和node2的引用计数。

6.智能指针—weak_ptr

std::weak_ptr 要与 std::shared_ptr 一起使用。 一个 std::weak_ptr 对象看做是 std::shared_ptr 对象管理的资源的观察者,它不影响共享资源的生命周期。

  •     如果需要使用 weak_ptr 正在观察的资源,可以将 weak_ptr 提升为 shared_ptr
  •     当 shared_ptr 管理的资源被释放时,weak_ptr 会自动变成 nullptr

各类智能指针的具体实现代码可以参考:C++智能指针详解_李 ~的博客-CSDN博客_c++ 智能指针定义

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