智能指针
- 为什么需要智能指针?
- 内存泄漏
- 什么是内存泄漏,内存泄漏的危害
- 内存泄漏分类
- 智能指针的使用及原理
- RAII
- 智能指针的原理
- std::auto_ptr
- std::unique_ptr
- std::shared_ptr
为什么需要智能指针?
下面我们先分析一下下面这段程序有没有什么内存方面的问题?
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void Func()
{
int* p1 = new int;
int* p2 = new int;
cout << div() << endl;
delete p1;
delete p2;
}
int main()
{
try {
Func();
}
catch (exception& e)
{
cout << e.what() << endl;
}
return 0;
}
1、如果p1这里new 抛异常会如何?
直接退出 Func()
,异常被捕获,资源未被释放,内存泄漏
2、如果p2这里new 抛异常会如何?
直接退出 Func()
,异常被捕获,资源未被释放,内存泄漏
3、如果div调用这里又会抛异常会如何?
异常被捕获,资源未被释放,内存泄漏
由此可见,简单的异常捕获会造成内存泄漏,所以需要引入新的方法来解决这个问题
内存泄漏
什么是内存泄漏,内存泄漏的危害
什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。
内存泄漏分类
C/C++程序中一般我们关心两种方面的内存泄漏:
- 堆内存泄漏(Heap leak)
堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。 - 系统资源泄漏
指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。
智能指针的使用及原理
RAII
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术,简单来说就是资源获得即初始化
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
- 不需要显式地释放资源。
- 采用这种方式,对象所需的资源在其生命期内始终保持有效。
智能指针的原理
上述的SmartPtr还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可
以通过->去访问所指空间中的内容,因此:AutoPtr模板类中还得需要将* 、->重载下,才可让其像指针一样去使用。
总结一下智能指针的原理:
- RAII特性
- 重载operator*和opertaor->,具有像指针一样的行为。
std::auto_ptr
auto_ptr的实现原理:管理权转移的思想,下面简化模拟实现了一份bit::auto_ptr来了解它的原理
template<class T>
class auto_ptr
{
public:
//RAII
//保存资源
auto_ptr(T* ptr)
:_ptr(ptr)
{
}
//释放资源
~auto_ptr()
{
delete _ptr;
cout << _ptr << endl;
}
//像指针一样
auto_ptr(auto_ptr<T>& sp)
:_ptr(sp._ptr)
{
sp._ptr = nullptr;
}
T& operator*()
{
return *_ptr;
}
T& operator->()
{
return &_ptr;
}
T& operator[](size_t pos)
{
return _ptr[pos];
}
private:
T* _ptr;
};
auto_ptr
智能指针的功能是资源管理权限转移,使用起来非常不方便
std::unique_ptr
unique_ptr的实现原理:为了解决auto_ptr
资源转移的问题,unique_ptr
直接禁止拷贝构造,从根本上解决问题,下面简化模拟实现了一份UniquePtr来了解它的原理
template<class T>
class unique_ptr
{
public:
//RAII
//保存资源
unique_ptr(T* ptr)
:_ptr(ptr)
{
}
//释放资源
~unique_ptr()
{
delete _ptr;
}
//像指针一样
unique_ptr(const unique_ptr<T>& sp) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;
T& operator*()
{
return *_ptr;
}
T& operator->()
{
return &_ptr;
}
T& operator[](size_t pos)
{
return _ptr[pos];
}
private:
T* _ptr;
};
unique_ptr
虽然是解决了上面的问题,但是使用起来还是很不方便
std::shared_ptr
shared_ptr的原理:在实现时,使用多使用一个指针用来统计指向同一地址的指针的个数;通过引用计数的方式来实现多个shared_ptr对象之间共享资源。
- shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。
- 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
- 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源。
- 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。
template<class T>
class shared_ptr
{
public:
//RAII
//保存资源
shared_ptr(T* ptr=nullptr)
:_ptr(ptr)
,_pcount(new int(1))
{
}
//释放资源
~shared_ptr()
{
Release();
}
//像指针一样
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
,_pcount(sp._pcount)
{
++(*_pcount);
}
void Release()
{
if (--(*_pcount) == 0)
{
delete _pcount;
delete _ptr;
}
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr != sp._ptr)
{
Release();
_pcount = sp._pcount;
_ptr = sp._ptr;
++(*_pcount);
}
return *this;
}
int use_count()const
{
return *_pcount;
}
T* get()const
{
return _ptr;
}
T* get()const
{
return _ptr;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T& operator[](size_t pos)
{
return _ptr[pos];
}
private:
T* _ptr;
int* _pcount;
};```
![在这里插入图片描述](https://img-blog.csdnimg.cn/c6150239a79e44a9b56fac49eb834176.png)
三个指针指向同一地址,通过引用计数统计指向同一地址的指针的个数
![在这里插入图片描述](https://img-blog.csdnimg.cn/ea15f69d7e564d0d8bfd70be1f15e5e2.png)
**std::shared_ptr的线程安全问题**
通过下面的程序我们来测试shared_ptr的线程安全问题。需要注意的是shared_ptr的线程安全分为两方面:
1. 智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时++或--,这个操作不是原子的,引用计数原来是1,++了两次,可能还是2.这样引用计数就错乱了。会导致资源未释放或者程序崩溃的问题。所以只能指针中引用计数++、--是需要加锁的,也就是说引用计数的操作是线程安全的。
2. 智能指针管理的对象存放在堆上,两个线程中同时去访问,会导致线程安全问题。
```cpp
struct Date
{
int _year = 0;
int _month = 0;
int _day = 0;
};
int main()
{
yjm::shared_ptr<Date> p1(new Date);
int n = 100;
thread t1([&]
{
for (int i = 0; i < n; i++)
{
yjm::shared_ptr<Date>p2(p1);
p2->_year++;
p2->_month++;
p2->_day++;
}
});
thread t2([&]
{
for (int i = 0; i < n; i++)
{
yjm::shared_ptr<Date>p3(p1);
p3->_year++;
p3->_month++;
p3->_day++;
}
});
t1.join();
t2.join();
cout << p1.use_count() << endl;
return 0;
}
由于两个线程同时访问同一变量,并且操作也不是原子性的,所以智能指针的引用计数并不安全
如果在线程进行访问变量之前进行加锁,便可避免线程安全的问题
std::shared_ptr的循环引用
struct Listnode
{
int val;
yjm::shared_ptr<Listnode> _next;
yjm::shared_ptr<Listnode> _prve;
~Listnode()
{
cout << "~Listnode()" << endl;
}
};
int main()
{
yjm::shared_ptr<Listnode>n1(new Listnode);
yjm::shared_ptr<Listnode>n2(new Listnode);
n1->_next = n2;
n2->_prve = n1;
return 0;
}
节点的资源并没有释放,由此造成内存泄漏的问题
循环引用分析:
yjm::shared_ptr<Listnode>n1(new Listnode);
yjm::shared_ptr<Listnode>n2(new Listnode);
n1->_next = n2;
n2->_prve = n1;
return 0;
程序结束后,节点依然存在;只有_next
析构,n2
才会被析构;只有_prev
析构,n1
才会被析构;但是由于_next
属于n1
,只有n1
析构之后,_next
才会被释放,_prev
同样如此;由此便进入了僵局,也就是循环引用
为了解决这一问题将节点中的指针进行修改,使其只能进行访问资源,但不能参与资源的管理,不增加引用计数
template<class T>
class weak_ptr
{
public:
weak_ptr()
:_ptr(nullptr)
{
}
weak_ptr(const shared_ptr<T>&sp)
:_ptr(sp.get())
{}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
问题完美解决