智能指针出现的原因
智能指针是专门用来解决内存泄漏问题的:
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void Func()
{
// 1、如果p1这里new 抛异常会如何?
// 2、如果p2这里new 抛异常会如何?
// 3、如果div调用这里又会抛异常会如何?
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;
}
如果在p1抛异常,那么p1内存不会被释放;在p2抛异常,那么p1和p2内存不会被释放;div抛异常,那么p1和p2不会被释放。这就造成了内存泄漏,而智能指针是专门为解决内存泄漏问题出现的。
内存泄漏
内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内 存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对 该段内存的控制,因而造成了内存的浪费。
内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现 内存泄漏会导致响应越来越慢,最终卡死。
堆内存泄漏(Heap leak) 堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一 块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分 内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。
系统资源泄漏 指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放 掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。
内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄 漏检测工具(昂贵且不靠谱)
智能指针
1.RAII的思想和原理
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内 存、文件句柄、网络连接、互斥量等等)的简单技术。
具体实现方式就是在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:不需要显式地释放资源;采用这种方式,对象所需的资源在其生命期内始终保持有效。
对于智能指针来说,在模板类中还需要重载*,->运算符让它看起来像"指针"一样。
总结一下,智能指针的实现原理就是:
1. RAII特性
2. 重载operator*和opertaor->,具有像指针一样的行为。
但是智能指针只需要注意这些就可以了吗?当然不是,还需要注意拷贝问题!这里的拷贝都会浅拷贝,所以会涉及资源管理的问题,接下来看看库里面的智能指针都是怎么解决这个问题的!
2.std::auto_ptr
auto_ptr指针在C++98中就被提出。它解决关于拷贝权限的问题很简单,就是将原指针悬空,管理权转移的思想,原指针不具备指向资源的能力了。简单模拟实现一下
template<class T>
class auto_ptr
{
public:
//ARII
auto_ptr(T* _ptr)
:ptr(_ptr)
{
}
//转移权限
auto_ptr(auto_ptr<T>& t)
:ptr(t.ptr)
{
t.ptr = nullptr;
}
~auto_ptr()
{
if (ptr)
{
cout << "delete auto" << endl;
delete ptr;
ptr = nullptr;
}
}
//像指针一样
T operator*()
{
return *ptr;
}
T* operator->() //ap.operator->()
{
return ptr;
}
T* get()
{
return ptr;
}
public:
T* ptr;
};
这种解决方法并不好,所以后来在很多企业中都禁止使用auto_ptr。
3.unique_ptr(scoped_ptr)
C++11提出了unique_ptr,但是早在C++11之前,boost库中就已经存在了scoped_ptr,是一样的,unique_ptr就是scoped_ptr。unique_ptr直接禁止拷贝!
template<class T>
class unique_ptr
{
public:
unique_ptr(T* _ptr)
:ptr(_ptr)
{
}
~unique_ptr()
{
if (ptr)
{
cout << "delete auto" << endl;
delete ptr;
ptr = nullptr;
}
}
//像指针一样
T operator*()
{
return *ptr;
}
T* operator->() //ap.operator->()
{
return ptr;
}
T* get()
{
return ptr;
}
//不让拷贝构造和赋值!防止还有人进行拷贝和赋值
unique_ptr(const unique_ptr<T>& p) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& p) = delete;
public:
T* ptr;
};
4.shared_ptr和weak_ptr
C++11还有shared_ptr,它是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。
1. shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共 享;
2. 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减 一;
3. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
4. 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对 象就成野指针了。
template<class T>
class shared_ptr
{
public:
void Release()
{
if (ptr && --(*pcount)==0)
{
cout << "delete shared" << endl;
delete ptr;
ptr = nullptr;
delete pcount;
pcount = nullptr;
}
}
shared_ptr(T* _ptr = nullptr)
:ptr(_ptr),
pcount(new int(1))
{}
shared_ptr(const shared_ptr<T>& t)
:ptr(t.ptr),
pcount(t.pcount)
{
(*pcount)++;
}
//shared_ptr<T>& operator=(shared_ptr<T>& t)
//{//避免 T* p1= new T,T* p2 = new T; shared_ptr<T> sp1(p1); shared_ptr<T> sp2(p2);sp1 = sp2;如果不释放sp1,那么p1就不会被释放。
// Release();
// ptr = t.ptr;
// pcount = t.pcount;
// (*pcount)++;
//}
shared_ptr<T>& operator=(shared_ptr<T>& t)
{//避免 T* p1= new T,T* p2 = new T; shared_ptr<T> sp1(p1); shared_ptr<T> sp2(p2);sp1 = sp2;如果不释放sp1,那么p1就不会被释放。
if (ptr != t.ptr)
{
Release();
ptr = t.ptr;
pcount = t.pcount;
(*pcount)++;
}
return *this;
}
~shared_ptr()
{
Release();
}
T* operator->()
{
return ptr;
}
T operator*()
{
return *ptr;
}
T* get() const
{
return ptr;
}
int usecount()
{
return *pcount;
}
public:
T* ptr;
int* pcount;
};
template<class T>
class weak_ptr
{
public:
weak_ptr(T* _ptr)
:ptr(_ptr)
{}
weak_ptr(shared_ptr<T>& t)
:ptr(t.ptr)
{}
weak_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (ptr != sp.get())
{
ptr = sp.get();
}
return *this;
}
// 像指针一样
T& operator*()
{
return *ptr;
}
T* operator->()
{
return ptr;
}
public:
T* ptr;
};
但是说明这样是不是就完美了?并不是,在某些情况下会出现bug。
struct ListNode
{
wzz::shared_ptr<ListNode> _next = nullptr;
wzz::shared_ptr<ListNode> _prev = nullptr;
~ListNode()
{
cout << "~ListNode" << endl;
}
};
int main()
{
wzz::shared_ptr<ListNode> p1(new ListNode);
wzz::shared_ptr<ListNode> p2(new ListNode);
//循环引用
p1->_next = p2;
p2->_prev = p1;
}
//在这个时候就不会调用~ListNode和~shared_ptr了!
解决方案:在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了
原理就是,node1->_next = node2;和node2->_prev = node1;时weak_ptr的_next和 _prev不会增加node1和node2的引用计数。
struct ListNode
{
wzz::weak_ptr<ListNode> _next = nullptr;
wzz::weak_ptr<ListNode> _prev = nullptr;
~ListNode()
{
cout << "~ListNode" << endl;
}
};
int main()
{
wzz::shared_ptr<ListNode> p1(new ListNode);
wzz::shared_ptr<ListNode> p2(new ListNode);
//循环引用
p1->_next = p2;
p2->_prev = p1;
}
//weak_ptr的唯一作用就是解决shared_ptr中存在的循环引用问题.
定制删除器
在我们写的,包括库里面的智能指针的默认清理资源方式都是delete ptr的形式,如果是这些申请资源的方式呢?
int main()
{
wzz::unique_ptr<int> p1((int*)malloc(40));
wzz::unique_ptr<int> p2(new int[10]);
wzz::unique_ptr<int> p3((int*)malloc(40));
wzz::unique_ptr<FILE> p4((FILE*)fopen("test.cpp", "r"));
}
需要针对不同的申请方式进行资源清理,这里就需要定制的清除器了,实际上就是仿函数。
unique_ptr的默认清理方式就是delete的形式。
template<class T>
struct default_delete
{
void operator()(T* ptr)
{
delete ptr;
ptr = nullptr;
}
};
template<class T, class D = default_delete<T>>
class unique_ptr
{
public:
unique_ptr(T* _ptr)
:ptr(_ptr)
{
}
~unique_ptr()
{
if (ptr)
{
cout << "delete auto" << endl;
D del;
del(ptr);
}
}
//像指针一样
T operator*()
{
return *ptr;
}
T* operator->() //ap.operator->()
{
return ptr;
}
T* get()
{
return ptr;
}
//不让拷贝构造和赋值!
unique_ptr(const unique_ptr<T>& p) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& p) = delete;
public:
T* ptr;
};
template<class T>
struct DeleteArray
{
void operator()(T* ptr)
{
delete[] ptr;
}
};
template<class T>
struct Free
{
void operator()(T* ptr)
{
free(ptr);
}
};
struct Fclose
{
void operator()(FILE* ptr)
{
fclose(ptr);
}
};
int main()
{
wzz::unique_ptr<int> p1((int*)malloc(40));
wzz::unique_ptr<int> p2(new int[10]);
wzz::unique_ptr<int> p3((int*)malloc(40));
wzz::unique_ptr<FILE> p4((FILE*)fopen("test.cpp", "r"));
}
对于shared_ptr的删除方式和unique_ptr是不太一样的,他支持构造函数传参:
其内部实现比较复杂。
总结
智能指针就是RAII的思想,像指针一样去使用,考虑拷贝问题。
auto_ptr是权限转移,比较鸡肋,不需要拷贝有更新的unique,拷贝的话又有shared;
unique_ptr不支持拷贝,在不需要拷贝的情况下比较舒服;
shared_ptr比较靠谱,但是会有"循环引用"的bug出现,用weak_ptr来解决。