C++11前的智能指针
int div()
{
double a, b;
cin >> a >> b;
if(b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void f1()
{
int* p = new int;
cout << div() << endl;
delete p;
cout << p << endl;
}
int main()
{
try
{
f1();
}
catch(exception& e)
{
cout << e.what() << endl;
}
return 0;
}
如果发生了除0错误,在f1中没有到delete p就抛异常了,这时候只能在f1中也抛异常,这样才能让p释放。
void f1()
{
int* p = new int;
try
{
cout << div() << endl;
}
catch(...)
{
delete p;
cout << p << endl;
throw;
}
delete p;
cout << p << endl;
}
但这样处理不太好看,C++中可以用智能指针来处理。
RAII: 是一种利用对象生命周期来控制程序资源。构造函数获取资源,析构函数释放资源。
一个基本的智能指针包含两点:1、RAII。2、重载operator* 和operator-> 用起来像指针一样。
实现一个基本的智能指针:
template<class T>
class SmartPtr
{
public:
// 1、RAII
// 2、重载operator* 和 operator-> 用起来像指针一样
SmartPtr(T* ptr)
:_ptr(ptr)
{}
~SmartPtr()
{
delete _ptr;
cout << _ptr << endl;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
注意:这里的
(*spp).first
就是spp->first
,而它原本是spp->->first
,中间还有一个pair,但这样可读性太差,编译器优化成只有一个箭头就可以。
这里拷贝了一个智能指针,或传参时拷贝了。多个智能指针对象管理一个资源,析构时会析构多次,而这个智能指针没有写拷贝构造函数,默认构造函数会完成内置类型(T* 指针)的浅拷贝,程序崩溃。
C++98 : 拷贝时,管理权转移(auto_ptr)
永远只有一个对象管理资源。
赋值运算符重载也是如此,将管理权转移,把自己置空。
但是auto_ptr的管理权转移会导致被拷贝的对象悬空,如果不小心访问了sp1就访问了空指针,是一种不好的设计。
boost库(第三方库):scoped_ptr,shared_ptr,weak_ptr
C++11吸收了boost库的精华:unique_ptr,shared_ptr,weak_ptr
unique_ptr
设计思路:有些场景下面,智能指针仅仅用于管理资源,不需要拷贝。
防拷贝、赋值:
unique_ptr(const unique_ptr<T>& up) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;
如果使用它拷贝就直接报错。
shared_ptr
如果count是在栈上,那么每个对象都有一个count,少一个智能指针对象就要把每个对象的count都减1才能一致,增加对象时也一样,不是只有一个count。我们要的是每个对象都使用同一个计数。
如果使用静态变量,当有多个资源对象时,每个资源对象应该有独立的计数,但静态count是所有智能指针对象共享了,也不行。
所以我们给一个在堆上的计数变量
int* count;
如果使用了拷贝构造那么智能指针对象使用的也是同一个count资源
析构:
设计思路:多个智能指针对象管理一块资源,这块资源对应一个引用计数,析构时减减计数,计数等于0时表示是最后一个管理对象,就释放资源。
赋值重载:
要注意判断是不是自己给自己赋值,如果是以下的情况,没有判断是不是自己给自己赋值,计数减为0将释放sp6,count将会被置成随机值。
//sp1 = sp4
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
//if(*this != sp): 对象没有默认重载!=符号
//if(this != &sp):如果其中的指针_ptr指向同一块资源,但不是同一个对象,那--又++就白操作了,也不好
if(_ptr != sp._ptr)
{
if(--(*pCount) == 0)
{
delete _pCount;
delete _ptr;
}
_ptr = sp._ptr;
_pCount = sp._pCount;
++(*pCount);
}
}
现代写法:sp1 = sp4,先用sp4传值拷贝构造一个sp,计数++,sp的计数换给了sp1,sp1原来的计数换给了sp,sp是个局部对象,函数结束调用析构函数释放,减少了一个计数。
线程安全问题
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr)
:_ptr(ptr)
, _pCount(new int(1))
{}
// sp2(sp1)
shared_ptr(shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pCount(sp._pCount)
{
++(*_pCount);
}
// sp1 = sp4 现代写法
shared_ptr<T>& operator=(shared_ptr<T> sp)
{
swap(_ptr, sp._ptr);
swap(_pCount, sp._pCount);
return *this;
}
~shared_ptr()
{
if (--(*_pCount) == 0 && _ptr)
{
delete _ptr;
delete _pCount;
cout << _ptr << endl;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T* get()
{
return _ptr;
}
int use_count()
{
return *_pCount;
}
private:
T* _ptr;
int* _pCount;
};
引用计数是智能指针的内部实现,但是资源的线程安全不是智能指针能管的。智能指针对象拷贝析构过程中引用计数的线程安全需要保障。以上代码就会引发线程安全问题。
struct Date
{
int _year = 1;
int _month = 1;
int _day = 1;
};
void SharePtrFunc(my::shared_ptr<Date>& sp, size_t n)
{
for(size_t i = 0; i < n; i++)
{
//这里智能指针拷贝会++计数,析构会--计数,这里是线程安全的。
my::shared_ptr<Date> copy(sp);
//这里智能指针访问管理的资源,不是线程安全的。所以我们看看这些值两个线程++了2n次,但是最终看到的结果,不一定是加了2n
copy->_year++;
copy->_month++;
copy->_day++;
}
}
void test_shared_ptr_thread_salf()
{
my::shared_ptr<Date> p(new Date);
cout << p.get() << endl;
cout << p.use_count() << endl;
const size_t n = 10000;
thread t1(SharePtrFunc, p, n);
thread t2(SharePtrFunc, p, n);
t1.join();
t2.join();
cout << p.get() << endl;
cout << p.use_count() << endl;
cout << p->_year << endl;
cout << p->_month << endl;
cout << p->_day << endl;
}
我们可以专门抽象出来控制pCount
void add_ref()
{
_pMtx->lock();
++(*_pCount);
_pMtx->unlock();
}
void release_ref()
{
bool flag = false;
_pMtx->lock();
if (--(*_pCount) == 0 && _ptr)
{
D del;
del(_ptr); // 使用删除器释放即可
//delete _ptr;
delete _pCount;
flag = true;
cout << "释放资源:" << _ptr << endl;
}
_pMtx->unlock();
if (flag == true)
{
delete _pMtx;
}
}
完整代码
template<class T, class D = DefaultDel<T>>
class shared_ptr
{
template<class T>
friend class weak_ptr;
public:
explicit shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
, _pCount(new int(1))
, _pMtx(new mutex)
{}
void add_ref()
{
_pMtx->lock();
++(*_pCount);
_pMtx->unlock();
}
void release_ref()
{
bool flag = false;
_pMtx->lock();
if (--(*_pCount) == 0 && _ptr)
{
D del;
del(_ptr); // 使用删除器释放即可
//delete _ptr;
delete _pCount;
flag = true;
cout << "释放资源:" << _ptr << endl;
}
_pMtx->unlock();
if (flag == true)
{
delete _pMtx;
}
}
// sp2(sp1)
shared_ptr(shared_ptr<T, D>& sp)
:_ptr(sp._ptr)
, _pCount(sp._pCount)
, _pMtx(sp._pMtx)
{
add_ref();
}
// sp1 = sp4
shared_ptr<T, D>& operator=(shared_ptr<T, D> sp)
{
swap(_ptr, sp._ptr);
swap(_pCount, sp._pCount);
return *this;
}
~shared_ptr()
{
release_ref();
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T* get()
{
return _ptr;
}
int use_count()
{
return *_pCount;
}
private:
T* _ptr;
int* _pCount;
mutex* _pMtx;
};
循环引用
n2要销毁,它的引用计数要到0,而它被n1的_next管理,n1的_next要销毁就要让n1销毁,而n1被n2的_prev管理,n1销毁就要让n2的_prev销毁,而n2的_prev要销毁就要让n2销毁,这样两个节点都无法销毁,形成循环引用。
weak_ptr
它可以用一个shared_ptr去构造它。
在会产生循环引用的位置,把shared_ptr换成weak_ptr。
weak_ptr不是一个RAII智能指针,它不参与资源的管理,它是专门用来解决循环引用的问题的。可以把一个shared_ptr用来初始化一个weak_ptr,但是weak_ptr不增加引用计数,不参与管理,但是也像指针一样访问修改资源。
struct ListNode
{
int _data;
weak_ptr<ListNode> _prev;
weak_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(shared_ptr)赋值给node1->_next(weak_ptr),不会增加node2(shared_ptr)的引用计数
node2->_prev = node1;
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
return 0;
}
解决方案:在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了。
原理就是,node1->_next = node2;和node2->_prev = node1;时weak_ptr的_next和_prev不会增加node1和node2的引用计数。
简单模拟实现:
template<class T>
class weak_ptr
{
public:
weak_ptr()
:_ptr(nullptr)
{}
weak_ptr(shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pCount(sp._pCount)
{}
weak_ptr<T>& operator=(shared_ptr<T>& sp)
{
_ptr = sp._ptr;
_pCount = sp._pCount;
return *this;
}
private:
T* _ptr;
int* _pCount;
};
定制删除器
前面说的指针都是new一个出来的,但如果指针是其他方式生成的呢?
std::shared_ptr<pair<int, int>> sp1(new pair<int, int>[10]);
std::shared_ptr<string> sp2(new string[10]);
std::shared_ptr<string> sp3((string*)malloc(sizeof(string)));
此时就要用到删除器进行删除,如
template<class T>
struct DeleteArray
{
void operator()(T* ptr)
{
delete[] ptr;
}
};
void test_deletor()
{
std::shared_ptr<string> sp(new string[10], DeleteArray<string>());
}
定制删除器最重要的是在析构函数时调用
我们模拟实现删除器的传递位置跟std的不太一样
std的框架设计底层用一个类专门管理资源技术和释放,所以它可以在构造函数传参,把删除器类型传递给专门管理资源引用计数的这个类。
我们是一体化的,只能shared_ptr实例化给删除器,析构函数才能拿到删除器。
template<class T>
struct DefaultDel
{
void operator()(T* ptr)
{
delete ptr;
}
};
template<class T, class D = DefaultDel<T>>
class shared_ptr{...};
void test_deletor()
{
bit::shared_ptr<string> sp1(new string);
bit::shared_ptr<string, DeleteArray<string>> sp2(new string[10]);
auto ffree = [](string* ptr){free(ptr); };
bit::shared_ptr<string, decltype(ffree)> sp4((string*)malloc(sizeof(string)));
auto ffclose = [](FILE* ptr){fclose(ptr); };
bit::shared_ptr<FILE, decltype(ffclose)> sp5(fopen("test.cpp", "r"));
}
总结:
- 不要使用auto_ptr,他是早期的设计缺陷。
- 不拷贝的场景建议使用unique_ptr。
- 如果要拷贝或者定制删除器,建议使用shared_ptr。不过要注意循环引用的问题。
智能指针的一些常见问题:
- 什么是RAII
- 了解智能指针的发展历史
- 模拟实现一个简洁的智能指针 – unique_ptr
- 什么是循环引用?如何解决?解决的原理是什么
相关知识:
内存泄漏,资源泄漏:一个内存或资源,不使用了还没释放,就会导致资源泄漏/内存泄漏。
内存泄漏危害:1、进程僵尸了,资源无法释放。2、长期运行的服务器一直泄漏。
如何避免内存泄漏:
1、事前预防:养成良好的编码规范;RAII思想或智能指针来管理资源。
2、事后查错:内存泄漏工具。Linux:valgrind