智能指针
智能指针主要是为了解决内存泄漏的问题, 这与异常安全是密不可分的, 如果在malloc和free之间或new和delete之间存在抛出异常的话, 还是会存在内存泄漏的问题.
智能指针的原理
RAII(Resource Acquisition Is Initialization): 资源获取即初始化
这是一种利用对象声明周期来控制程序资源的技术.
在对象构造时获取资源, 在这个对象的整个声明周期内对资源的控制都是有效的, 当对象析构时同时释放资源. 这样就不需要显示的去释放资源, 并且对资源在合理的声明周期内进行了有效的控制.
智能指针的原理:
智能指针有两个特性:
1.符合RAII技术思想
2.像指针一样对资源进行控制
#include <iostream>
#include <stdexcept>
using namespace std;
template<class T>
class SmartPtr {
public:
SmartPtr(T* ptr) : _ptr(ptr) {}
~SmartPtr() {
if (_ptr) {
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
T& operator*() const {
return *_ptr;
}
T* operator->() const {
return _ptr;
}
private:
T* _ptr;
};
struct Num {
double a;
double b;
};
double division(const double& a, const double& b) {
if (b == 0) {
throw invalid_argument("divisor is zero");
} else {
return a / b;
}
}
void Fun() {
//int* num = new int;
//SmartPtr<int> sp(num);
SmartPtr<int> sp(new int);
*sp = 10;
cout << *sp << endl;
SmartPtr<Num> nsp(new Num);
nsp->a = 10;
nsp->b = 0;
cout << nsp->a << " " << nsp->b << endl;
cout << division(nsp->a, nsp->b) << endl;
}
int main() {
try {
Fun();
} catch(const exception& e) {
cout << e.what() << endl;
}
return 0;
}
在异常的总结当中, 捕获异常 + 重新抛出异常 虽然可以来以这种方式实现上述效果, 但是这样毕竟是不友好的, 而且纷繁复杂.
其实智能指针就是用来托管需要管理的资源的, 在上述程序中, 智能指针可以有效的解决异常安全带来的执行流混乱而导致的内存泄漏问题.
常见的智能指针
auto_ptr:
管理权转移
即新的托管者代替旧的托管者, 旧的托管者将管理权交给新的托管者后被销毁
缺陷: 当对象拷贝或赋值后, 旧的对象会悬空, 容易造成内存访问错误.
namespace sock {
template<class T>
class auto_ptr {
public:
auto_ptr(T* ptr) : _ptr(ptr) {}
~auto_ptr() {
if (_ptr) {
cout << "delete:" << _ptr << endl;
delete _ptr;
_ptr = nullptr;
}
}
// 管理权转移
auto_ptr(auto_ptr<T>& ptr) : _ptr(ptr._ptr) {
ptr._ptr = nullptr;
}
auto_ptr<T>& operator=(const auto_ptr<T>& ptr) {
if (_ptr != &ptr) { // 考虑给自己赋值的情况
if (_ptr) {
delete _ptr;
}
_ptr = ptr._ptr;
ptr._ptr = nullptr;
}
}
T& operator*() const {
return *_ptr;
}
T* operator->() const {
return _ptr;
}
private:
T* _ptr;
};
}
unique_ptr:
一块资源只能由一个对象管理
即不允许对象发生赋值和拷贝
这样简单粗暴避免了指针悬空的问题, 但是不支持多个对象管理同一块资源
namespace sock {
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*() const {
return *_ptr;
}
T* operator->() const {
return _ptr;
}
private:
T* _ptr;
unique_ptr(const unique_ptr<T>& ptr);
unique_ptr<T>& operator=(const unique_ptr<T>& ptr);
};
}
shared_ptr:
引用计数
通过引用计数的方式来实现多个智能指针共同管理一块资源, 该计数是记录总共有多少个对象在共享这块资源, 当计数为 0 时才将对象释放.
这样即支持拷贝和赋值, 同时多个智能指针能同时管理一块资源
namespace sock {
template<class T>
class shared_ptr {
public:
shared_ptr(T* ptr)
: _ptr(ptr), _pcount(new int(1)) {}
void Release() {
if (--(*_pcount) == 0) {
cout << "delete:" << _ptr << endl;
delete _ptr;
delete _pcount;
_ptr = nullptr;
_pcount = nullptr;
}
}
~shared_ptr() {
Release();
}
shared_ptr(const shared_ptr<T>& ptr)
: _ptr(ptr._ptr), _pcount(ptr._pcount) {
++(*_pcount);
}
shared_ptr<T>& operator=(const shared_ptr<T>& ptr) {
if(_ptr != ptr._ptr) { // 考虑给自己赋值的情况
Release();
_ptr = ptr._ptr;
_pcount = ptr._pcount;
++(*_pcount);
}
return *this;
}
T& operator*() const {
return *_ptr;
}
T* operator->() const {
return _ptr;
}
private:
T* _ptr;
int* _pcount; // 计数器, 用来统计当前管理该资源的对象个数
};
}
但是引用计数这种方式会存在线程安全的问题, 因此还需要进一步改进:
namespace sock {
template<class T>
class shared_ptr {
public:
shared_ptr(T* sp)
: _ptr(sp), _pcount(new int(1)), _pmtx(new mutex) {}
void Release() { // 释放函数, 因为会多次利用(析构和赋值), 所以封装成一个函数
_pmtx->lock();
bool lastone = false;
if (--(*_pcount) == 0) {
cout << "delete:" << _ptr << endl;
delete _ptr;
delete _pcount;
_ptr = nullptr;
_pcount = nullptr;
lastone = true;
}
_pmtx->unlock();
if (lastone == true) {
delete _pmtx;
}
}
~shared_ptr() {
Release();
}
shared_ptr(const shared_ptr<T>& sp)
: _ptr(sp._ptr), _pcount(sp._pcount), _pmtx(sp._pmtx) {
_pmtx->lock();
++(*_pcount);
_pmtx->unlock();
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp) {
_pmtx->lock();
if(_ptr != sp._ptr) { // 考虑给自己赋值的情况
Release();
_ptr = sp._ptr;
_pcount = sp._pcount;
_pmtx = sp._pmtx;
++(*_pcount);
}
_pmtx->unlock();
return *this;
}
T& operator*() const {
return *_ptr;
}
T* operator->() const {
return _ptr;
}
int getCounts() {
return *_pcount;
}
private:
T* _ptr;
int* _pcount; // 计数器, 用来统计当前管理该资源的对象个数
mutex* _pmtx; // 锁, 因为shared_ptr是非线程安全的(计数器导致的), 因此对于非原子性操作要加锁
};
}
struct Date{
int year;
int month;
int day;
~Date() {
cout << "~Date" << endl;
}
};
void sharedFun(sock::shared_ptr<Date>& sp, size_t size) {
size_t i;
for (i = 0; i < size; ++i) {
sock::shared_ptr<Date> copy(sp);
pmtx->lock(); // 对资源操作进行加锁
copy->year += 1;
copy->month += 1;
copy->day += 1;
pmtx->unlock();
}
}
void Fun() {
sock::shared_ptr<Date> sp(new Date);
sp->year = 1;
sp->month = 1;
sp->day = 1;
cout << "counts:" << sp.getCounts() << endl;
cout << sp->year << "-" << sp->month << "-" << sp->day << endl;
const size_t size = 100000;
std::thread t1(sharedFun, std::ref(sp), size);
std::thread t2(sharedFun, std::ref(sp), size);
t1.join();
t2.join();
cout << "counts:" << sp.getCounts() << endl;
cout << sp->year << "-" << sp->month << "-" << sp->day << endl;
}
智能指针本身是线程安全的, 但是智能指针管理的资源可以被多个线程进行访问, 因此对资源的操作是非线程安全的, 需要加锁.
shared_ptr的循环引用:
所谓循环引用, 一般出现在像循环链表这样的数据结构中, 两个智能指针相互托管彼此从属的结点资源但是却都得不到释放.
// 有关循环引用的问题
#include <iostream>
#include <memory>
using std::cout;
using std::endl;
struct ListNode {
std::shared_ptr<ListNode> next;
std::shared_ptr<ListNode> prev;
int value;
~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;
}
当node1和node2生命周期结束的时候会自动释放, 引用计数count都由2变为1, 但此时next的释放依赖于prev托管的资源的释放, 而prev的释放依赖于next托管的资源的释放; 再抽象点来讲, next的释放依赖于prev的释放, prev的释放依赖于next的释放, 这最终导致循环对峙的情况, 结果两者都不会释放.
这类似于双方拿着枪指向彼此, 要求两个人必须同时死一样.
struct ListNode {
std::weak_ptr<ListNode> next;
std::weak_ptr<ListNode> prev;
int value;
~ListNode() {
cout << "~ListNode()" << endl;
}
};
只需要将循环链表中的next和prev用weak_ptr的方式托管即可, weak_ptr托管的资源在进行赋值操作的时候, 引用计数不会进行+1操作, 这样的话, node1和node2释放的时候引用计数直接由1变为0, next和prev托管的资源同时被释放.
智能指针的延伸
仿函数定制删除器:
智能指针默认管理的是new出来的单一资源, 但是像malloc或new[mounts]这种情况的资源, 智能指针该如何正常的释放这些类型的资源呢
#include <iostream>
#include <string>
#include <memory>
#include <typeinfo>
using std::cout;
using std::endl;
template<class T>
struct FreeDeletor {
void operator()(T* ptr) {
cout << "delete " << typeid(T).name() << endl;
free(ptr);
}
};
template<class T>
struct ArrayDeletor {
void operator()(T* ptr) {
cout << "delete " << typeid(T).name() << " array" << endl;
delete[] ptr;
}
};
int main() {
FreeDeletor<int> freeInt;
std::shared_ptr<int> sp1((int*)malloc(sizeof(int)), freeInt);
FreeDeletor<std::string> freeString;
std::shared_ptr<std::string> sp2((std::string*)malloc(sizeof(std::string)), freeString);
ArrayDeletor<int> deleteInt;
std::shared_ptr<int> sp3(new int[10], deleteInt);
return 0;
}
这得意于shared_ptr底层的实现, 我们可以写一个仿函数来重载operator()来实现对malloc和new[]资源的正常释放.
boost与c++11中智能指针的关系:
- 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中的实现的。
RAII设计守卫锁:
设计守卫锁的目的是为了防止在加锁后解锁前发生异常安全而导致死锁的情况.
#include <iostream>
#include <thread>
#include <mutex>
using std::cout;
using std::endl;
namespace sock {
template<class Mutex>
class lock_guard {
public:
lock_guard(Mutex& mtx) : _mtx(mtx) {
_mtx.lock();
}
~lock_guard() {
_mtx.unlock();
cout << "~unlock" << endl;
}
private:
// 必须要使用引用, 因为要锁同一个互斥量对象
std::mutex& _mtx;
};
}
std::mutex mtx;
static int n = 0;
void Fun() {
sock::lock_guard<std::mutex> lock(mtx);
size_t i;
for (i = 0; i < 100000; ++i) {
++n;
}
}
int main() {
int begin = clock();
std::thread t1(Fun);
std::thread t2(Fun);
t1.join();
t2.join();
int end = clock();
cout << "n:" << n << endl;
cout << "cost time:" << end - begin << endl;
return 0;
}
还有unique_lock, 它与lock_guard的区别是, 前者可以在生命周期内对资源进行临时加锁和解锁操作.