1. RAII
智能指针是用以资源管理的一种工具。所谓资源就是,一旦用了它,将来就必须还给系统。如果不这样的话,会发生糟糕的事,如内存泄漏。C++程序中最常见的资源就是动态分配的内存(new/malloc申请堆上的内存,delete/free释放,如果不释放会导致内存泄漏)。谨慎地编写程序能让我们避免大部分不将资源还给系统的情况,但还是会有一些情况是难以避免的,举两个例子:
情况1:func1()
中,因为抛异常而未能执行语句delete[] pArray
,资源未释放
void func1()
{
int* pArray = new int[10];
// to do something...
throw("error");
delete[] pArray;
}
int main()
{
try
{
func1();
}
catch (const char* errmsg)
{
}
return 0;
}
情况2:函数内多重回传路径
void func1(int x)
{
int* pArray = new int[10];
if (x > 0)
{
return;
}
else
{
std::cout << x << std::endl;
}
delete[] pArray;
}
int main()
{
int x = 0;
func1(x);
return 0;
}
事实上,内存只是你管理的众多资源之一。其它资源如:打开的文件描述符(File Describtors)、互斥量(Mutex)、网络sockets等在使用后都需要归还给系统。而仅仅让程序员谨慎代码是很难完全规避内存泄漏的问题的。
-
RAII思想
RAII(Resource Acquisiton Is Initialization),是一种利用对象的生命周期管理资源的方法,即“资源取得时机便是初始化时机”。每一笔资源在被获取到时,立刻被放入管理对象中。而管理对象运用析构函数确保资源被释放。也就是说,无论执行流以任何形式离开当前区块,一旦管理对象被销毁,生命周期结束,既调用管理对象的析构函数,而析构函数中包含了对象管理的资源的释放方法,这样就能保证资源被释放了。
-
RAII的优势
- 不需要显式地释放资源
- 对象管理的资源在其生命周期内有效
而智能指针正是运用了RAII思想,对资源进行管理的一种对象。
2. 智能指针
智能指针,顾名思义,除了能够以RAII思想管理资源外,还要能够像原生指针一样使用,既通过
*
和->
访问管理资源,在C++类中可通过运算符重载实现。而智能指针真正的难点在于拷贝问题,不同的智能指针实现对拷贝问题有不同的解决方法。
下面介绍C++标准库中三种智能指针:
头文件:#include <memory>
auto_ptr
C++98版本的库中就提供了auto_ptr的智能指针,下面演示auto_ptr的模拟实现。
拷贝问题解决方法:资源管理权的交换
auto_ptr模拟实现
namespace ckf
{
template <class T>
class auto_ptr
{
public:
auto_ptr(T* ptr)
:_ptr(ptr)
{
}
// 拷贝构造,直接取ap的指针,并将ap指针设为空
auto_ptr(auto_ptr<T>& ap)
:_ptr(ap._ptr)
{
ap._ptr = nullptr;
}
// 赋值重载
auto_ptr<T>& operator=(auto_ptr<T>& ap)
{
//交换资源管理权,即交换指针
std::swap(ap._ptr, _ptr);
return *this;
}
~auto_ptr()
{
// 析构时释放资源
if (_ptr)
delete _ptr;
}
// 像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr; // 指针管理资源
};
}
auto_ptr的缺陷:
- 多个auto_ptr无法指向同一资源的问题。(若多个auto_ptr指向同一资源,资源会被删除多次,这是错误的)
- 频繁交换资源管理权,会导致auto_ptr的指向不明确。
综上所述,auto_ptr是一个失败的设计。
unique_ptr
C++11库中给出的更靠谱的unique_ptr
拷贝问题解决方法:简单粗暴的禁止拷贝
unique_ptr模拟实现
namespace ckf
{
template <class T>
class unique_ptr
{
public:
unique_ptr(T* ptr = nullptr)
:_ptr(ptr)
{
}
// 删除拷贝构造和赋值重载函数
unique_ptr(const unique_ptr& up) = delete;
unique_ptr<T>& operator=(const unique_ptr& up) = delete;
~unique_ptr()
{
if (_ptr)
delete _ptr;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
}
unique_ptr的缺陷:
比起auto_ptr,指针的指向非常明确,但依然无法解决多个指针资源共享的问题。
shared_ptr
C++11中的shared_ptr完美解决了以上两个智能指针的问题。
拷贝问题解决方法:引用计数
- shared_ptr模拟实现
namespace ckf
{
// 1. RAII
// 2. like pointer
// 3. copy
template <class T>
class shared_ptr
{
typedef shared_ptr<T> Self;
public:
T& operator*()
{
assert(_pdata);
return *_pdata;
}
T* operator->()
{
assert(_pdata);
return _pdata;
}
// constructor
shared_ptr(T* pdata = nullptr)
:_pdata(pdata)
{
if (_pdata)
{
_pcount = new int(1);
_pmtx = new std::mutex;
}
else
{
// 若指向资源为空,则计数器和互斥量也为空
_pcount = nullptr;
_pmtx = nullptr;
}
}
// destructor
~shared_ptr()
{
// release先对计数器减1,再检查计数器是否为0,为0则释放资源
release();
}
// copy
shared_ptr(const Self& sp)
:_pdata(sp._pdata)
, _pcount(sp._pcount)
, _pmtx(sp._pmtx)
{
if(_pdata)
add_count();
}
// operator=
Self& operator=(const Self& sp)
{
// 相同就不用赋值了
if (_pdata != sp._pdata)
{
release();
_pdata = sp._pdata;
_pcount = sp._pcount;
_pmtx = sp._pmtx;
if (_pdata)
add_count();
}
return *this;
}
// 获取计数器内容(指向为空返回0)
int use_count() const
{
if (_pdata)
return *_pcount;
else
return 0;
}
T* get() const
{
return _pdata;
}
private:
void release()
{
if (_pdata) // 若为空,什么也不做
{
// 访问计数器,需要加锁保护
_pmtx->lock();
bool deleteFlag = false;
// 计数器减到0,即释放资源
if (--(*_pcount) == 0)
{
delete _pdata;
delete _pcount;
deleteFlag = true;
}
_pmtx->unlock();
if (deleteFlag) // 资源释放,锁才释放
delete _pmtx;
}
}
void add_count()
{
// 访问计数器,需要加锁保护
_pmtx->lock();
++(*_pcount);
_pmtx->unlock();
}
T* _pdata;
int* _pcount;
std::mutex* _pmtx;
};
// 多线程可能会使用同一个shared_ptr(包括它的拷贝)
// 因此
// 对于计数器的访问,应该由shared_ptr保证线程安全
// 对于sp指向数据,应该由用户自行保证数据安全
}
- shared_ptr赋值的过程:
- shared_ptr线程安全的测试demo
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>
using namespace std;
class Date
{
public:
Date(int year = 2023, int month = 9, int day = 7)
:_year(year)
, _month(month)
, _day(day)
{
}
~Date()
{
std::cout << "~Date()" << std::endl;
}
void getDate()
{
printf("%d年%d月%d日\n", _year, _month, _day);
}
int _year;
int _month;
int _day;
};
void SmartPtrTest(ckf::shared_ptr<Date>& sp, const int& n, std::mutex& mtx)
{
// 测试ckf::shared_ptr的线程安全
for (int i = 0; i < n; i++)
{
// 这里两个线程不断拷贝sp和释放sp,既不断对sp计数器的++和--,最终保证sp计数器为1
ckf::shared_ptr<Date> copy(sp);
// 管理资源的安全由程序员保证
// 若对资源的访问是线程安全的
// 那么两个线程执行后三个值应该都是20000
mtx.lock();
copy->_year++;
copy->_month++;
copy->_day++;
mtx.unlock();
}
//这里出现2的原因:
//可能是另外一个线程正处于for循环中创建copy和销毁copy的间隙
//此时计数器为2
//线程调度到当前线程,use_count()打印的数就是2
//但是不影响最终计数器依旧为1
//std::cout << sp.get() << " " << sp.use_count() << std::endl;
}
void test_thread_safe()
{
ckf::shared_ptr<Date> sp(new Date(0, 0, 0));
const int n = 100000;
std::mutex mtx;
// 观察多线程执行前后,sp计数器是否依然为1
cout << "before: " << sp.use_count() << endl;
sp->getDate();
cout << endl;
std::thread t1(SmartPtrTest, std::ref(sp), n, std::ref(mtx));
std::thread t2(SmartPtrTest, std::ref(sp), n, std::ref(mtx));
std::this_thread::sleep_for(std::chrono::seconds(1));
cout << "after: " << sp.use_count() << endl;
sp->getDate();
cout << endl;
t1.join();
t2.join();
}
int main()
{
test_thread_safe();
return 0;
}
运行结果
- 删除器deleter
有时候,单单的
delete ptr
无法满足释放资源的需求。就如:管理的资源是以new T[]
形式开辟的,则需delete[]
释放;又如:管理的资源是C语言打开文件的FILE结构体,那么需要fclose()
函数释放。标准库中的shared_ptr配套了删除器,用于指定某种释放资源的方式。
使用方法如下:
void test_deleter()
{
// 事实上deleter就是一个可调用对象,这里用了lambda表达式
std::shared_ptr<Date> sp1(new Date[10], [](Date* ptr) {
delete[] ptr; });
std::shared_ptr<FILE> sp2(fopen("log.txt", "w"), [](FILE* fp) {
fclose(fp); });
}
shared_ptr with deleter
namespace ckf
{
// 1. RAII
// 2. like pointer
// 3. copy
template <class T>
class shared_ptr
{
typedef shared_ptr<T> Self;
public:
T& operator*()
{
assert(_pdata);
return *_pdata;
}
T* operator->()
{
assert(_pdata);
return _pdata;
}
// constructor
shared_ptr(T* pdata = nullptr)
:_pdata(pdata)
{
if (_pdata)
{
_pcount = new int(1);
_pmtx = new std::mutex;
}
else
{
_pcount = nullptr;
_pmtx = nullptr;
}
}
// 增加一个构造函数,可自动推导deleter类型
template <class D>
shared_ptr(T* pdata, D del)
:_pdata(pdata)
,_del(del)
{
if (_pdata)
{
_pcount = new int(1);
_pmtx = new std::mutex;
}
else
{
_pcount = nullptr;
_pmtx = nullptr;
}
}
// destructor
~shared_ptr()
{
release();
}
// deleter也要拷贝
// copy
shared_ptr(const Self& sp)
:_pdata(sp._pdata)
, _pcount(sp._pcount)
, _pmtx(sp._pmtx)
, _del(sp._del)
{
if(_pdata)
add_count();
}
// operator=
Self& operator=(const Self& sp)
{
// 相同就不用赋值了
if (_pdata != sp._pdata)
{
release();
_pdata = sp._pdata;
_pcount = sp._pcount;
_pmtx = sp._pmtx;
_del = sp._del;
if (_pdata)
add_count();
}
return *this;
}
int use_count() const
{
if (_pdata)
return *_pcount;
else
return 0;
}
T* get() const
{
return _pdata;
}
private:
void release()
{
if (_pdata) // 若为空,什么也不做
{
_pmtx->lock();
bool deleteFlag = false;
// sp为空或者减计数到0,即释放空间
if (--(*_pcount) == 0)
{
//delete _pdata;
_del(_pdata);
delete _pcount;
deleteFlag = true;
}
_pmtx->unlock();
if (deleteFlag)
delete _pmtx;
}
}
void add_count()
{
_pmtx->lock();
++(*_pcount);
_pmtx->unlock();
}
T* _pdata;
int* _pcount;
std::mutex* _pmtx;
std::function<void(T*)> _del = [](T* ptr) {
delete ptr; };
};
}
shared_ptr的缺陷:
shared_ptr是较为靠谱的一种智能指针,但是会有一个小问题——循环引用。看下面代码:
struct Node
{
ckf::shared_ptr<Node> _next;
ckf::shared_ptr<Node> _prev;
Node(int val) :_val(val)
{
}
~Node()
{
std::cout << _val << "~Node()" << std::endl;
}
int _val = 0;
};
void test_cycle_ref()
{
ckf::shared_ptr<Node> n1(new Node(1));
ckf::shared_ptr<Node> n2(new Node(2));
n1->_next = n2;
n2->_prev = n1;
}
int main()
{
test_cycle_ref();
return 0;
}
演示图:
流程分析:
- n1和n2两个shared_ptr分别指向节点1和节点2,以RAII方式管理资源。此时两个节点的计数器都为1。
- 两个节点中的_next和_prev也是shared_ptr,节点1的_next指向节点2,节点2的_prev指向节点1。此时两个节点的计数器都增加为2。
- 函数执行完毕,先销毁n2——节点2的计数器减为1(节点1中的_next还指向节点2),再销毁n1——节点1的计数器减为1(节点2的_prev还指向节点1)。
- 此时陷入了僵局。只有_next析构了,节点2才能释放,只有_prev析构了,节点1才能释放。根据类的特性,节点1释放,_next才会析构,节点2释放,_prev才会析构。二者的释放条件依赖于彼此,谁也不释放,这就是循环引用。(
节点1释放要2.prev析构->2.prev析构要节点2释放->节点2释放要1.next析构->1.next析构要节点1释放,形成闭环
)
解决方法:
C++11中引进了weak_ptr智能指针。该类型指针通常不单独使用,只能和 shared_ptr搭配使用来解决循环引用问题。
- weak_ptr并不参与资源的管理,只是作为shared_ptr的辅助,与shared_ptr指向相同的资源,不会修改所管理资源配套的计数器。
- weak_ptr没有重载
*
和->
运算符,这意味着weak_ptr只能指向内存空间,主要用于保存或赋予指针给shared_ptr,无法访问修改内存空间。 - weak_ptr也有引用计数,但只用于查看与该weak_ptr指向相同的shared_ptr的数量。引用计数为0时,weak_ptr处于过期状态。
struct Node
{
int _val = 0;
std::weak_ptr<Node> _next;
std::weak_ptr<Node> _prev;
Node(int val) :_val(val)
{
}
~Node()
{
std::cout << _val << "~Node()" << std::endl;
}
};
void test_cycle_ref()
{
std::shared_ptr<Node> n1(new Node(1));
std::shared_ptr<Node> n2(new Node(2));
// weak_ptr可以接收shared_ptr的赋值,也能以shared_ptr进行初始化构造
n1->_next = n2;
n2->_prev = n1;
// weak_ptr可以作为shared_ptr初始化构造的值,但是无法赋值给shared_ptr
std::shared_ptr<Node> n3(n1->_next); // 此时n3与n2指向相同
std::cout << n3.use_count() << std::endl;
std::cout << (n1->_next).use_count() << std::endl;
}
int main()
{
test_cycle_ref();
return 0;
}
运行结果:
3. 智能指针历史
- C++ 98 中产生了第一个智能指针auto_ptr;
- C++ boost给出了更实用的scoped_ptr和shared_ptr和weak_ptr;
- C++ TR1,引入了shared_ptr等。不过注意的是TR1并不是标准版;
- C++ 11,引入了unique_ptr和shared_ptr和weak_ptr。需要注意的是unique_ptr对应boost的scoped_ptr。并且这些智能指针的实现原理是参考boost中的实现的。
Ending…
文章评论