C++四种智能指针
为什么要有智能指针
1、裸指针中可能存在的问题
裸指针是指未经类封装的原生指针。在工程项目中,如果使用裸指针不规范或者书写代码逻辑时候不仔细,那么就有可能产生各种错误、异常现象。
(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.使用时定义一个该类的对象
3、C++内存泄漏
(1)堆内存泄露
堆内存指的是程序执行中依据须要分配通过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中已经移除不可用。
总结:智能指针是将原生指针(裸指针)封装成类,通过构造、析构函数来实现资源的即使释放,避免内存泄漏和多次释放空间等非法行为发生(RAII特性)。同时还需要重载operator*和operator->来使该类有指针的行为,能够像指针一样使用。
第一种智能指针:auto_ptr
1、auto_ptr的使用
在C98中,auto_ptr所做的事情,就是动态分配对象以及当对象不再需要时自动执行清理。
std::auto_ptr<int> ap1(new int);
std::auto_ptr<int> ap2(ap1);
2、auto_ptr的问题
(1)拷贝问题:编译器对auto_ptr默认生成的拷贝构造和复制重载会对指针进行”浅拷贝“,这个时候对指针进行delete时候就会对同一块空间delete多次,就会发生错误。
这个时候就有人说,那我就深拷贝。但事实是,我们需要的就是浅拷贝,想一下指针赋值的场景下,我们的目的就是让多个指针指向相同的空间,即指针内容相同。所以只能指针去赋值或者拷贝也要进行浅拷贝。
3、问题的解决
(1)C++98中对于auto_ptr的拷贝问题,解决方案是管理权转移
管理权转移:指针浅拷贝以后,将被赋值指针指向nullptr。
auto_ptr<int> sp1 = new auto_ptr(new int);//管理权在sp1手中
auto_ptr<int> sp2(sp1);//sp1拷贝构造sp2后,sp1中管理的指针指向nullptr了,此时sp2中指针指向sp1创建时开辟的空间。
<注>:这个时候,sp1已经悬空了,sp1指向空,如果这个时候再去对其中的指针解引用,就会触发空指针异常。
4、auto_ptr的模拟实现
template<class T>
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<T>& my_ap)
{
_ptr = my_ap._ptr;
my_ap._ptr = nullptr;
}
my_auto_ptr<T>& operator=(my_auto_ptr<T>& my_ap)
{
if(this != &my_ap)
{
delete _ptr;
_ptr = my_ap._ptr;
my_ap._ptr = nullptr;
}
return *this;
}
};
第二种智能指针unique_ptr
1、unique_ptr的使用
禁止拷贝(复制重载+拷贝构造),这样就不存在auto_ptr中析构两次的问题。也就不需要管理权转移的方法。
std::unqiue_ptr<int> up1(new int);
<注>:不能进行拷贝构造和赋值操作
2、unique_ptr的模拟实现
tempalte<class T>
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<T>& my_up) = delete;
my_unique_ptr<T>& operator=(my_unique_ptr<T>& my_up) = delete;
T& operator*()
{
return *_ptr;
}
T* operator()
{
return _ptr;
}
private:
T* _ptr;
};
3、unique_ptr的问题
unique_ptr是一种十分暴力的方式,我直接禁用operator=和拷贝构造,这样就根本无法使用,从根本上解决了拷贝带来的问题。但是太暴力,毕竟有些情况下确实需要拷贝。所以C++库又采用了另一种机制引用计数。
使用引用计数的只能指针叫做shared_ptr,将在后续继续介绍。
第三种智能指针shared_ptr
1、shared_ptr的使用
使用引用计数机制,统计指向同一块空间的shared_ptr的个数。增加一个,计数器就+1,减少一个计数器就-1。shared_ptr出作用域调用析构函数时,只有当计数器数值为0时,才回去调用delete去释放空间;否则只对计数器进行–操作。
#include <iostream>
#include <memory>
using namespace std;
int main()
{
shared_ptr<int> sp1(new int);
shared_ptr<int> sp2(sp1);
//use_count()可以统计出管理同一块空间的shared_ptr个数
cout << sp2.use_count() << endl;
return 0;
}
2、shared_ptr的模拟实现
template<class T>
class 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<T>& my_sp)
:_ptr(my_sp._ptr)
,_count(my_sp._count)
{
AddRef()
}
my_shared_ptr<T>& operator=(const my_shared_ptr<T>& 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;
};
引用计数需要注意的问题:
(1)int _count:
(2)static int _count;
(3)int* _count;
3、shared_ptr的问题
shared_ptr也是解决了指针使用的绝大部分问题,但是还是在有些场景下会出现问题。shared_ptr出现的问题时:循环引用,接下来具体解释一下什么叫循环引用。
(1)循环引用举例1
示例1
struct B;
struct A
{
shared_ptr<B> _b;
};
struct B
{
shared_ptr<A> _a;
};
int main()
{
shared_ptr<A> asp(new A);
shared_ptr<B> bsp(new B);
asp->_b = bsp;
bsp->_a = asp;
}
此时我们可以看到,_a和asp两个shared_ptr都指向A对象的空间;_b和bsp两个shared_ptr都指向B对象的空间。
所以_a\asp的引用计数都为2;_b\bsp的引用计数也为2。
我们接着看,当出作用域调用析构函数时,asp和bsp析构,调用析构函数,因为最初引用计数为2,所以调用析构函数只将引用计数-1,所以此时A对象、B对象空间的引用计数为1,空间不会delete释放。A对象B对象空间不释放,就不会调用A、B的析构函数,既不会释放成员变量_a和_b指向的空间,所以引用计数一直保持为1,所以就会造成内存泄漏(堆泄露)。
A、B两个对象中,A中成员变量指向B、B中成员变量指向A,导致双方的空间都依赖于对方是否释放。这种就形成了”循环引用“(类似于死锁的循环等待问题)。
(2)循环引用举例2
在双向链表中,定义_next和_prev两个指针,指向前后节点
struct ListNode
{
shared_ptr<ListNode> _next;
shared_ptr<ListNode> _prev;
int _val;
};
int main()
{
shared_ptr<ListNode> node1(new ListNode);
shared_ptr<ListNode> 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> _b;
};
struct B
{
weak_ptr<A> _a;
};
int main()
{
shared_ptr<A> asp(new A);
shared_ptr<B> bsp(new B);
asp->_b = bsp;
bsp->_a = asp;
}
struct ListNode
{
weak_pre<ListNode> _next;
weak_ptr<ListNode> _prev;
int _val;
};
int main()
{
shared_ptr<ListNode> node1(new ListNode);
shared_ptr<ListNode> node2(new ListNode);
node1->_next = node2;
node2->_prev = node1;
}
2、weak_ptr的模拟实现
template<class T>
class weak_ptr
{
public:
weak_ptr()
:_ptr(nullptr)
{
}
weak_ptr(const shared_ptr<T>& sp)
:_ptr(sp.get())
{
}
weak_ptr(const weak_ptr<T>& wp)
:_ptr(wp._ptr)
{
}
weak_ptr<T>& operator(const shared_ptr<T>& sp)
{
_pre = sp.get();
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
shared_ptr中定制删除器
库中实现的智能指针,统一的操作就是对指针进行delete,但是如果交付给智能指针管理的指针指向一个new出来的数组(要delete [],而不是delete),或者是FILE*的指针(要close,而不是delete)等情况,就会出现错误。
默认的删除方式只适合new出来的指向单个对象的指针。
所以我们就要根据智能指针中不同的内容来定制不同的析构方式(定制删除器)。
在库中,D del就是删除器。在构造时候 传入一个可调用对象 作为删除器
举例
template<class T>
struct DeleteArray
{
void operator()(const T* ptr)
{
delete[] ptr;
}
};
void fun()
{
//函数对象
std::shared_ptr<int,DeleteArray<int>> arrSptr(new int[10],DeleteArray<int>());
//lambda表达式(使用包装器或者decltype)
std::shared_ptr<FILE*,std::function<void(FILE*)>> fileSptr(fopen("test.txt","w"),[](FILE* ptr){
fclose(ptr);});
}
shared_ptr中增加定制删除器的功能
template<class T, class D>
class 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<T>& my_sp)
:_ptr(my_sp._ptr)
,_count(my_sp._count)
{
AddRef()
}
my_shared_ptr<T>& operator=(const my_shared_ptr<T>& 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;
};