- 为什么要有智能指针
- 1、裸指针中可能存在的问题
- 2、RAII思想
- (1)RAII原理
- (2)整个RAlI过程总结为四个步骤:
- 3、C++内存泄漏
- (1)堆内存泄露
- (2)系统资源泄露
- 什么是智能指针
- 第一种智能指针:auto_ptr
- 1、auto_ptr的使用
- 2、auto_ptr的问题
- 3、问题的解决
- 4、auto_ptr的模拟实现
- 第二种智能指针unique_ptr
- 1、unique_ptr的使用
- 2、unique_ptr的模拟实现
- 3、unique_ptr的问题
- 第三种智能指针shared_ptr
- 1、shared_ptr的使用
- 2、shared_ptr的模拟实现
- 3、shared_ptr的问题
- (1)循环引用举例1
- (2)循环引用举例2
- 第四种智能指针weak_ptr
- 1、weak_ptr的使用
- 2、weak_ptr的模拟实现
- shared_ptr中定制删除器
- shared_ptr中增加定制删除器的功能
裸指针是指未经类封装的原生指针。在工程项目中,如果使用裸指针不规范或者书写代码逻辑时候不仔细,那么就有可能产生各种错误、异常现象。
(1)malloc出来的空间,如果没有及时释放,就会造成内存泄漏
(2)异常安全问题:如果malloc和free之间存在异常,那么发生异常时,就有可能无法释放空间,造成内存泄漏。这种问题称为“异常安全问题”。
(3)当要delete一个指针指向的空间时,不方便判断该指针指向的是一个数组还是一个单独的对象,所以使用“delete”还是"delete[]"不方便确定。
(4)无法判断一个不为nullptr的指针是否为悬挂指针。
(5)还有可能出现多次释放空一块空间的情况
面对这些问题,GC(垃圾回收机制)是可以有效解决的,但是C++并没有垃圾回收机制,而是提出了一种思想RAII。
2、RAII思想RAll (Resource Acquisition ls Initialization)是C++之父Bjarne Stroustrup提出的,翻译为资源获取即初始化。使用局部对象来管理资源的技术称为资源获取即初始化;这里的资源主要是指操作系统中有限的东西如内存、网络套接字,互斥量,文件句柄等等,局部对象是指存储在栈的对象,它的生命周期是由操作系统来管理的,无需人工介入。
(1)RAII原理符合RAII的资源一般要经历三个步骤:
获取资源——使用资源——销毁资源
其实在C++中,类对象就很好体现了RAII这一原理。具体如下
获取资源(构造)——使用资源——销毁资源(析构)
我们都知道,当我们创建一个类对象的时候,编译器就会自动调用构造函数,当对象出了所在作用域,编译器就会自动调用该类的析构函数。无论是构造函数还是析构函数,都不需要我们自己手动调用,这样就避免了我们忘记初始化,忘记销毁对象的不好的事情发生。
这样看来,避免忘记析构是不是也对应着我们想要避免忘记free/delete指针。
类对象符合RAII的举例
class A { public: A() { cout << "construct the object" << endl; } ~A() { cout << "destroy the object" << endl; } }; void fun() { //创建一个对象 A a = A(); //fun调用结束,a出了作用域,自动调用其析构函数 } int main() { fun(); return 0; }
(2)整个RAlI过程总结为四个步骤:代码运行结果
a.设计一个类封装资源
b.在构造函数中初始化
c.在析构函数中执行销毁操作
d.使用时定义一个该类的对象
堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak(堆内存泄露)。
(2)系统资源泄露指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。
什么是智能指针从比较简单的层面来看,智能指针是RAII(Resource Acquisition Is Initialization,资源获取即初始化)机制对普通指针进行的一层封装。这样使得智能指针的行为动作像一个指针,本质上却是一个对象,这样可以方便管理一个对象的生命周期。
C++中总共有四种智能指针:
auto_ptr
unique_ptr
shared_ptr
weak_ptr
其中,auto_ptr 在 C++11已被摒弃,在C++17中已经移除不可用。
第一种智能指针:auto_ptr 1、auto_ptr的使用总结:智能指针是将原生指针(裸指针)封装成类,通过构造、析构函数来实现资源的即使释放,避免内存泄漏和多次释放空间等非法行为发生(RAII特性)。同时还需要重载operator*和operator->来使该类有指针的行为,能够像指针一样使用。
在C98中,auto_ptr所做的事情,就是动态分配对象以及当对象不再需要时自动执行清理。
std::auto_ptr2、auto_ptr的问题ap1(new int); std::auto_ptr ap2(ap1);
(1)拷贝问题:编译器对auto_ptr默认生成的拷贝构造和复制重载会对指针进行”浅拷贝“,这个时候对指针进行delete时候就会对同一块空间delete多次,就会发生错误。
3、问题的解决这个时候就有人说,那我就深拷贝。但事实是,我们需要的就是浅拷贝,想一下指针赋值的场景下,我们的目的就是让多个指针指向相同的空间,即指针内容相同。所以只能指针去赋值或者拷贝也要进行浅拷贝。
(1)C++98中对于auto_ptr的拷贝问题,解决方案是管理权转移
管理权转移:指针浅拷贝以后,将被赋值指针指向nullptr。
auto_ptrsp1 = new auto_ptr(new int);//管理权在sp1手中 auto_ptr sp2(sp1);//sp1拷贝构造sp2后,sp1中管理的指针指向nullptr了,此时sp2中指针指向sp1创建时开辟的空间。
4、auto_ptr的模拟实现<注>:这个时候,sp1已经悬空了,sp1指向空,如果这个时候再去对其中的指针解引用,就会触发空指针异常。
template第二种智能指针unique_ptr 1、unique_ptr的使用class my_auto_ptr { private: T* _ptr; public: //构造和析构实现RAII特性 my_auto_ptr(T* ptr) :_ptr(ptr) {} ~my_auto_ptr() { if(_ptr) { delete _ptr; _ptr = nullptr; } } //*和->的重载实现指针行为的模拟 T& operator*() { return *_ptr; } T* operator() { return _ptr; } //赋值+拷贝实现管理权转移 my_auto_ptr(my_auto_ptr & my_ap) { _ptr = my_ap._ptr; my_ap._ptr = nullptr; } my_auto_ptr & operator=(my_auto_ptr & my_ap) { if(this != &my_ap) { delete _ptr; _ptr = my_ap._ptr; my_ap._ptr = nullptr; } return *this; } };
禁止拷贝(复制重载+拷贝构造),这样就不存在auto_ptr中析构两次的问题。也就不需要管理权转移的方法。
std::unqiue_ptrup1(new int);
2、unique_ptr的模拟实现<注>:不能进行拷贝构造和赋值操作
tempalte3、unique_ptr的问题class my_unique_ptr { public: my_unique_ptr(T* ptr) :_ptr(ptr) {} ~my_unique_ptr() { if(_ptr != nullptr) { delete _ptr; _ptr = nullptr; } } //禁用拷贝 my_unique_ptr(my_unique_ptr & my_up) = delete; my_unique_ptr & operator=(my_unique_ptr & my_up) = delete; T& operator*() { return *_ptr; } T* operator() { return _ptr; } private: T* _ptr; };
unique_ptr是一种十分暴力的方式,我直接禁用operator=和拷贝构造,这样就根本无法使用,从根本上解决了拷贝带来的问题。但是太暴力,毕竟有些情况下确实需要拷贝。所以C++库又采用了另一种机制引用计数。
使用引用计数的只能指针叫做shared_ptr,将在后续继续介绍。
使用引用计数机制,统计指向同一块空间的shared_ptr的个数。增加一个,计数器就+1,减少一个计数器就-1。shared_ptr出作用域调用析构函数时,只有当计数器数值为0时,才回去调用delete去释放空间;否则只对计数器进行–操作。
#include2、shared_ptr的模拟实现#include using namespace std; int main() { shared_ptr sp1(new int); shared_ptr sp2(sp1); //use_count()可以统计出管理同一块空间的shared_ptr个数 cout << sp2.use_count() << endl; return 0; }
templateclass my_shared_ptr { void AddRef() { ++(*_count); } void ReleaseRef() { if(--(*_count) == 0) { if(_ptr) { delete _ptr; } delete _count; } } public: my_shared_ptr(T* ptr) :_ptr(ptr) ,_count(new int(1)) {} ~my_shared_ptr() { ReleaseRef() } my_shared_ptr(const my_shared_ptr & my_sp) :_ptr(my_sp._ptr) ,_count(my_sp._count) { AddRef() } my_shared_ptr & operator=(const my_shared_ptr & my_sp) { //防止自己给自己赋值时候,不能使用 //if(this != & my_sp)这种方式赋值 if(_ptr != my_sp._ptr) { ReleaseRef() _ptr = my_sp._ptr; _count = my_sp._count; AddRef() } return *this; } T& operator*() { return *_ptr; } T* operator->() { return _ptr; } size_t use_count() const { return *_count; }' const T* get() const { return _ptr; } private: T* _ptr; int* _count; };
3、shared_ptr的问题引用计数需要注意的问题:
(1)int _count:
(2)static int _count;
(3)int* _count;
shared_ptr也是解决了指针使用的绝大部分问题,但是还是在有些场景下会出现问题。shared_ptr出现的问题时:循环引用,接下来具体解释一下什么叫循环引用。
(1)循环引用举例1示例1
struct B; struct A { shared_ptr _b; }; struct B { shared_ptr _a; }; int main() { shared_ptr asp(new A); shared_ptr bsp(new B); asp->_b = bsp; bsp->_a = asp; }
此时我们可以看到,_a和asp两个shared_ptr都指向A对象的空间;_b和bsp两个shared_ptr都指向B对象的空间。
所以_aasp的引用计数都为2;_bbsp的引用计数也为2。
我们接着看,当出作用域调用析构函数时,asp和bsp析构,调用析构函数,因为最初引用计数为2,所以调用析构函数只将引用计数-1,所以此时A对象、B对象空间的引用计数为1,空间不会delete释放。A对象B对象空间不释放,就不会调用A、B的析构函数,既不会释放成员变量_a和_b指向的空间,所以引用计数一直保持为1,所以就会造成内存泄漏(堆泄露)。
A、B两个对象中,A中成员变量指向B、B中成员变量指向A,导致双方的空间都依赖于对方是否释放。这种就形成了”循环引用“(类似于死锁的循环等待问题)。
在双向链表中,定义_next和_prev两个指针,指向前后节点
struct ListNode { shared_ptr_next; shared_ptr _prev; int _val; }; int main() { shared_ptr node1(new ListNode); shared_ptr node2(new ListNode); node1->_next = node2; node2->_prev = node1; }
和情况1相同。node1空间的释放依赖于node2空间释放,node2空间释放依赖于node1空间释放。形成循环引用现象,导致两部分空间都不释放。
第四种智能指针weak_ptr 1、weak_ptr的使用weak_ptr并不像前三种智能指针一样单独使用。weak_ptr是为了解决循环引用问题而产生的,所以weak_ptr是配合shared_ptr来使用的。不参与资源的管理,只是来访问内容。
weak_ptr可以使用shared_ptr来构造或者将shared_ptr赋值给weak_ptr
weak_ptr不会改变引用计数
上述两种示例的修改
struct B; struct A { weak_ptr _b; }; struct B { weak_ptr _a; }; int main() { shared_ptr asp(new A); shared_ptr bsp(new B); asp->_b = bsp; bsp->_a = asp; }
struct ListNode { weak_pre2、weak_ptr的模拟实现_next; weak_ptr _prev; int _val; }; int main() { shared_ptr node1(new ListNode); shared_ptr node2(new ListNode); node1->_next = node2; node2->_prev = node1; }
templateshared_ptr中定制删除器class weak_ptr { public: weak_ptr() :_ptr(nullptr) {} weak_ptr(const shared_ptr & sp) :_ptr(sp.get()) {} weak_ptr(const weak_ptr & wp) :_ptr(wp._ptr) {} weak_ptr & operator(const shared_ptr & sp) { _pre = sp.get(); return *this; } T& operator*() { return *_ptr; } T* operator->() { return _ptr; } private: T* _ptr; };
库中实现的智能指针,统一的操作就是对指针进行delete,但是如果交付给智能指针管理的指针指向一个new出来的数组(要delete [],而不是delete),或者是FILE*的指针(要close,而不是delete)等情况,就会出现错误。
默认的删除方式只适合new出来的指向单个对象的指针。
所以我们就要根据智能指针中不同的内容来定制不同的析构方式(定制删除器)。
在库中,D del就是删除器。在构造时候 传入一个可调用对象 作为删除器
举例
templateshared_ptr中增加定制删除器的功能struct DeleteArray { void operator()(const T* ptr) { delete[] ptr; } }; void fun() { //函数对象 std::shared_ptr > arrSptr(new int[10],DeleteArray ()); //lambda表达式(使用包装器或者decltype) std::shared_ptr > fileSptr(fopen("test.txt","w"),[](FILE* ptr){fclose(ptr);}); }
templateclass my_shared_ptr { void AddRef() { ++(*_count); } void ReleaseRef() { if(--(*_count) == 0) { if(_ptr) { _del(_ptr); } delete _count; } } public: my_shared_ptr(T* ptr,D del) :_ptr(ptr) ,_count(new int(1)) ,_del(del) {} ~my_shared_ptr() { ReleaseRef() } my_shared_ptr(const my_shared_ptr & my_sp) :_ptr(my_sp._ptr) ,_count(my_sp._count) { AddRef() } my_shared_ptr & operator=(const my_shared_ptr & my_sp) { //防止自己给自己赋值时候,不能使用 //if(this != & my_sp)这种方式赋值 if(_ptr != my_sp._ptr) { ReleaseRef() _ptr = my_sp._ptr; _count = my_sp._count; AddRef() } return *this; } T& operator*() { return *_ptr; } T* operator->() { return _ptr; } size_t use_count() const { return *_count; }' const T* get() const { return _ptr; } private: T* _ptr; int* _count; D _del; };