目录
1. 智能指针的基本概念
2. 智能指针的使用
3. C++库中的智能指针
3.1 auto_ptr
3.2 unique_ptr
3.3.1 多线程计数的安全问题:
3.3.2 定制删除器(仿函数、函数指针、lambda表达式):
3.4 weak_ptr
3.4.1 weak_ptr的模拟实现:
3.4.2 循环引用问题:
4. 如何选择智能指针
5. 内存泄漏(补充)
智能指针主要用于管理在堆上分配的内存,它将普通的指针封装为一个栈对象。当栈对象的生存周期结束后,会在析构函数中释放掉申请的内存,从而防止内存泄漏。
智能指针的实质是一个类对象,它是利用模板类对一般的指针进行封装,在类内的构造函数实现对指针的初始化,并在析构函数里编写delete语句删除指针指向的内存空间。这样在程序过期的时候,对象会被删除,内存会被释放,实现指针的安全使用。
作用:一是防止忘记调用delete,二是异常安全(在一段进行了 try/catch 的代码段里面,即使你写入了 delete,也有可能因为发生异常。程序进入 catch 块,从而没能释放内存)。
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
- 不需要显式地释放资源。
- 采用这种方式,对象所需的资源在其生命期内始终保持有效。
智能指针的实现要考虑三个方面的问题:
四种智能指针:
template
class SmartPtr
{
public:
SmartPtr(T* ptr)
:_ptr(ptr)
{}
~SmartPtr()
{
//cout << "delete:" << _ptr << endl;
delete _ptr;
}
// 可以像指针一样使用
T& operator*(){
return *_ptr;
}
T* operator->(){
return _ptr;
}
private:
T* _ptr;
};
int main() {
SmartPtr sp(new int);
*sp = 10;
cout << *sp << endl;
}
在上面这个简单实现的类中没有定义拷贝构造函数和赋值重载函数,那么只能调用类中原生的拷贝构造函数和赋值重载函数。那么就会程序就会出现崩溃的问题,如下:
int main(){
SmartPtr ptr1(new int(0));
SmartPtr ptr2(ptr1);
retrun 0;}
ptr2和ptr1指向的同一块空间,当ptr2被销毁时,它会调用它的析构函数去delete该资源对象,当ptr1被销毁时,也会去调用它的析构函数去释放ptr1所指向的资源。所以,当程序结束时,ptr2被先被销毁,同时释放ptr2所指向的资源,然后ptr1被销毁,也去释放该资源对象,那么如下的资源对象同时被释放两次,所以程序就会被崩溃掉。(资源对象被释放后,如果再去释放该资源,程序就会崩溃)。
综上所述,不能使用原生的拷贝构造函数和赋值重载函数,且定义的拷贝构造函数和赋值重载函数需要考虑只能释放一次资源对象。也不能使用深拷贝,因为拷贝时我们想实现的就是p2与p1指向同一块地址。因此下面c++库中几种智能指针都是围绕如何解决拷贝这个问题来进行的。
auto_ptr是c++98版本库中提供的智能指针,该指针解决上诉的问题采取的措施是管理权转移的思想,也就是原对象拷贝给新对象的时候,原对象就会被设置为nullptr,此时就只有新对象指向一块资源空间。
但是会有新问题,比如int* p2 = p1; 再通过p1就访问不了了 。很多公司都明确的规定了,不能使用auto_ptr。
模拟实现:
template
class auto_ptr
{
public:
auto_ptr(T* ptr)
:_ptr(ptr)
{}
~auto_ptr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
_ptr = nullptr;
}
}
// 拷贝构造并不能用深拷贝,因为int * p2 = p1;要的就是p2和p1指向同一块地址
// 所以auto_ptr中通过管理权转移来实现,但是又会有新的问题
// sp2(sp1);
auto_ptr(auto_ptr& ap)
:_ptr(ap._ptr)
{
ap._ptr = nullptr;
}
// ap1 = ap3,也是资源转移
// ap1 = ap1
auto_ptr& operator=(auto_ptr& ap)
{
if (this != &ap)
{
delete _ptr;
_ptr = ap._ptr;
ap._ptr = nullptr;
}
return *this;
}
// 可以像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
简单粗暴的解决拷贝问题:禁止拷贝。
模拟实现:
template
class unique_ptr
{
public:
unique_ptr(T* ptr)
:_ptr(ptr)
{}
~unique_ptr()
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
// 可以像指针一样使用
T& operator*(){
return *_ptr;
}
T* operator->(){
return _ptr;
}
unique_ptr(const unique_ptr&) = delete;
unique_ptroperator=(const unique_ptr&) = delete;
private:
T* _ptr;
};
引用计数的原理:
- shared_ptr在内部会维护着一份引用计数,用来记录该份资源被几个对象共享。
- 当一个shared_ptr对象被销毁时(调用析构函数),析构函数内就会将该计数减1。
- 如果引用计数减为0后,则表示自己是最后一个使用该资源的shared_ptr对象,必须释放资源。
- 如果引用计数不是0,就说明自己还有其他对象在使用,则不能释放该资源,否则其他对象就成为野指针。
void SharePtrFunc(std::shared_ptr& sp, size_t n)
{
for (size_t i = 0; i < n; ++i)
{
// 这里智能指针拷贝会++计数,智能指针析构会--计数,这里是线程安全的。
std::shared_ptr copy(sp);
//这里智能指针访问管理的资源,不是线程安全的。
//所以我们看看这些值两个线程++了2n次,但是最终看到的结果,并一定是加了2n
//为什么不需要锁对其进行保护?因为*ptr返回的对象有可能被读或者被写,这个不是指针内部所考虑的,而是由调用者进行考虑的。
(*copy)++;
}
}
void test_multithread_shared_ptr()
{
std::shared_ptr p(new int(0));
const size_t n = 10000;
thread t1(SharePtrFunc, p, n);
thread t2(SharePtrFunc, p, n);
t1.join();
t2.join();
cout << p.use_count() << endl;//这里应该是安全的
cout << *p << endl;//这里可能不安全
}
当我们释放一个指向数组的指针的时候,delete[]后面的空方括号是必须存在(如下),它指示编译器此指针指向的是一个对象数组的第一个元素,如果我们在delete一个指向数组的指针中忽略了方括号,我们的程序可能在执行过程中在没有任何警告下行为异常。
我们如果在动态内存中创建出一个数组,用一个shared_ptr对象去指向该数组,当shared_ptr使用完后,就会去调用析构函数,由于shared_ptr默认的删除方式是 delete ptr,后面没有带方括号,那么程序就会崩掉。
因此,shared_ptr 类中提供了一个构造函数可以自定义一个删除器去指定析构函数的删除方式。这个自定义删除器可以是函数指针,仿函数,lamber,包装器。
template
struct DelArr//仿函数
{
void operator()(const T* ptr){
cout << "delete[]:" << ptr << endl;
delete[] ptr;
}
};
// 定制删除器 -- 删除器控制释放资源的方式
void test_shared_ptr_deletor()
{
std::shared_ptr spArr(new int[10], DelArr());
std::shared_ptr spfl(fopen("test.txt", "w"), [](FILE* ptr) {
cout << "fclose:" << ptr << endl;
fclose(ptr);
});
}
template
struct Delete
{
void operator()(const T* ptr)
{
delete ptr;
}
};
//引用计数
template>
class shared_ptr
{
private:
void AddRef()
{
_pmutex->lock();//要保证引用计数的线程安全
++(*_pcount);
_pmutex->unlock();
}
void ReleaseRef()
{
_pmutex->lock();
bool flag = false;
if (--(*_pcount) == 0)
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
//delete _ptr;
_del(_ptr);
}
delete _pcount;
flag = true;
}
_pmutex->unlock();
if (flag == true)
{
delete _pmutex;//这里锁的释放要注意
}
}
public:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
, _pcount(new int(1))
, _pmutex(new mutex)
{}
shared_ptr(T* ptr, D del)
: _ptr(ptr)
, _pcount(new int(1))
, _pmutex(new mutex)
, _del(del)
{}
~shared_ptr()
{
ReleaseRef();
}
shared_ptr(const shared_ptr& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
, _pmutex(sp._pmutex)
{
AddRef();
}
// sp1 = sp1// sp1 = sp2// sp3 = sp1;
shared_ptr& operator=(const shared_ptr& sp)
{
if (_ptr != sp._ptr)
{
ReleaseRef();
_pcount = sp._pcount;
_ptr = sp._ptr;
_pmutex = sp._pmutex;
AddRef();
}
return *this;
}
// 可以像指针一样使用
T& operator*(){
return *_ptr;
}
T* operator->(){
return _ptr;
}
int use_count(){
return *_pcount;
}
T* get() const{
return _ptr;
}
private:
T* _ptr;
int* _pcount;//计数
mutex* _pmutex;
D _del;//模拟实现定制删除器
};
不参与资源管理,不增加shared_ptr管理资源的引用计数,可以像指针一样使用
template
class weak_ptr
{
public:
weak_ptr()
:_ptr(nullptr)
{}
weak_ptr(const shared_ptr& sp)
:_ptr(sp.get())
{}
weak_ptr& operator=(const shared_ptr& sp)
{
_ptr = sp.get();
return *this;
}
// 可以像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
//对list节点使用new delete,可能会忘记delete
struct ListNode
{
ListNode* _next;
ListNode* _prev;
int _val;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
void test1()
{
ListNode* node1 = new ListNode;
ListNode* node2 = new ListNode;
node1->_next = node2;
node2->_prev = node1;
//...
//delete node1;
//delete node2;//为了防止遗忘delete,可以采用智能指针来管理节点
}
// 假设我们想用智能指针来进行管理上述节点
// 若采用shared_ptr,会引发循环引用问题,weak_ptr可以解决
struct ListNode
{
//std::shared_ptr _next;
//std::shared_ptr _prev;//这个不行,下面的对象不能正常析构
std::weak_ptr _next;
std::weak_ptr _prev;//这样才可以
int _val;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
void test_shared_ptr_cycle_ref()
{
std::shared_ptr node1(new ListNode);
std::shared_ptr node2(new ListNode);
cout << node1.use_count() << endl;//1
cout << node2.use_count() << endl;//1
// 循环引用
node1->_next = node2;
node2->_prev = node1;
// ...
cout << node1.use_count() << endl;//1
cout << node2.use_count() << endl;//1
}
循环引用分析:
在了解 STL 的四种智能指针后,大家可能会想另一个问题:在实际应用中,应使用哪种智能指针呢?下面给出几个使用指南。
长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。
void MemoryLeaks()
{
// 1.内存申请了忘记释放
int* p1 = (int*)malloc(sizeof(int));
int* p2 = new int;
// 2.异常安全问题
int* p3 = new int[10];
Func(); // 这里Func函数抛异常导致 delete[] p3未执行,p3没被释放.
delete[] p3;
}
C/C++程序中一般我们关心两种方面的内存泄漏:
- 堆内存泄漏(Heap leak)
堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。- 系统资源泄漏
指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。
总结一下: 内存泄漏非常常见,解决方案分为两种:
1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工具。