智能指针的使用及其原理
RAll
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。具体来说,RAll的核心是:对象构造时获取资源,对象析构的时候释放资源,又由于对象析构会在退出函数栈帧时自动释放,所以我们不需要显式的释放资源。
智能指针就是使用这种机制来实现对资源的控制。
智能指针的原理
来看下面一段代码:
template<class T>
class SmartPtr {
public:
SmartPtr(T* ptr = nullptr)
: _ptr(ptr)
{}
~SmartPtr()
{
if(_ptr)
delete _ptr;
}
T& operator*() {return *_ptr;}
T* operator->() {return _ptr;}
private:
T* _ptr;
};
上述代码就是智能指针的基本原理。构造时传递给SmartPtr一个指向资源的指针,之后我们就不用再显式的释放资源了,因为SmartPtr会自动帮我们释放。通过重载*
和->
,我们可以把SmartPtr对象当指针从而来使用资源,就像指向资源的指针一样。比如下面代码
template<class T>
class SmartPtr {
public:
SmartPtr(T* ptr = nullptr)
: _ptr(ptr)
{}
~SmartPtr()
{
if (_ptr)
delete _ptr;
}
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
private:
T* _ptr;
};
struct Date
{
int _year;
int _month;
int _day;
};
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;
return 0;
}
综上总结智能指针的主要基本原理:
- 具有RAll特性,即是用对象控制资源的生命周期
- 重载operator*和opertaor->,具有像指针一样的行为,类似迭代器
四种智能智能
C++标准库中给我们提供了一些智能指针类模板且包含在memory头文件中,下面介绍一些常见的智能指针
auto_ptr
C++98其实就已经提供了auto_ptr
的智能指针,我们可以通过手册来查看:std::auto_ptr文档
auto_ptr的实现原理:管理权转移的思想,下面简化模拟实现了一份bit::auto_ptr来了解它的原理:
namespace bit
{
template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr)
:_ptr(ptr)
{}
auto_ptr(auto_ptr<T>& sp)
:_ptr(sp._ptr)
{
// 管理权转移
sp._ptr = nullptr;
}
auto_ptr<T>& operator=(auto_ptr<T>& ap)
{
// 检测是否为自己给自己赋值
if (this != &ap)
{
// 释放当前对象中资源
if (_ptr)
delete _ptr;
// 转移ap中资源到当前对象中
_ptr = ap._ptr;
ap._ptr = NULL;
}
return *this;
}
~auto_ptr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
// 像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
}
我们可以看到,auto_ptr
支持拷贝构造和赋值构造,从管理角度来说,拷贝一个智能指针其实就是将资源管理权交给另一个auto_ptr
对象。表面上看似乎没什么问题,可是对于原来的auto_ptr
对象来说,它的内置资源指针就悬空了。比如:
上述代码中,p1将资源交给p2后自己就悬空了,此时再想通过p1去访问资源就会出现报错。
auto_ptr
这种设计模式会导致悬空指针,所以比较少见。
unique_ptr
C++11中提供了更完善的智能指针unique_ptr
。
unique_ptr的实现原理:简单粗暴的防拷贝,下面简化模拟实现了一份unique_ptr来了解它的原理:
template<class T>
class unique_ptr
{
public:
unique_ptr(T* ptr)
:_ptr(ptr)
{}
~unique_ptr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
// 像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
unique_ptr(const unique_ptr<T>& sp) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;
private:
T* _ptr;
};
出了不能拷贝构造和拷贝赋值,unique_ptr
的其余实现和auto_ptr
还是一样的。
shared_ptr
前两种智能指针实际上没有很好的解决智能指针之间的拷贝问题,于是C++11又设计了另一种可以支持拷贝且不会造成指针悬空的智能指针,即shared_ptr
。简单来说,shared_ptr通过引用计数的方式支持对象之间共享资源。具体地:
- shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。
- 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
- 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源。
- 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。
下面看看具体代码:
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr=nullptr)
:_ptr(ptr)
,_count(new int(1))
{
}
shared_ptr(const shared_ptr<T>& sp)
{
_ptr = sp._ptr;
_count = sp._count;
*_count++;
}
~shared_ptr()
{
if (*_count == 1)
{
delete _ptr;
delete _count;
}
else {
*_count--;
}
}
shared_ptr<T>& operator=(shared_ptr<T>& sp)
{
if (_ptr != sp._ptr)
{
if (*_count == 1)
{
delete _ptr;
delete _count;
}
else {
*_count--;
}
_ptr = sp._ptr;
_count = sp._count;
*_count++;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
int* _count;
};
我这里是简单实现了一下,其实还可以使用锁保证多线程下线程同步访问计数器。标准库里面的shared_ptr也是使用了锁来保证线程安全的。
shared_ptr的缺陷
尽管 std::shared_ptr 提供了许多优势,它也有一些缺陷和局限性。主要缺陷是循环引用造成内存泄漏:
如果两个或多个 std::shared_ptr 之间形成循环引用(即,A 引用 B,B 又引用 A),则引用计数永远不会变为 0,从而导致内存泄漏。
观察下面代码:
#include <iostream>
#include <memory>
class A;
class B;
class A {
public:
std::shared_ptr<B> b;
~A() { std::cout << "A destroyed" << std::endl; }
};
class B {
public:
std::shared_ptr<A> a;
~B() { std::cout << "B destroyed" << std::endl; }
};
void example() {
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();
a->b = b;
b->a = a; // 形成循环引用
// A 和 B 对象不会被销毁,因为它们互相引用
}
int main()
{
example();
return 0;
}
上面代码运行结果是什么都没有打印,也就意味着a和b都没有析构。为什么呢?
分析:
- 对象创建,创建 std::shared_ptr 对象 a 和 std::shared_ptr 对象 b。a 持有 B 的共享指针,b 持有 A 的共享指针。
- 设置引用:a 的 b_ptr 指向 b。b 的 a_ptr 指向 a。此时a的引用计数为2,b的引用计数也是2。
- 析构a对象,引用计数减一,但是不为0,于是不释放资源。析构b对象也是如此。所以出现了内存泄漏。
wead_ptr
为了专门解决shared_ptr的循环引用问题,C++11又提供了一种weak_ptr的智能指针。用于观察由shared_ptr 管理的对象,而不影响对象的引用计数。
weak_ptr的构造可以传入一个shared_ptr对象
再来看一段代码:
#include <iostream>
#include <memory>
class A;
class B {
public:
std::weak_ptr<A> a_ptr; // 使用 weak_ptr 观察 A
~B() { std::cout << "B destroyed" << std::endl; }
};
class A {
public:
std::shared_ptr<B> b_ptr;
~A() { std::cout << "A destroyed" << std::endl; }
};
void example() {
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();
a->b_ptr = b; // A 持有对 B 的共享指针
b->a_ptr = a; // B 观察 A,使用 weak_ptr
// 当 a 和 b 超出作用域时,它们的引用计数会归零
}
int main() {
example(); // A 和 B 对象会被正确销毁
return 0;
}
上面代码中的a、b对象出作用域后可以正确销毁。这是因为b->a_ptr = a
表示 B 持有 A 的一个 std::weak_ptr
,这个 std::weak_ptr
不增加 A 的引用计数。所以a的引用计数还是1,也就能正确销毁。