为什么需要智能指针?
- 申请出来的空间没有进行释放,存在内存泄漏。
- 异常安全问题,如果在malloc和free之间如果程序抛出异常,那么还是有内存泄漏。这样的问题就存在程序异常安全问题。
RAII
RAII是一种利用对象的生命周期来控制资源的简单技术。
在对象构造时获取资源,对象所控制的资源在整个生命周期内有效,最后在对象调用析构函数的时候对资源进行释放,借此我们把资源的申请和释放问题交给了一个对象。
- 不需要显示地释放资源。
- 采用这种方式资源在其生命周期内始终有效。
智能指针实现原理
- 采用RAII思想
- 模拟指针特性,在类中实operator*()和operator->()的运算符重载。
auto_ptr:
auto_ptr是C++98版本中提供的。自己简单模拟实现了一个auto_ptr
class Data
{
public:
Data()
{}
~Data()
{}
int _year;
};
template<class T>
class AutoPtr
{
public:
AutoPtr(T* ptr = nullptr)
:_ptr(ptr)
{}
AutoPtr(AutoPtr<T>& sp)
:_ptr(sp._ptr)
{
sp._ptr = nullptr;
}
~AutoPtr()
{
if (_ptr)
delete _ptr;
}
AutoPtr<T>& operator=(AutoPtr<T>& sp)
{
if (this != &sp){
if (_ptr)
delete _ptr;
_ptr = sp._ptr;
sp._ptr = nullptr;
}
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T *_ptr;
};
int main()
{
AutoPtr<Data> ap(new Data);
AutoPtr<Data> ap1(ap);
ap->_year = 2019;
return 0;
}
可是C++98中给的auto_ptr会出现内存管理权转移现象,当ap拷贝构造给ap1时,ap就失去了对自己内存空间的操作权。所以当ap再次去访问这块内存空间的时候,运行期间就会报错。那么该如何解决这个问题呢?
unique_ptr:
接下来,为了解决auto_ptr的问题,下面简单模拟一个unique_ptr的实现
template<class T>
class UniquePtr
{
public:
UniquePtr(T* ptr = nullptr)
:_ptr(ptr)
{}
~UniquePtr()
{
if (_ptr)
delete _ptr;
}
T* operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
//c++98
UniquePtr(const UniquePtr<T>&);
UniquePtr<T>& operator=(const UniquePtr<T>&);
//c++11
UniquePtr(const UniquePtr<T>&) = 0;
UniquePtr<T>& operator=(const UniquePtr<T>&) = 0;
T* _ptr;
};
unique_ptr为了解决auto_ptr中多个指针不能指向同一块地址空间而造成继续访问同一块资源时出现问题和可能出现多次析构这个函数的问题,做法是防止对象的拷贝构造和赋值运算符重载,而一份资源只能被管理一块内存地址空间。
shared_ptr:
以上两个智能指针都不能体现出指针的特性,指针要能够有不同指针指向同一块内存地址空间。而share_ptr解决了这个问题,解决办法是给出每份资源维护一个计数器,当最后一个对象享用这块资源后,对资源进行释放。也就是当计数器的值为0时,此时这个对象释放这块资源,而当计数器不为0时,就不释放资源。
但是这里要考虑到一个问题,就是计数器是一个临界资源,当多个线程来访问这个临界资源时会出现线程安全问题,所以这里因为了C++中锁的概念。当有线程操作这个临界资源时就对临界资源加锁,当操作完成后就解锁。
template<class T>
class SharePtr
{
public:
SharePtr(T* ptr = nullptr)
:_ptr(ptr)
, _pCount(nullptr)
{
if (_ptr){
_pCount = new int(1);
_pm = new mutex;
}
}
~SharePtr()
{
if (_ptr == nullptr && 0 == SubRefCount()){
delete _ptr;
delete _pCount;
delete _pm;
}
}
SharePtr(const SharePtr<T>& sp)
:_ptr(sp._ptr)
, _pCount(sp._pCount)
, _pm(sp._pm)
{
if (_ptr)
AddRefCount();
}
SharePtr<T>& operator=(const SharePtr<T>& sp)
{
if (this != &sp){
if (_ptr && 0 == SubRefCount()){
delete _ptr;
delete _pCount;
delete _pm;
}
_ptr = sp._ptr;
_pCount = sp._pCount;
if (_ptr)
AddRefCount();
}
return *this;
}
T* operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
int GetCount()
{
assert(_pCount);
return *_pCount;
}
int AddRefCount()
{
_pm->lock();
++(*_pCount);
_pm->unlock();
return *_pCount;
}
int SubRefCount()
{
_pm->lock();
--(*_pCount);
_pm->unlock();
return *_pCount;
}
private:
T* _ptr;
int* _pCount;
mutex* _pm;
};
void TestSharePtr()
{
SharePtr<int> pb1(new int);
SharePtr<int> pb2(pb1);
SharePtr<int> pb3(new int);
pb2 = pb3;
cout << pb1.GetCount() << endl;
}
int main()
{
TestSharePtr();
return 0;
}
但是share_ptr还是有问题,就是不能出现循环引用。看下面代码
struct ListNode
{
int _data;
shared_ptr<ListNode> _pre;
shared_ptr<ListNode> _pNext;
~ListNode()
{
cout << " ~ListNode()" << endl;
}
};
int main()
{
shared_ptr<ListNode> n1(new ListNode);
shared_ptr<ListNode> n2(new ListNode);
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
n1->_pNext = n2;
n2->_pre = n1;
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
return 0;
}
如果出现以上现象那么问题就来了,当出现循环引用时,当析构了n1和n2时计数器减1
但是此时的n1的_pNext指向n2,而n2的_pre指向n1,所以此时n1和n2就不能被析构
如果要析构n1那么就要析构n1的_pNext,如果要析构n2那么就要析构n2的pre
所以就出现了恶性循环,这块地址空间不能被析构。
weak_ptr:
weak_ptr就是为了配合shared_ptr而引入的一种智能指针,它指向一个由shared_ptr管理的对象而不影响所指对象的声明周期,在引用计数的场景下用weak_ptr定义_pre和_pNext就可以了。因为当weak_ptr的_pNext和_pre不会增加n1的引用计数。
但是要注意的是---weak_ptr并没有重载operator*()和operator->(),所以不可以当weak_ptr来访问对象。
struct ListNode
{
int _data;
weak_ptr<ListNode> _pre;
weak_ptr<ListNode> _pNext;
~ListNode()
{
cout << " ~ListNode()" << endl;
}
};
int main()
{
shared_ptr<ListNode> n1(new ListNode);
shared_ptr<ListNode> n2(new ListNode);
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
n1->_pNext = n2;
n2->_pre = n1;
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
return 0;
}
而此时就出现了析构函数。
最后的最后还有一个问题,就是我们之前给出来的对象都是用new/delete进行申请和释放空间的,拿如果要是实现一个malloc/free和new[]/delete[]呢?需要重新下一个类吗?
这里就用到了一个STL六大组件中的之一就是仿函数。
什么是仿函数?
仿函数就是行为具有函数功能的class的对象就是仿函数。仿函数一般有成员ret_type operator();
仿函数的优点:
1、仿函数是对象,可以拥有成员函数和成员变量,即仿函数拥有状态。
2、每个仿函数都有自己的类型。
3、仿函数通常比一般函数快,因为仿函数在编译期间实现内联,性能要告。
4、重要的是仿函数结合泛型编程,仿函数的泛化大大增强了它的应用性。
template<class T>
class FreeFunc
{
public:
void operator()(T* ptr)
{
cout << "free :" << ptr << endl;
free(ptr);
}
};
template<class T>
class DeleteArrayFunc
{
public:
void operator()(T* ptr)
{
cout << "Delete[]:" << ptr << endl;
delete[] ptr;
}
};
int main()
{
FreeFunc<int> f;
shared_ptr<int> sp1((int*)malloc(4),f);
DeleteArrayFunc<int> d;
shared_ptr<int> sp2(new int[3], d);
return 0;
}