文章目录
C++ 智能指针shared_ptr 和 unique_ptr浅析
我们知道,对于JAVA和PYTHON这种语言,会把所有的东西当做对象处理,并且不用自己管理任何内存,如果需要使用,值需要new即可。这对于普通C/C++程序员来说,简直就是福音。
在C语言中,如果你需要使用堆内存,那么需要如下使用:
void fun()
{
int* p = (int*)malloc(sizeof (int));
if (p == null)
{
//error0
return;
}
if (/*error1*/)
{
free(p);
return;
}
if (/*error2*/)
{
free(p);
return;
}
if (/*error3*/)
{
free(p);
return;
}
free(p);
}
上面这个代码光是看起来就比较蛋疼了;因为C语言需要自己管理内存,如果函数退出,就需要管理自己分配的内存,如果一个函数退出点比较多的话每个地方都需要释放内存。这对于我这种菜鸟级别的程序员来说是很容易就忘记释放而返回的。
为了解决这个问题,C++就提出了智能指针的解决方案。
1. 入门
1.1 思路
如果从零出发,我们怎么样来自动管理自己的内存呢?当然一个很直白的想法就是利用类的析构函数自动释放内存。例如:
- 一个专门类专门管理指针信息。
- 类被析构的时候自动释放管理的指针。
这个其实就是智能指针的思路,下面来实现一下这个思路。
1.2 实现
template<typename T>
class AutoPtr
{
public:
AutoPtr() : ptr(nullptr) {}
explicit AutoPtr(T* p) : ptr(p) {}
AutoPtr(const AutoPtr&) = delete;
~AutoPtr()
{
if (ptr != nullptr)
{
delete ptr;
ptr = nullptr;
}
std::cout << "~AutoPtr called" << std::endl;
}
void set(T* p)
{
if (ptr != nullptr)
{
delete ptr;
}
ptr = p;
}
operator bool() { return ptr != nullptr; }
T* operator->() { return ptr; }
T& operator*() { return *ptr; }
private:
T* ptr;
};
void AutoPtrTest()
{
AutoPtr<int> p(new int(100));
std::cout << *p << std::endl;
*p = 200;
std::cout << *p << std::endl;
}
运行结果如下:
100
200
~AutoPtr called
这个就是一个最最简单的利用类的析构函数来管理堆内存的例子,当然这个例子简直简单到不行了,简单到基本不能太大的使用;但是不用急,C++的智能指针已经提供了全部的解决方案了。
2. shared_ptr 和 unique_ptr
2.1 基本的规则
shared_ptr的意思是这个指针是共享的,大家都可以使用;这个里面使用一个引用计数来管理使用者的数目,例如:
- A使用了一个内存对象,此时引用计数为1.
- B引用了同一个内存对象,此时引用计数为2.
- C也引用了同一个内存对象,此时引用计数为3.
- 此时B不使用内存了,释放了
shared_ptr,此时引用计数变为2. - 接着A也不使用了,释放了
shared_ptr,此时引用计数变为1. - 最后C也不使用了,释放了
shared_ptr,此时引用计数变为0,当引用计数为0之后,释放内存对象占用的内存。
unique_ptr的意思是这个指针只有我自己能够使用,别人都不能用,所以unique_ptr不支持拷贝。
2.2 shared_ptr的使用
template<class _Ty>
class shared_ptr
: public _Ptr_base<_Ty>
{
}
template<class _Ty>
class _Ptr_base
{
private:
element_type * _Ptr{nullptr};
_Ref_count_base * _Rep{nullptr};
}
这里对象和引用计数都是使用指针来操作,这样的话,新建对象的时候就可以简单的值拷贝(拷贝指针)就可以了。
shared_ptr提供两种方式初始化:
- 构造的时候提供指针。
std::make_shared<xxx>构造。
// make_shared example
#include <iostream>
#include <memory>
int main () {
std::shared_ptr<int> foo = std::make_shared<int> (10);
// same as:
std::shared_ptr<int> foo2 (new int(10));
auto bar = std::make_shared<int> (20);
auto baz = std::make_shared<std::pair<int,int>> (30,40);
std::cout << "*foo: " << *foo << '\n';
std::cout << "*bar: " << *bar << '\n';
std::cout << "*baz: " << baz->first << ' ' << baz->second << '\n';
return 0;
}
2.3 unique_ptr
这个是指一个指针只能被一个类管理,也就是说,这个类不支持普通的拷贝和赋值(因为底层直接使用浅拷贝,会导致问题)。
template<class _Ty,
class _Dx> // = default_delete<_Ty>
class unique_ptr
: public _Unique_ptr_base<_Ty, _Dx>
{
}
2.4 管理自己的释放函数
无论是shared_ptr还是unique_ptr都是使用delete来释放内存的,但是如果我们不是使用delete来释放内存呢?那就需要自己设置释放函数了。
对于释放函数shared_ptr和unique_ptr是有点不同的。
2.4.1 shared_ptr
这个类的构造函数如下:
//default (1)
constexpr shared_ptr() noexcept;
//from null pointer (2)
constexpr shared_ptr(nullptr_t) : shared_ptr() {}
//from pointer (3)
template <class U> explicit shared_ptr (U* p);
//with deleter (4)
template <class U, class D> shared_ptr (U* p, D del);
也就是说,我们可以在构造函数中设置释放函数,例如如果我们想用shared_ptr管理HANDLE句柄就可以如下使用:
void handle_ptr_delete(PHANDLE pHandle)
{
CloseHandle(*pHandle);
delete pHandle;
}
std::shared_ptr<HANDLE> make_handle_ptr(HANDLE Handle)
{
PHANDLE pHandle = new HANDLE(Handle);
return std::shared_ptr<HANDLE>(pHandle, handle_ptr_delete);
}
int main(int args, char* argv[])
{
HANDLE hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
std::shared_ptr<HANDLE> Handle = make_handle_ptr(hEvent);
SetEvent(*Handle);
return 0;
}
2.4.2 unique_ptr
unique_ptr的声明如下:
template<class _Ty,
class _Dx> // = default_delete<_Ty>
class unique_ptr
: public _Unique_ptr_base<_Ty, _Dx>
{
}
从声明我们可以知道, unique_ptr 的删除参数是在模板参数中的。
例如使用如下:
void handle_ptr_delete(PHANDLE pHandle)
{
CloseHandle(*pHandle);
delete pHandle;
}
int main(int args, char* argv[])
{
HANDLE hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
std::unique_ptr<HANDLE, decltype(handle_ptr_delete)*> Handle(new HANDLE(hEvent), handle_ptr_delete);
SetEvent(*Handle);
return 0;
}
2.4.3 区别
shared_ptr和unique_ptr的释放函数使用的时候还是有不同的。那么为什么C++为什么会带来这种差异呢?我自己猜测主要是如下原因:
shared_ptr主要是灵活,shared_ptr这个指针在使用的时候可以随意改变;那么将删除函数设置为成员,使用上面带来了很大的便利,例如template <class U, class D> void reset (U* p, D del);直接设置相关信息。unique_ptr这个东西比较固定,无法拷贝和其他操作,主要是用来管理单个内存对象,所有就不用那么灵活的使用了,为了性能上面的考虑unique_ptr就直接使用了模板参数来设置删除函数(毕竟模板参数在编译期间确定,性能会有所提升)。
3. 总结
通过shared_ptr和unique_ptr我们可以方便的管理堆内存了;更加重要的一个问题是,不用担心底层抛出异常而跳出内存释放的逻辑(因为析构函数永远都会被调用)。
其实除了shared_ptr和unique_ptr之外还有weak_ptr,weak_ptr比较特殊,下面专门抽一篇文章来讲解。