学习笔记------智能指针

测试环境为:Windows/VS2017

前言

       最近在学习的过程中,遇到了智能指针这个知识点,之前只知道智能指针能够自动释放资源,但是对于其它的细节都一无所知,今天就来研究一下智能指针。
       智能指针,运用了RAII(Resource Acquisition is initialization)的思想,简单来说就是,使用一个对象,在其构造时获取资源,在对象生命期控制对资源的访问使之始终保持有效,最后在对象析构的时候释放资源。说白了就是:将基本类型指针封装为类对象指针(这个类肯定是个模板,以适应不同基本类型的需求),在构造函数中,通过赋值等操作,让该指针管理资源,并在析构函数里编写delete语句释放指针指向的内存空间。
       那么,为什么要有智能指针呢?我们来看一段代码。

int main() 
{
    ch a = 0;
    FILE *pOut = fopen("test.txt", "wb");
    if (pOut == nullptr) {
        cout << "error for pOut" << endl;
        exit(0);
    }
    cin >> a;
    if (a != 'A') {
        return 0;
    }
    fclose(pOut);
    return 0   
}

这段代码有什么问题呢?在if 的判断语句中,少写了fclose(pOut), 如果在判断输入的a 是不是 A的时候,直接return 了,那么就会存在内存泄露的问题,有的人可能会说,这些基本的东西怎么可能会忘记呢?但是智者千虑必有一失,有时可能只是因为一点点的疏忽,就会导致问题的产生。因此,智能指针的出现是很必要的。
       STL中为我们提供了四种类型的智能指针:
       auto_ptr    :管理权限的转移
       unique_ptr:唯一管理,防止拷贝
       shared_ptr:引用计数机制
       weak_ptr    :weak_ptr 不能单独使用,只能在shared_ptr 中使用,具体的使用,我们在后面再介绍。
       我们可以模拟STL库中智能指针最基本的思想,当我们再遇到上面代码的问题时,就很好处理了:

template<class T>
class AutoPtr
{
public:
    AutoPtr(T* ptr = nullptr)
        :_ptr(ptr)
    {}

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

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

    ~AutoPtr()
    {
        if (_ptr) {
            delete _ptr;			//不考虑其他类型的资源,假设都是new出来的
        }
    }
private:
    T* _ptr;
};

       STL中的智能指针的最基本的思想就类似于上面的这种,不同的地方是,STL库中的智能指针都进行了各种功能的增强,以及各种缺陷的解决,我们来依次剖析。

auto_ptr

C++98版本的库中提供了auto_ptr,但在C++11中被废弃,同时不推荐使用它。
首先,我们先看一下auto_ptr 的文档介绍:
在这里插入图片描述
文档中介绍,当两个 auto_ptr 对象之间发生赋值操作时,将转移所有权,这意味着失去所有权的对象被设置为不再指向该元素(它被设置为空指针)。
因此,我们可以知道 auto_ptr 的原理实际为:管理权限的转移。我们可以使用一段代码来验证它。

#include <iostream>
#include <memory>
using namespace std;
int main()
{
	auto_ptr<int> autoPtr(new int(5));
	cout << *autoPtr << endl;
	
	auto_ptr<int> autoPtrCopy1(autoPtr);		//用第一个auto_ptr对象构造第二个
	cout << autoPtr.get() << endl;
	cout << *autoPtrCopy1 << endl;

	auto_ptr<int> autoPtrCopy2 = autoPtrCopy1;		//用第二个给第三个赋值
	cout << autoPtrCopy1.get() << endl;
	cout << *autoPtrCopy2 << endl;
	
	return 0;
}

运行结果:
在这里插入图片描述
我们通过调试和监视来看在程序运行的过程中发生了什么。
在这里插入图片描述
我们可以看到:
       当用autoPtr 去构造 autoPtrCopy1 时,autoPtr 所管理的资源被转移到 autoPtrCopy1 上,并且autoPtr 被置为空。
       当autoPtrCopy1 赋值给autoPtrCopy2 时,autoPtrCopy1 所管理的资源被转移到 autoPtrCopy2 上,并且autoPtrCopy1 被置为空。

这种转移管理权限的做法原理其实就是:每份资源只能由一个auto_ptr 来管理。但是在本版本的auto_ptr 中,并没有对这种机制进行严格的限制,只是告诉编程人员当进行管理权转移的操作后,原来的指针会被置为空,具体的操作需要编程人员自行处理。这样就有可能产生对空指针操作的问题。
我们在代码中增加逻辑判断,就可以解决对空指针解引用的问题:

#include <iostream>
#include <memory>
using namespace std;
int main()
{
	auto_ptr<int> autoPtr(new int(5));
	cout << *autoPtr << endl;
	auto_ptr<int> autoPtrCopy1(autoPtr);		//用第一个auto_ptr对象构造第二个
	if (autoPtr.get() != nullptr) {
		//auto_ptr 为我们提供了get() 接口,让我们来自行判断当前指针是否管理了资源。
		cout << *autoPtr << endl;
	}
	cout << autoPtr.get() << endl;
	cout << *autoPtrCopy1 << endl;
	auto_ptr<int> autoPtrCopy2 = autoPtrCopy1;		//用第二个给第三个赋值
	if (autoPtrCopy1.get() != nullptr) {
		//auto_ptr 为我们提供了get() 接口,让我们来自行判断当前指针是否管理了资源。
		cout << *autoPtrCopy1 << endl;
	}
	cout << autoPtrCopy1.get() << endl;
	cout << *autoPtrCopy2 << endl;
	return 0;
}

同时,auto_ptr 还提供了其他的接口:
在这里插入图片描述
release 可以将auto_ptr 对象与其管理的资源断开联系,即将auto_ptr 内部指针设置为空,但是不影响它管理的资源,说简单点就是不使用auto_ptr 管理资源了。当使用这个接口后,可能就需要手动释放资源了,因此应该小心内存泄漏问题
在这里插入图片描述
测试代码:

#include <iostream>
#include <memory>
using namespace std;
int main()
{
	int* p = new int(5);
	auto_ptr<int> autoPtr(p);
	cout << *p << endl;
	cout << *autoPtr << endl;
	autoPtr.release();
	cout << "autoPtr.release();" << endl;
	cout << *p << endl;
	cout << autoPtr.get() << endl;
	return 0;
}

运行结果:
在这里插入图片描述
reset ,实现的功能为:释放auto_ptr 管理的资源,同时,如果指定了新的资源,就指向新的资源,否则指向空。
在这里插入图片描述
测试代码:

#include <iostream>
#include <memory>

using namespace std;
int main()
{
	int* p = new int(5);
	auto_ptr<int> autoPtr(p);
	cout << *p << endl;
	cout << *autoPtr << endl;
	cout << "autoPtr.reset();" << endl;
	autoPtr.reset();		//本次 reset 没有传参
	cout << *p << endl;
	cout << autoPtr.get() << endl;
	cout << "autoPtr.reset(new int(6));" << endl;		
	autoPtr.reset(new int(6));	//本次reset 重新传入了一个值
	cout << *autoPtr << endl;
	return 0;
}

运行结果:
在这里插入图片描述
这里的p 在第一次reset 的时候就已经被释放了,已经是野指针了,因此再解引用是不对的。

unique_ptr

我们在前面说过,auto_ptr 的思想是:资源只能被一个auto_ptr 对象管理,也就是管理权的转移。同时,它也在文档中说明,当进行了管理权的转移后,原本的对象将被置为空。至于后续的对原对象的操作,则需要编程人员自行进行判断和处理。这种处理方式就有可能导致对空指针进行操作,进而引起内存奔溃的问题。
因此,在C++11中,提供了一种新的智能指针unique_ptr ,首先,我们先了解一下unique_ptr 的介绍。
在这里插入图片描述
我们可以看到,在unique_ptr 中,对资源唯一管理性的理念更明确了。即,每份资源,只允许一个unique_ptr 对象来管理。

#include <iostream>
#include <memory>
using namespace std;
int main()
{
	unique_ptr<int> autoPtr(new int(5));
	cout << *autoPtr << endl;
	unique_ptr<int> autoPtr1(autoPtr);
	unique_ptr<int> autoPtr2(autoPtr);
	return 0;
}

运行结果:
在这里插入图片描述
通过错误提示我们可以知道,库中删除了拷贝构造函数和同类对象的赋值运算符重载,这样就不会存在资源管理转移的问题,那也就不会有对空指针操作的问题了。
我们可以查看unique_ptr 的拷贝构造函数和同类对象的赋值运算符重载在库中的实现:
在这里用到了C++11的语法,直接删除掉了函数:
在这里插入图片描述
在这里插入图片描述
这里需要注意的是,在unique_ptr 中还有一个特别的地方。
测试代码:

#include <iostream>
#include <memory>
using namespace std;
unique_ptr<int> fun1()
{
	unique_ptr<int> a(new int(5));
	cout << a.get() << endl;
	return a;
}
int main()
{
	unique_ptr<int> a = fun1();
	cout << a.get() << endl;
	return 0;
}

运行结果:
在这里插入图片描述
两个管理的是同一片空间。

我们再来看unique_ptr 提供的其他接口:
在这里插入图片描述

unique_ptr 中,引入了删除器的思想:
在这里插入图片描述
因为不同类型的资源的释放方法是不同的,因此不能只用delete来进行删除 ,应该针对每种类型的资源,都创建自己的删除函数,而编译器并不知道用户指向的资源是什么类型,因此,就多添加一个参数,用来将删除方法传入,这里的传参方法是仿函数的方法。在unique_ptr 中,使用的默认的删除方法是 delete 。因此,如果是其它类型的资源就需要自己定义删除函数,然后在定义unique_ptr 对象的时候,一同传入。
在这里插入图片描述
这个删除函数会在 unique_ptr 的析构函数中调用。同时可以使同 get_deleter() 接口实现单独访问。
在这里插入图片描述
测试用例:

#include <iostream>
#include <memory>
using namespace std;
struct delete_ptr_malloc
{	
	delete_ptr_malloc()
	{}
	template<class T>
	void operator()(T *p) {
		if (p) {
			cout << "*p == "  << *p << "   free p" <<  endl;
			free(p);
		}
	}
};
int main()
{
	int* a = (int*)malloc(sizeof(int));
	*a = 5;
	unique_ptr<int, delete_ptr_malloc> autoPtr(a);		//先让autoPtr 管理 a
	cout << *autoPtr << endl;				
	int* b = (int*)malloc(sizeof(int));
	*b = 6;
	autoPtr.reset(b);						//对autoPtr 进行reset  让它管理 b 
	cout << *autoPtr << endl;				//输出新的值


	int* c = (int*)malloc(sizeof(int));		
	*c = 7;
	autoPtr.get_deleter()(c);				//直接通过 autoPtr.get_deleter() 单独访问
	
	return 0;
}

运行结果:
在这里插入图片描述
unique_ptr 重载了bool 运算符,可以直接用unique_ptr 对象进行逻辑判断,判断的依据为unique_ptr 指向的资源是否为空。
在这里插入图片描述
测试用例:

#include <iostream>
#include <memory>
using namespace std;
int main()
{
	unique_ptr<int> autoPtr(new int(5));
	//借助reset 完成操作,reset 的默认参数为空,即释放原来的空间,并指向空
	autoPtr.reset();	
	if (autoPtr) {
		cout << "autoPtr is not empty" << endl;
	}
	else {
		cout << "autoPtr is empty" << endl;
	}
	return 0;
}

运行结果:
在这里插入图片描述
releasereset 的功能与auto_ptr 中的功能相似,不同之处在于reset 中会调用我们传入的删除器,这里不再过多介绍。

swap 函数顾名思义,它实现的功能就是两个unique_ptr 管理资源的交换,交换完成后,两个对象管理的资源都变成了之前对方管理的资源。
在这里插入图片描述
测试用例:

#include <iostream>
#include <memory>
using namespace std;
int main()
{
	unique_ptr<int> autoPtr0(new int(5));
	unique_ptr<int> autoPtr1(new int(6));
	cout << "*autoPtr0 = " << *autoPtr0 << endl;
	cout << "*autoPtr1 = " << *autoPtr1 << endl;
	autoPtr0.swap(autoPtr1);
	cout << "autoPtr0.swap(autoPtr1);" << endl;
	cout << "*autoPtr0 = " << *autoPtr0 << endl;
	cout << "*autoPtr1 = " << *autoPtr1 << endl;
	return 0;
}

运行结果:
在这里插入图片描述
这里需要注意的是,交换的两个对象管理的资源必须是同类型的

shared_ptr

通过上面的介绍,我们知道C++11中引入了unique_ptr ,它的原理是保证管理同一份资源的 unique_ptr 始终只有一个。这样虽然可以起到管理资源的作用,但是如果对于应该有多个指针维护的资源,就会产生问题,因此,C++11中还引入了另外一种智能指针,即shared_ptr,它与unique_ptr 不同的是,它支持多个 shared_ptr 管理同一份资源,只有当所有管理那份资源的shared_ptr 对象都被销毁的时候,才会释放资源。
首先,我们了解一下shared_ptr 的说明:
在这里插入图片描述
shared_ptr 实现的原理很简单,就是引入了计数器,每当有一个指针指向资源的时候,计数器的计数就会增加,当需要销毁对象时,如果计数器的值为0,那么就释放资源,否则计数器的值减一。

#include <iostream>
#include <memory>
using namespace std;
int main()
{
	shared_ptr<int> autoPtr(new int(5));
	cout << "autoPtr.use_count() = " << autoPtr.use_count() << endl;
	cout << "autoPtr.get() = " << autoPtr.get() << endl;
	shared_ptr<int> autoPtr1 = autoPtr;
	//让autoPtr1 也管理 autoPtr 管理的资源
	cout << "\nshared_ptr<int> autoPtr1 = autoPtr" << endl;
	cout << "autoPtr.get() = " << autoPtr.get() << endl;
	cout << "autoPtr1.get() = " << autoPtr1.get() << endl;
	cout << "autoPtr.use_count() = " << autoPtr.use_count() << endl;
	cout << "autoPtr1.use_count() = " << autoPtr1.use_count() << endl;
	autoPtr.reset();		
	cout << "\nautoPtr.reset()" << endl;
	//reset  会释放智能指针管理的资源,并指向新的资源(新资源默认为空)
	//但是在shared_ptr 中 还需要考虑count 的值
	//因为count的值为2,因此autoPtr只会指向空,并不会释放资源
	cout << "autoPtr.get() = " << autoPtr.get() << endl;
	cout << "autoPtr1.use_count() = " << autoPtr1.use_count() << endl;
	cout << "*autoPtr1 = " << *autoPtr1 << endl;
	return 0;
}

我们这里,用autoPtrautoPtr1 管理了同一份资源,当我们对 autoPtr 进行 reset 操作时,autoPtrcount 会进行 减一 操作,并判断是否为0,这里判断的结果为 1 。因此,只是解除autoPtr 对 资源的管理,并且让autoPtr 管理新的资源,因为这里没有传入新的资源,因此,autoPtr 指向默认的空。
运行结果:
在这里插入图片描述
我们再来了解shared_ptr 提供的其他接口:
在这里插入图片描述
shared_ptr 的 接口大部分与前两种相似,因此这里不做过多解释,我们需要对shared_ptr 中新添加的接口进行了解。
use_count 因为shared_ptr 中引入了计数器,因此这个接口的作用就是返回,当前对象管理的资源共被几个同类型对象管理。
unique 判断当前shared_ptr管理的资源是否只被当前对象一个管理,如果是的话,返回true,否则返回false。
owner_before 这个函数暂时还没有研究懂,等后面再更新
在这里插入图片描述
以上就是shared_ptr 几种常见的接口,在shared_ptr的构造函数中还有另外的很多种构造方式,等到使用时,具体查询库函数即可。
在这里插入图片描述
根据shared_ptr 的介绍来看,我们知道它是可以用多个对象来维护同一份资源,我们把它应用到具体的代码中来,假设我们用shared_ptr管理一个双向链表:

#include <iostream>
#include <memory>
using namespace std;
struct Node
{
	int a;
	shared_ptr<struct Node> next;
	shared_ptr<struct Node> prev;
	~Node()
	{
		cout << "~Node()" << endl;
	}
};
int main()
{
	shared_ptr<struct Node> n1(new Node);
	shared_ptr<struct Node> n2(new Node);
	cout << n1.use_count() << endl;
	cout << n2.use_count() << endl;
	n1->next = n2;
	n2->prev = n1;
	cout << n1.use_count() << endl;
	cout << n2.use_count() << endl;
	return 0;
}

运行结果:
在这里插入图片描述
等到链表指完之后,我们可以看到每个节点的use_count 都为2,我们画一个图理解一下:
在这里插入图片描述
等到函数到达结尾,准备析构的时候,首先判断 n2n2use_count21 后 不进行析构,这样 n2 的成员也就不进行析构,直接退出。再判断 n1n1use_count21 后 不进行释放,n1 的成员也不进行析构,直接退出。这样就形成了以下这种情况:n1n2 进行析构,它们的 use_count 变为1 ,也就是,只是还有n1->next 指向 n2n2->prev 指向 n1 ,这时,只要n1->next 析构完成了,n2 就析构了,只要n2->prev 析构完成了,n1 就析构了。但是,它们各自的成员变量,必须是在它们自己析构之后,才能释放。这样到最后两个对象都没有释放。这就是循环引用问题。这个问题该如何解决呢?

weak_ptr

这个时候,weak_ptr 就应运而生了。我们只需要对上述代码进行一些小小的更改,就可以解决问题:

#include <iostream>
#include <memory>
using namespace std;
struct Node
{
	int a;
	weak_ptr<struct Node> next;				//我们将Node中的两个指针类型变为weak_ptr
	weak_ptr<struct Node> prev;
	~Node()
	{
		cout << "~Node()" << endl;
	}
};
int main()
{
	shared_ptr<struct Node> n1(new Node);
	shared_ptr<struct Node> n2(new Node);
	cout << n1.use_count() << endl;
	cout << n2.use_count() << endl;
	n1->next = n2;
	n2->prev = n1;
	cout << n1.use_count() << endl;
	cout << n2.use_count() << endl;
	return 0;
}

运行结果:
在这里插入图片描述
这个原理是什么呢?
我们查看weak_ptr的介绍可以发现weak_ptr 被称为弱管理参数,我们再看它的赋值运算符重载中的介绍:
在这里插入图片描述
如果,给weak_ptr 赋值的对象不为空,那么weak_ptr 就成为该对象的一部分,能访问该对象的资源,且不增加use_count
这就解释了为什么将struct Node中的指针类型更改后,就可以成功释放两个对象了。
在weak_ptr 中,多了两个接口:
在这里插入图片描述
expired ,用于返回weak_ptr 指向的资源是否已经被释放,如果已经释放就返回true,否则返回false。
在这里插入图片描述
lock, 用于获取所管理的对象的强引用(shared_ptr). 如果 expired 为 true, 返回一个空的 shared_ptr; 否则返回一个 shared_ptr, 其内部对象指向与 weak_ptr 相同。
在这里插入图片描述
测试用例:

#include <iostream>
#include <memory>
using namespace std;
int main()
{
	shared_ptr<int> sp1(new int(5));
	shared_ptr<int> sp2(new int(6));
	weak_ptr<int> wp1;
	wp1 = sp1;
	sp2 = wp1.lock();			//返回强引用 sp1;
	cout << wp1.expired() << endl;
	sp1.reset();				//清空sp1
	sp1 = wp1.lock();			//返回的是sp2   恢复sp1
	sp1.reset();				//清空sp1
	sp2.reset();				//清空sp2
	cout << wp1.expired() << endl;		//wp1 已经过期
	sp1 = wp1.lock();			//过期后返回的是空
	if (sp1) {
		cout << "sp1 is not empty" << endl;
	}
	else {
		cout << "sp1 is empty" << endl;
	}
	return 0;
}

运行结果:
在这里插入图片描述

智能指针的使用选择

  1. 如果需要多个对象共同维护资源,就使用shared_ptr,如果有造成循环引用的可能,就在对应位置修改为weak_ptr维护。
  2. 如果只需要单个对象管理,就使用 unique_ptr即可。
    以上就是我在目前在智能指针学到的知识,不足之处还望指正。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值