[C++] 智能指针
Posted 哦哦呵呵
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[C++] 智能指针相关的知识,希望对你有一定的参考价值。
目录
1. 为什么需要智能指针?
我们在使用指针时一般都需要向内存申请一块内存空间进行使用,但是如果忘记对该块空间进行释放,就造成了内存泄漏。
并且如果在申请内存的使用时间,程序中有异常处理,并且抛出了异常,那么这个过程中没有进行空间的释放,同样造成了内存泄漏。
内存泄漏的危害:
长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死
所以为了避免上述情况的出现,引入了只能指针,使用智能指针进行资源的管理,就可以避免上述问题。
2. RAII (资源获取及初始化)
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
巧妙利用编译器会自动调用构造函数以及析构函数的特性,来完成资源的自动释放。实际就是定义一个指针类,使用其构造函数与析构函数进行资源的管理。
构造函数: 在对象构造时获取资源,控制对资源的访问使之在对象的生命周期内始终保持有效。
析构函数: 释放资源
优点
- 不需要显式地释放资源
- 采用这种方式,对象所需的资源在其生命周期内始终保持有效
2.1 RAII方式的原理
注意: 只能指针的类中,构造函数中不能申请空间,而是由用户在外部自己申请空间,传递到只能指针的类中去管理该资源。
智能指针只是将资源管理起来,找一个合适的时机去释放。
// 使用RAII思想设计的SmartPtr类
template<class T>
class SmartPtr
public:
SmartPtr(T* ptr = nullptr)
: _ptr(ptr)
~SmartPtr()
if(_ptr)
delete _ptr;
// 让该类对象具有指针类似的操作就可以了
T& operator*()
return *ptr;
// -> 只能指针指向对象或者结构体的这些场景中
T* operator->()
return ptr;
private:
T* _ptr; // 采用类将指针管理起来
;
void MergeSort(int* a, int n)
int* tmp = (int*)malloc(sizeof(int)*n);
// 将tmp指针委托给了sp对象,给tmp指针找了一个可怕的女朋友!天天管着你,直到你go die^^
SmartPtr<int> sp(tmp);
// _MergeSort(a, 0, n - 1, tmp);
// 这里假设处理了一些其他逻辑
vector<int> v(1000000000, 10);
// ...
如上只是实现了资源的管理,但是该类不能使用指针独有的操作,所以需要重载指针的一些操作符。
2.2 重大问题
上述代码中没有提供拷贝构造函数,那么如果进行拷贝时,会调用编译器提供的默认拷贝构造函数。智能指针不能申请资源,只能替用户管理资源,因此不能使用深拷贝来代替浅拷贝
如何解决: C++标准库中提供了,最优解,下面会介绍到。
2.3 智能指针原理
所以智能指针的原理: RAII + operator*() / operator->() + 解决浅拷贝问题
RAII: 能够保证资源可以被自动释放
operator()/operator->()*: 可以保证对象能够按照指针的方式来运行
解决浅拷贝的方式:可能保证资源不被释放多次而引起代码崩溃的问题
3. C++98 auto_ptr(不要用)
RAII + operator*() / operator->() + 解决浅拷贝问题
3.1 解决浅拷贝的方式一
auto_ptr: 解决浅拷贝使用的是资源转移
当发生拷贝或赋值时,将被拷贝对象中的资源转移给新对象,然后让被拷贝对象与资源断开联系
但是同时只能有一个指针被使用,另外一个指针就没有指向了
// 解决浅拷贝方式:资源转移
// auto_ptr<int> ap2(ap1)
auto_ptr(auto_ptr<T>& ap)
: _ptr(ap._ptr)
ap._ptr = nullptr;
// ap1 = ap2;
auto_ptr<T>& operator=(auto_ptr<T>& ap)
if (this != &ap)
// 此处需要将ap中的资源转移给this
// 但是不能直接转移,因为this可能已经管理资源了,否则就会造成资源泄漏
if (_ptr)
delete _ptr;
// ap就可以将其资源转移给this
_ptr = ap._ptr;
ap._ptr = nullptr; // 让ap与之前管理的资源断开联系,因为ap中的资源已经转移给this了
return *this;
3.2 解决浅拷贝的方式二
资源管理权限转移
如果发生了拷贝或者赋值,不断开原来指针与资源块的联系,而是将资源的释放权限交给新指针,但是会造成野指针问题,因为新指针一旦析构,就会释放资源,而老指针还指向那块空间
4. C++11 unique_ptr
RAII + operator*() / operator->() + 解决浅拷贝问题
浅拷贝解决方式: 资源独占
禁止拷贝,一份资源只能让一个对象管理,对象间不能共享资源。
使用场景: 只能应用于资源被一个对象管理,并且资源不会共享的场景中。
如何实现
让编译器不生成默认的拷贝构造以及赋值运算符的重载。
C++11中: delete关键字
,可以用其修饰默认的成员函数,表明编译器不会生成修饰的成员函数。
C++98中: 将拷贝构造函数以及赋值运算符重载只声明不定义,并且把访问权限设置为私有。
只声明不定义,没有将权限设置为private,仍然可以在类外进行定义。
4.1 管理的资源多样如何释放
管理的资源可能是从堆上申请的空间,也可能是文件指针等等
定制删除器: 此处的释放方式不能写死,应该按照资源的不同类型对应不同的释放方式。
定义只能指针类时,模板参数加上资源释放的方式,用户自定义资源释放的类,自己指定释放资源的方法。在初始化智能指针时,就该把资源的释放方式确定下来,在初始化模板参数时就传入释放资源的类。
4.2 模拟实现
// 负责释放new资源
template<class T>
class DFDef
public:
void operator()(T*& ptr)
if (ptr)
delete ptr;
ptr = nullptr;
;
// 负责:malloc的资源的释放
template<class T>
class Free
public:
void operator()(T*& ptr)
if (ptr)
free(ptr);
ptr = nullptr;
;
// 关闭文件指针
class FClose
public:
void operator()(FILE* & ptr)
if (ptr)
fclose(ptr);
ptr = nullptr;
;
namespace test
// T: 资源中所放数据的类型
// DF: 资源的释放方式
// 定制删除器
template<class T, class DF = DFDef<T>>
class unique_ptr
public:
/
// RAII
unique_ptr(T* ptr = nullptr)
: _ptr(ptr)
~unique_ptr()
if (_ptr)
// 问题:_ptr管理的资源:可能是从堆上申请的内存空间、文件指针、malloc空间...
// delete _ptr; // 注意:此处的释放资源的方式不能写死了,应该按照资源类型不同找对应的方式释放
// malloc--->free
// new---->delete
// fopen--->fclose关闭
DF df;
df(_ptr);
// C++11: 可以让编译器不生成默认的拷贝构造以及赋值运算符重载---delete
// 在C++11当中,delete关键字的功能扩展:释放new申请的空间 用其修饰默认成员函数,表明:编译器不会生成了
unique_ptr(const unique_ptr<T,DF>&) = delete; // 表明:编译器不会生成默认的拷贝构造函数
unique_ptr<T,DF>& operator=(const unique_ptr<T,DF>&) = delete;// 表明:编译器不会生成默认的赋值运算符重载
private:
unique_ptr(const unique_ptr<T,DF>&);
unique_ptr<T,DF>& operator=(const unique_ptr<T,DF>&);
private:
T* _ptr;
;
// 用户在外部可以对方法进行定义---在unique_ptr的类中如果将该权限设置为private的
//template<class T>
//unique_ptr<T>::unique_ptr(const unique_ptr<T>& up)
//
5. C++11 shared_ptr
多个对象之间可以共享资源
5.1 如何解决浅拷贝
采用引用计数的方式解决浅拷贝
引用计数: 实际是一个整型的空间,记录着资源的对象的个数。释放资源时,判断有没有其它对象在使用资源,没有的话就释放掉资源。
5.2 实现原理
拷贝时:
新对象要与之前的对象共享所有资源,并且计数器进行++
释放时:
1.先检测资源是否还存在
2.存在的话,先对计数器–,检测计数器当前是否为0
- =0: 当前对象是最后使用该资源的对象,需要将资源以及计数器进行释放。
- !=0: 还有其他对象在使用资源,当前对象不需要释放,如果释放了就会造成其它对象变成野指针。
5.3 多线程使用时的问题
上述操作在单线程下不会出现问题,但是在多线程中有多个执行流。
多个线程共享一份资源,多个线程在结束时需要将其管理的资源释放掉,多个线程可能会同时释放同一份空间,计数器的操作不是原子性的,所以多个线程判断当前资源都还有人在使用,导致资源没有释放,而引起内存泄漏。
智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时++或–,这个操作不是原子的,引用计数原来是1,++了两次,可能还是2.这样引用计数就错乱了。会导致资源未释放或者程序崩溃的问题。所以只能指针中引用计数++、–是需要加锁的,也就是说引用计数的操作是线程安全的
解决方案
1.加锁,保证计数器的操作是安全的
2.把计数器改为原子操作
C++11中的share_ptr,自身是线程安全的,标准库中是按照原子类型变量实现的 atomic_int
5.4 缺陷- 循环引用
struct ListNode
shared_ptr<ListNode> next;
shared_ptr<ListNode> prev;
//weak_ptr<ListNode> next;
//weak_ptr<ListNode> prev;
int data;
ListNode(int x)
//: data(x)
: next(nullptr)
, prev(nullptr)
, data(x)
cout << "ListNode(int):" << this << endl;
~ListNode()
cout << "~ListNode():" << this << endl;
;
void TestLoopRef()
shared_ptr<ListNode> sp1(new ListNode(10));
shared_ptr<ListNode> sp2(new ListNode(20));
cout << sp1.use_count() << endl; // 1
cout << sp2.use_count() << endl; // 1
sp1->next = sp2;
sp2->prev = sp1;
cout << sp1.use_count() << endl; // 2
cout << sp2.use_count() << endl; // 2
上述代码导致的后果就是内存泄漏,释放资源时没有调用析构函数。
什么是循环引用
函数中sp1 ,sp2之间互相引用,两个资源的引用计数为2,当要跳出函数时,智能指针sp1 ,sp2析构时两个资源引用计数会减一,但是两者引用计数还是为1,导致跳出函数时资源没有被释放(析构函数没有被调用),如果把其中一个改为weak_ptr就可以了,我们把类种里面的shared_ptr 指针; 改为weak_ptr 指针; 运行结果如下,这样的话,资源B的引用开始就只有1,当pb析构时,B的计数变为0,B得到释放,B释放的同时也会使A的计数减一,同时pa析构时使A的计数减一,那么A的计数为0,A得到释放
5.5 weak_ptr
weak_ptr的作用,配合shared_ptr解决循环引用问题。weak_ptr不能独立管理资源
不控制对象生命周期,指向一个shared_ptr
管理的对象,只是提供了对管理对象的一个访问手段。weak_ptr设计的目的为了配合shared_ptr解决循环引用,导致无法释放内存空间的问题。weak_ptr的构造和析构不会造成引用计数的增加或者减少
原理
实际上shared_ptr维护了两块计数器,一份用来记录shared_ptr的使用次数,一份用来记录weakptr的使用次数。weak_ptr指向资源,shared_ptr的use计数器不懂,weak计数器+1
销毁时
当一个资源被shared_ptr类型的对象共享时,给use+1
当一个资源被weak_ptr类型的对象时,给weak+1
销毁时: 先给use-1,确认资源能够释放,再给weak-1,确认资源能释放
5.5 代码实现
#include <mutex>
namespace test
// shared_ptr: 自身才是安全的---加锁:为了保证shared_ptr自身的安全性
template<class T, class DF = DFDef<T>>
class shared_ptr
public:
//
// RAII
shared_ptr(T* ptr = nullptr)
: _ptr(ptr)
, _pCount(nullptr)
, _pMutex(new mutex)
if (_ptr)
// 此时只有当前刚刚创建好的1个对象在使用该份资源
_pCount = new int(1);
~shared_ptr()
Release();
/
// 具有指针类似的行为
T& operator*()
return *_ptr;
T* operator->()
return _ptr;
T* Get()
return _ptr;
//
// 用户可能需要获取引用计数
int use_count()const
return *_pCount;
///
// 解决浅拷贝方式:采用引用计数
shared_ptr(const shared_ptr<T>& sp)
: _ptr(sp._ptr)
, _pCount(sp._pCount)
, _pMutex(sp._pMutex)
AddRef();
shared_ptr<T, DF>& operator=(const shared_ptr<T, DF>& sp)
if (this != &sp)
// 在和sp共享之前,this先要将之前的状态清空
Release();
// this就可以和sp共享资源以及计数了
_ptr = sp._ptr;
_pCount = sp._pCount;
_pMutex = sp._pMutex;
AddRef();
return *this;
private:
void AddRef()
if (nullptr == _ptr)
return;
_pMutex->lock();
++(*_pCount);
_pMutex->unlock();
void Release()
if (nullptr == _ptr)
return;
bool isDelete = false;
_pMutex->lock();
if (_ptr && 0 == --(*_pCount))
// delete _ptr;
DF df;
df(_ptr);
delete _pCount;
_pCount = nullptr;
isDelete = true;
_pMutex->unlock();
if (isDelete)
delete _pMutex;
private:
T* _ptr; // 用来接收资源的
int* _pCount; // 指向了引用计数的空间---记录的是使用资源的对象的个数
mutex* _pMutex; // 目的:保证对引用计数的操作是安全的
;
以上是关于[C++] 智能指针的主要内容,如果未能解决你的问题,请参考以下文章