智能指针
一、智能指针概念
智能指针是存储指向动态分配(堆)对象指针的类。除了能够在适当的时间自动删除指向的对象外,他们的工作机制很像C++的内置指针。智能指针在面对异常的时候格外有用,因为他们能够确保正确的销毁动态分配的对象。他们也可以用于跟踪被多用户共享的动态分配对象。
二、为什么需要智能指针
下面我们先分析一下下面这段程序有没有什么内存方面的问题?
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void func()
{
int* p1 = new int[10]; // 这里可能会抛异常,此时抛异常内存会分配失败,程序结束后不会造成内存泄漏。
int* p2 = new int[10]; // 这里可能会抛异常,此时抛异常会导致p2以及后面的内存分配失败,退出程序后,
//由于p1已经成功申请内存,但是C++没有内存回收机制,因此会造成内存泄漏。
int* p3 = new int[10]; // 这里可能会抛异常
try
{
div();
}
catch (...)
{
delete[] p1;
delete[] p2;
delete[] p3;
throw;
}
delete[] p1;
delete[] p2;
delete[] p3;
}
int main()
{
try
{
func();
}
catch (const exception& e)
{
cout << e.what() << endl;
// ...
}
return 0;
}
关于内存泄漏见文章【C++ 内存管理】
三、智能指针设计原理剖析
RAII思想
RAII(Resource Acquisition Is Initialization),也称为 “资源获取就是初始化” ,是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。RAII是C++语言的一种管理资源、避免泄漏的惯用法。C++标准保证任何情况下,已构造的对象最终会销毁,即它的析构函数最终会被调用。简单的说,RAII 的做法是使用一个对象,在其构造时获取资源,在对象生命期控制对资源的访问使之始终保持有效,最后在对象析构的时候释放资源。这种做法有两大好处:
- 不需要显式地释放资源。
- 采用这种方式,对象所需的资源在其生命期内始终保持有效
利用RAII思想设计的SmartPtr类
template<class T>
class SmartPtr {
public:
SmartPtr(T* ptr = nullptr)
: _ptr(ptr)
{}
~SmartPtr()
{
if (_ptr)
delete _ptr;
}
private:
T* _ptr;
};
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void Func()
{
SmartPtr<int> sp1(new int);
SmartPtr<int> sp2(new int);
cout << div() << endl;
}
int main()
{
try {
Func();
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}
利用RAII的思想,将变量的资源交给一个对象管理,在对象的生命周期结束之时自动释放资源,巧妙地解决了内存泄漏的问题。
但是上述的SmartPtr还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可以通过->去访问所指空间中的内容,因此:AutoPtr模板类中还得需要将 * 、-> 重载下,才可让其像指针一样去使用。
template <class T>
class SmartPtr
{
public:
// RAII思想
SmartPtr(T* ptr)
:_ptr(ptr)
{}
~SmartPtr()
{
//delete[] _ptr;
delete _ptr;
_ptr = nullptr;
}
// 像指针一样
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T* Get()
{
return _ptr;
}
private:
T* _ptr;
};
struct Date
{
int _year;
int _month;
int _day;
Date(int year = 1, int month = 1, int day = 1)
{
}
};
int main()
{
SmartPtr<int> sp1(new int);
*sp1 = 10;
cout << *sp1 << endl;
SmartPtr<Date> sparray(new Date);
// 需要注意的是这里应该是sparray.operator->()->_year = 2018;
// 本来应该是sparray->->_year这里语法上为了可读性,省略了一个->
sparray->_year = 2018;
sparray->_month = 1;
sparray->_day = 1;
}
总结一下智能指针的原理:
- RAII特性
- 重载operator*和opertaor->,具有像指针一样的行为。
四、C++标准库中的智能指针
1. std::auto_ptr
std::auto_ptr文档
C++98版本的库中就提供了auto_ptr的智能指针。下面演示的auto_ptr的使用及问题。
int main()
{
std::auto_ptr<int> sp1(new int);
std::auto_ptr<int> sp2(sp1); // 管理权转移
// sp1悬空
*sp2 = 10;
cout << *sp2 << endl;
cout << *sp1 << endl;
return 0;
}
当sp2利用sp1进行拷贝构造时,sp1便被置空了,auto_ptr是一个失败设计,很多公司明确要求不能使用auto_ptr。
auto_ptr的实现原理:管理权转移的思想,下面简化模拟实现了一份bit::auto_ptr来了解它的原理。
namespace lhf
{
template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr)
:_ptr(ptr)
{}
auto_ptr(auto_ptr<T>& p)
:_ptr(p._ptr)
{
p._ptr = nullptr; //管理权转移
}
auto_ptr<T>& operator=(auto_ptr<T>& p)
{
if (this != &p)
{
if (_ptr)
{
delete _ptr;
}
//资源转移到当前对象
_ptr = p._ptr;
p._ptr = nullptr;
}
return *this;
}
~auto_ptr()
{
if (_ptr)
{
//cout << "delete" << endl;
delete _ptr;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
}
2. std::unique_ptr
unique_ptr是C++11提供的。C++11库才更新了智能指针的实现,在C++11出来之前,第三方库boost已经搞好了更好用的scoped_ptr、shared_ptr、weak_ptr等智能指针,C++11将boost库中的智能指针的精华部分吸收了过来,实现了官方库的unique_ptr、shared_ptr、weak_ptr等智能指针。
unique_ptr文档
unique_ptr的实现原理:简单粗暴的防拷贝。下面简化模拟实现了一份unique_ptr来了解它的原理。
namespace lhf
{
template<class T>
class unique_ptr
{
public:
unique_ptr(T* ptr)
:_ptr(ptr)
{}
~unique_ptr()
{
if (_ptr)
{
delete _ptr;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
unique_ptr(const unique_ptr<T>& p) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& p) = delete;
private:
T* _ptr;
};
}
int main()
{
lhf::unique_ptr<int> sp1(new int);
lhf::unique_ptr<int> sp2(sp1);
std::unique_ptr<int> sp1(new int);
std::unique_ptr<int> sp2(sp1);
return 0;
}
3. std::shared_ptr
std::shared_ptr文档
shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。引用计数支持多个拷贝管理同一个资源,最后一个析构对象释放资源。
- shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。
- 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
- 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
- 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。
模拟实现shared_ptr的代码
namespace lhf
{
template<class T>
class shared_ptr
{
private:
void release()
{
if (--(*_pCount) == 0 && _ptr)
{
cout << "delete" << _ptr << endl;
delete _ptr;
_ptr = nullptr;
delete _pCount;
_pCount = nullptr;
}
}
public:
shared_ptr(T* ptr)
:_ptr(ptr)
,_pCount(new int(1))
{}
shared_ptr(const shared_ptr<T>& p)
:_ptr(p._ptr)
, _pCount(p._pCount)
{
++(* _pCount);
}
shared_ptr<T>& operator=(const shared_ptr<T>& p)
{
if (_ptr != p._ptr)
{
this->release();
_ptr = p._ptr;
_pCount = p._pCount;
++(*_pCount);
}
return *this;
}
~shared_ptr()
{
this->release();
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
int* _pCount;
};
}
int main()
{
lhf::shared_ptr<int> sp1(new int);
lhf::shared_ptr<int> sp2(sp1);
lhf::shared_ptr<int> sp3(sp1);
lhf::shared_ptr<int> sp4(new int);
lhf::shared_ptr<int> sp5(sp4);
//sp1 = sp1;
//sp1 = sp2;
sp1 = sp4;
sp2 = sp4;
sp3 = sp4;
*sp1 = 2;
*sp2 = 3;
return 0;
}
4. std::shared_ptr的循环引用
案例如下:
struct ListNode
{
int _data;
shared_ptr<ListNode> _prev;
shared_ptr<ListNode> _next;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
shared_ptr<ListNode> node1(new ListNode);
shared_ptr<ListNode> node2(new ListNode);
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
node1->_next = node2;
node2->_prev = node1;
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
return 0;
}
循环引用分析:
- node1和node2两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动delete。
- node1的_next指向node2,node2的_prev指向node1,引用计数变成2。
- node1和node2析构,引用计数减到1,但是_next还指向下一个节点。但是_prev还指向上一个节点。
- 也就是说_next析构了,node2就释放了。
- 也就是说_prev析构了,node1就释放了。
- 但是_next属于node的成员,node1释放了,_next才会析构,而node1由_prev管理,_prev属于node2成员,所以这就叫循环引用,谁也不会释放。
解决方法:在引用计数的场景下,把节点中的_prev和_next改成weak_ptr。原理就是,node1->_next = node2;和node2->_prev = node1,时weak_ptr的_next和 _prev不会增加node1和node2的引用计数。
5. std::weak_ptr
std::weak_ptr 要与 std::shared_ptr 一起使用。 一个 std::weak_ptr 对象看做是 std::shared_ptr 对象管理的资源的观察者,它不影响共享资源的生命周期。
- 如果需要使用 weak_ptr 正在观察的资源,可以将 weak_ptr 提升为 shared_ptr。
- 当 shared_ptr 管理的资源被释放时,weak_ptr 会自动变成 nullptr。
weak_ptr的模拟实现
namespace lhf
{
// 不参与指向资源的释放管理
template<class T>
class weak_ptr
{
public:
weak_ptr()
:_ptr(nullptr)
{}
weak_ptr(const shared_ptr<T>& sp)
:_ptr(sp.get())
{}
weak_ptr<T>& operator=(const lhf::shared_ptr<T>& p)
{
_ptr = p._ptr;
return *this;
}
// 像指针一样
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
public:
T* _ptr;
};
}
struct ListNode
{
/*ListNode* _next = nullptr;
ListNode* _prev = nullptr;*/
std::weak_ptr<ListNode> _next;
std::weak_ptr<ListNode> _prev;
int _val = 0;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
std::shared_ptr<ListNode> node1(new ListNode);
std::shared_ptr<ListNode> node2(new ListNode);
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
node1->_next = node2;
node2->_prev = node1;
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
return 0;
}
删除器:
如果不是new出来的对象如何通过智能指针管理呢?其实shared_ptr设计了一个删除器来解决这个问题。
// 仿函数的删除器
template<class T>
struct FreeFunc
{
void operator()(T* ptr)
{
cout << "free:" << ptr << endl;
free(ptr);
}
};
template<class T>
struct DeleteArrayFunc
{
void operator()(T* ptr)
{
cout << "delete[]" << ptr << endl;
delete[] ptr;
}
};
int main()
{
FreeFunc<int> freeFunc;
std::shared_ptr<int> sp1((int*)malloc(4), freeFunc);
DeleteArrayFunc<int> deleteArrayFunc;
std::shared_ptr<int> sp2((int*)malloc(4), deleteArrayFunc);
std::shared_ptr<A> sp4(new A[10], [](A* p){delete[] p; });
std::shared_ptr<FILE> sp5(fopen("test.txt", "w"), [](FILE* p){fclose(p); });
return 0;
}
由此可见,利用删除器就可以使不是new出来的对象或者是不同的类型的对象都可以由智能指针进行管理。