1. 什么是智能指针
简而言之,为了更安全的使用指针。
实现方式简单来说,就是用一个模板类把一般的指针包装起来。用这个类来维护内部指针的释放操作。
std中有四种智能指针。
- auto_ptr(已弃用)
- unique_ptr
- shared_ptr
- weak_ptr
2. auto_ptr
比较简单的智能指针,实现逻辑如下代码。
实现的原则是:内存空间只能由一个指针所拥有。
存在的问题:拷贝和赋值后,原对象内部的指针会为空,有风险。
template<class T>
class autoPtr{
public:
autoPtr(T *a):m_ptr(a){
}
~autoPtr(){
if(m_ptr){
delete m_ptr;
m_ptr = nullptr;
}
}
// 拷贝构造 权限转移
autoPtr(autoPtr<T> &a):m_ptr(a.m_ptr) {
a.m_ptr = nullptr;
}
// 赋值 权限转移
autoPtr& operator= (autoPtr<T> &a){
if(this != &a){
if(m_ptr) delete m_ptr;
m_ptr = a.m_ptr;
a.m_ptr = nullptr;
}
return *this;
}
T* operator->(){
return m_ptr;
}
T& operator* (){
return *m_ptr;
}
private:
T *m_ptr;
};
3. unique_ptr
是对auto_ptr的改进,实现逻辑如下代码。
实现的原则是:内存空间只能由一个指针所拥有,但是禁止了拷贝和赋值。
在C++中unique_ptr的进制拷贝有一个特例,如果一个unique_ptr是将亡值的话,是允许的,比如unique_ptr作为函数的返回值时,是允许的。
// uniquePtr 对于autoPtr来说 禁止了拷贝和赋值
template<class T>
class uniquePtr{
public:
uniquePtr(const T*a):m_ptr(a) { };
~uniquePtr(){ if(m_ptr) delete m_ptr; }
T& operator *(){
return *m_ptr;
}
T* operator->(){
return m_ptr;
}
T* get(){ return m_ptr; }
private:
T *m_ptr;
// 禁止拷贝和赋值
uniquePtr(const uniquePtr<T> &a) { };
uniquePtr<T> & operator = (const uniquePtr<T> &a) { };
};
unique_ptr禁止了拷贝,那么如何把unique_ptr作为参数传递呢?
- 法1:使用指针或者引用
void func(unique_ptr<int> &a){
}
*法2: 暂时转移内存的所有权,函数返回时,再把所有权交回来。
unique_ptr<int> func2(unique_ptr<int> a){
cout << *a << endl;
return a;
}
int main()
{
unique_ptr<int> a(new int(6));
// 暂时释放所有权
a = func2(unique_ptr<int>(a.release()));
return 0;
}
- 法3:move语义
void func1(unique_ptr<int> a)
{
cout << *a << endl;
}
int main()
{
unique_ptr<int> a(new int(6));
func1(std::move(a));
return 0;
}
4. shared_ptr
实现逻辑如下代码。
实现的原则是:内存空间可以由多个指针所拥有,但是要控制内存释放的时机,这个时机由“引用计数
”来实现。
存在为问题:会存在循环引用问题,导致内存无法正常释放。
template<class T>
class sharedPtr{
public:
sharedPtr(T *a):m_ptr(a), m_count(new int(1)) { };
~sharedPtr(){
if(--(*m_count) == 0){
delete m_ptr;
delete m_count;
m_ptr = nullptr;
m_count = nullptr;
}
}
T& operator *(){
return *m_ptr;
}
T* operator->(){
return m_ptr;
}
// 拷贝构造
sharedPtr(const sharedPtr<T> &a):m_ptr(a.m_ptr),m_count(a.m_count) {
++(*m_count);
}
// 赋值函数
sharedPtr<T> & operator=(const sharedPtr<T> &a){
if(m_ptr != a.m_ptr){
// this 原来的那个区域处理
if(--(*m_count) == 0){ // 这里不仅仅是检查,也会引用计数减1
delete m_ptr;
delete m_count;
m_ptr = nullptr;
m_count = nullptr;
}
m_ptr = a.m_ptr;
m_count = a.m_count;
++(*m_count);
}
return *this;
}
int use_count(){
return *m_count;
}
int * use_countPtr(){
return m_count;
}
T* get(){
return m_ptr;
}
private:
T *m_ptr;
int *m_count;
};
shared_ptr会产生循环引用
问题,如下代码
template<class T>
class sharedPtr{
public:
sharedPtr(T *a):m_ptr(a), m_count(new int(1)) { };
~sharedPtr(){
if(--(*m_count) == 0){
delete m_ptr;
delete m_count;
m_ptr = nullptr;
m_count = nullptr;
}
}
T& operator *(){
return *m_ptr;
}
T* operator->(){
return m_ptr;
}
// 拷贝构造
sharedPtr(const sharedPtr<T> &a):m_ptr(a.m_ptr),m_count(a.m_count) {
++(*m_count);
}
// 赋值函数
sharedPtr<T> & operator=(const sharedPtr<T> &a){
if(m_ptr != a.m_ptr){
// this 原来的那个区域处理
if(--(*m_count) == 0){ // 这里不仅仅是检查,也会引用计数减1
delete m_ptr;
delete m_count;
m_ptr = nullptr;
m_count = nullptr;
}
m_ptr = a.m_ptr;
m_count = a.m_count;
++(*m_count);
}
return *this;
}
int use_count(){
return *m_count;
}
int * use_countPtr(){
return m_count;
}
T* get(){
return m_ptr;
}
private:
T *m_ptr;
int *m_count;
};
int main() {
struct Node{
Node():pre(nullptr), next(nullptr) { }
int data;
sharedPtr<Node> pre;
sharedPtr<Node> next;
};
int *c1 = nullptr;
int *c2 = nullptr;
{ // 这里一个花括号,限制了作用域
sharedPtr<Node> a(new Node);
sharedPtr<Node> b(new Node);
a->next = b;
b->pre = a;
// 把各自的m_count取出来
c1 = a.use_countPtr();
c2 = b.use_countPtr();
cout << *c1 <<endl;
cout << *c2 <<endl;
}
// 因为上边a 和 b在析构时,没有释放m_ptr和m_count,所以,下边两行可以访问,并且为1
cout << *c1 <<endl;
cout << *c2 <<endl;
return 0;
}
对象a和b本身为sharedPtr类型,内部各包含一个Node*
并且开辟了内存。为了方便后续描述,a中申请的这块内存称之为A_Node*
,b中的这块内存称之为B_Node*
。
a和b初始化时,内部的引用计数均为1
,因为这个时候只有自己使用自己内部的这块内存空间。
当执行了
a->next = b;
b->pre = a;
这两行代码以后,a和b内部的引用技术均为2
。此时对于a
来说,不仅仅自己使用A_Node*
,同时b
中的一个sharedPtr<Node>
类型(与a同类型)的pre
,也使用A_Node*
,所以a
中的引用计数为2,b中pre内部的引用计数也为2。
对于b来说,同理,不仅仅自己使用B_Node*
,同时a
中的一个sharedPtr<Node>
类型(与b同类型)的next
,也使用B_Node*
,所以b
中的引用计数为2,a中next内部的引用计数也为2。
重点来了,当程序执行到花括号
外边的时候,a和b两个对象的生命周期到了,要执行各自的析构函数。但是!执行a的析构函数时,由于引用计数为2,减去1之后不为0,所以,不会释放a内部的m_ptr和m_count
,只不过把m_count
变为了1,由于引用计数机制的存在,此时b中的pre,这个智能指针内部的引用计数也会变为1。总结一下,就是a这个对象被析构了,但是 A_Node* 没释放。
同理,执行b的析构函数时,由于引用计数为2,减去1之后不为0,所以,不会释放b内部的m_ptr和m_count
,只不过把m_count
变为了1,由于引用计数机制的存在,此时a中的next,这个智能指针内部的引用计数也会变为1。总结一下,就是b这个对象被析构了,但是 B_Node* 没释放。
经过了a和b两个对象的析构函数,现在的情况是怎么样的?
A_Node*
中的next指向B_Node*
。
B_Node*
中的pre指向A_Node*
。
此时呢,形成了环。
在没有人为干预的情况下,想释放A_Node*
,就必须先释放 B_Node*
。
因为一旦B_Node*
释放,会触发B_Node*
内部pre指针的析构函数,因此这时pre内部的引用计数为1,减1之后变为0,所以,可以释放掉pre所指向的内存空间,也就是会释放掉A_Node*
,然后接着会触发A_Node*
内部next指针的析构函数,同理,next指针会把自己指向的B_Node*
给释放掉,这样,A_Node*和B_Node*
这两块被a和b遗留下来的内存空间就可以正常被释放。
但是!!!问题来了,想释放B_Node*
,就必须先释放 A_Node*
。
二者互相等待,都等待对方先释放,这个就是循环引用
。
解决办法
把Node中的两个shared_ptr
换成weak_ptr
即可,这样,a和b的引用计数均为1,可以正常是释放。
5. weak_ptr
是对shared_ptr的修改,实现逻辑如下代码。
实现的原则是:允许一块内存由多个指针拥有,与shared_ptr不同的是,不使用引用计数。
存在的问题:适用性不如shared_ptr广,因为一个weak_ptr释放的时候,就会把自己指向的区域释放掉,可能会重复释放。
// weak ptr ,与sharedPtr的不同在于,没有引用计数,总是,weakPtr,共享,但不计数,直接释放
template<class T>
class weakPtr{
public:
weakPtr(T *a):m_ptr(a){ }
~weakPtr(){
if(m_ptr) {
delete m_ptr;
m_ptr = nullptr;
}
}
// 赋值
weakPtr<T> & operator= (const weakPtr<T> &a){
if(m_ptr) delete m_ptr;
m_ptr = a.m_ptr;
return *this;
}
weakPtr<T> & operator= (T *a){
m_ptr = a;
return *this;
}
T& operator *(){
return *m_ptr;
}
T* operator->(){
return m_ptr;
}
private:
T *m_ptr;
};