智能指针的作用
RAII(Resource Acquisition Is Initialization)
代码中经常会忘掉释放动态开辟的资源,引用智能指针可用于动态资源管理,资源分配即初始化,定义一个类来封装资源的分配和释放,在构造函数完成资源 的分配和初始化,在析构函数完成资源的清理,可以保证资源的正确初始化和释放。
智能指针的原理:智能指针是一个类,这个类的构造函数中传入一个普通指针,析构函数中释放传入的指针。智能指针的类是栈上的对象,智能指针指向堆上开辟的空间,函数结束时,栈上的函数会自动被释放,智能指针指向的内存也会随之消失,防止内存泄漏。
智能指针的实现需要实现构造、析构、拷贝构造、操作符重载。
常见的几种智能指针:Auto_Ptr 、Scoped_Ptr、Shared_Ptr、Weak_Ptr
首先定义一个普通类,然后用上述智能指针来对该类进行一些操作
#include<iostream>
#include"smart_ptr.h"
using namespace std;
class A
{
public:
A()
{
cout << "A()" << endl;
}
~A()
{
cout << "~A()" << endl;
}
void Print()
{
cout << "Print()" << endl;
}
};
首先来看Auto_Ptr
template<class T>
class AutoPtr
{
public:
AutoPtr(T* ptr)
:_ptr(ptr)
{
}
AutoPtr(AutoPtr<T>& ap) //管理权转移 拷贝构造完后后,原来的对象没有管理权 置空
:_ptr(ap._ptr)
{
ap._ptr = NULL;
}
AutoPtr<T>& operator=(AutoPtr<T>& ap)
{
if (this != &ap)
{
delete _ptr;
_ptr = ap._ptr;
ap._ptr = NULL;
}
return *this;
}
~AutoPtr()
{
delete _ptr;
}
T* operator->()
{
return _ptr;
}
T& operator*()
{
return *_ptr;
}
protected:
T* _ptr;
};
void TestAutoPtr()
{
AutoPtr<A> ap1(new A);
//AutoPtr<A> ap2 = ap1;
ap1->Print();
// ap1.operator->(&ap1)->Print() => (_ptr)->Print() 指针(A*类型)->函数 编译器为提高可读性,忽略一个->
(*ap1).Print();
// ap1.operator*(&ap1) => (*_ptr).print() 对象(A类型).(点运算符) 函数
}
运行结果如下:
上述代码相当于将资源类A的管理权交给智能指针ap1去管理,ap1使用完后调用析构,自动释放资源A。该智能指针实现了自动释放开辟的资源,并且可以像指针一样使用
但是该智能指针有一个缺陷,在进行拷贝构造的时候,会将管理权转移,从而导致原先的指针失去管理权(被置空)
为了防止这种管理权转移而被置空的现象出现,我们引用了 一种可以防止拷贝的智能指针ScopedPtr,代码如下:
template<class T>
class ScopedPtr
{
public:
ScopedPtr(T* ptr)
:_ptr(ptr)
{
}
~ScopedPtr()
{
delete _ptr;
}
T* operator->()
{
return _ptr;
}
T& operator*()
{
return *_ptr;
}
private: //将拷贝构造函数和赋值运算符重载函数声明为私有(无需定义)防止用户拷贝
ScopedPtr(const ScopedPtr<T>& sp);
ScopedPtr<T>& operator=(const ScopedPtr<T>& sp);
protected:
T* _ptr;
};
void TestScopedPtr()
{
ScopedPtr<A> sp1(new A);
//ScopedPtr<A> sp2 = sp1; 不可访问
sp1->Print();
(*sp1).Print();
}
这种智能指针的原理就是将拷贝构造函数和赋值运算符重载函数声明为私有成员(不需要定义),这样我们就不能访问这两个函数,从而实现了防拷贝。
运行结果如下:
如果我们想要使用拷贝功能的话,上述两种智能指针都无法实现,这就需要引入另一种智能指针,SharedPtr,该智能指针的原理就是引用一个计数,每次拷贝构造或者复制时都将计数加一,释放时如果计数为1则释放资源,如果不为1则将计数减一,从而解决了多次释放资源的问题(只释放一次)。
代码如下:
//SharedPtr 共享指针,引用计数
template<class T>
class SharedPtr
{
template<class T>
friend class WeakPtr;
public:
SharedPtr(T* ptr=NULL)
:_ptr(ptr), _pCount(new int(1))
{
}
SharedPtr(SharedPtr<T>& sp)
{
_ptr = sp._ptr;
_pCount = sp._pCount;
(*_pCount)++;
}
SharedPtr<T>& operator=(SharedPtr<T>& sp)
{
if (this->_ptr != sp._ptr) //释放this之前也要先判断this的计数是否为1
{
if (--(*_pCount) == 0)
{
delete _ptr;
delete _pCount;
}
_ptr = sp._ptr;
_pCount = sp._pCount;
++(*_pCount);
}
return *this;
}
~SharedPtr()
{
if ((*_pCount) == 1)
{
delete _ptr;
delete _pCount;
}
else
{
--(*_pCount);
}
}
T* operator->()
{
return _ptr;
}
T& operator*()
{
return *_ptr;
}
protected:
T* _ptr;
int *_pCount;
};
void TestSharedPtr()
{
SharedPtr<A> s1(new A);
SharedPtr<A> s2 = s1;
SharedPtr<A> s3;
s1->Print();
(*s1).Print();
s2->Print();
(*s2).Print();
s1 = s3;
}
值得注意的是,我们需要将计数变量声明为int* 类型,这样我们在进行加减操作时,可以确保所有指向同一资源的智能指针的计数都能得到相应的加减操作。另外,在进行赋值时,我们需要先判断左指针的计数是否为1,若为1则将左值指针释放然后赋值,若不为1,将左值指针的计数减一,再进行复制。
运行结果如下:
循环引用问题:
假设我们定义一个链表结构体,然后定义两个节点存放到SharedPtr中,假设我们将n1的next指向n2,将n2的prev指向n1,(next和prev由默认构造函数初始化,指向NULL)然后我们会发现程序并没有释放掉资源(没有调用析构)。
struct ListNode
{
SharedPtr<ListNode> _next;
SharedPtr<ListNode> _prev;
ListNode()
{
cout << "ListNode()" << endl;
}
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
void TestCycle() //SharedPtr 循环引用问题
{
SharedPtr<ListNode> n1 = new ListNode;
SharedPtr<ListNode> n2 = new ListNode;
n1->_next = n2;
n2->_prev = n1;
}
运行结果:
原因是,我们将n1的next指向n2,调用赋值运算符重载函数,此时智能指针n2计数加一(变为2),同样将n2的prev指向n1,智能指针n1计数也会加一,这时我们程序运行结束调用析构时,计数并不为1,所以无法释放资源。
为了解决这类问题,需要用到另一种智能指针WeakPtr(弱指针),该智能指针的功能是辅助SharedPtr(共享指针),解决循环引用问题。
原理:
我们可以将上述问题的next,prev声明为WeakPtr,在WeakPtr类中的赋值运算符重载函数中不进行计数操作。这样我们就可以成功释放资源。
代码如下:
template<class T>
class WeakPtr
{
public:
WeakPtr()
:_ptr(NULL)
{
}
WeakPtr(WeakPtr<T>& wp)
:_ptr(wp._ptr)
{
}
WeakPtr(const SharedPtr<T>& sp)
:_ptr(sp._ptr)
{
}
WeakPtr<T>& operator=(SharedPtr<T>& sp)
{
_ptr = sp._ptr; //无法在WeakPtr类中访问SharedPtr类的保护成员_ptr 需要将WeakPtr声明为SharedPtr的友元类
return *this;
}
T* operator->()
{
return _ptr;
}
T& operator*()
{
return *_ptr;
}
//~WeakPtr()
//{
// delete _ptr;
//}
private:
T* _ptr;
};
struct ListNode
{
WeakPtr<ListNode> _next;
WeakPtr<ListNode> _prev;
ListNode()
{
cout << "ListNode()" << endl;
}
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
void TestCycle() //SharedPtr 循环引用问题
{
SharedPtr<ListNode> n1 = new ListNode;
SharedPtr<ListNode> n2 = new ListNode;
n1->_next = n2;
n2->_prev = n1;
}
需要注意的几点:
因为我们执行的操作 n1->_next = n2; n2->_prev = n1;是将SharedPtr类型的变量赋值给WeakPtr类型变量,所以需要我们在写拷贝构造函数和赋值运算符重载函数时,传入SharedPtr类型的引用。
在赋值运算符重载函数里面,我们进行了操作_ptr = sp._ptr;因为WeakPtr类中无法访问SharedPtr中的保护成员,所以需要将WeakPtr类声明为SharedPtr类的友元类。
该类需要一个默认的构造函数,给next和prev设初值为空。
这样就可以解决循环引用问题,实现资源的正确释放
程序运行结果:
以上便是对智能指针的介绍。