目录
一、异常层层嵌套+执行流乱跳容易导致内存泄漏
二、使用智能指针解决上述问题
1、RAII
2、像指针一样
3、智能指针=RAII+运算符重载
三、C++98的auto_ptr
四、C++11的unique_ptr和shared_ptr
1、unique_ptr唯一指针
2、shared_ptr共享指针
2.1shared_ptr是否线程安全
2.2shared_ptr的死穴——循环引用
3、weak_ptr弱指针
五、定制删除器
一、异常层层嵌套+执行流乱跳容易导致内存泄漏
int div()
{
throw invalid_argument("除0错误");
}
void Func()
{
// 1、如果p1这里new 抛异常会如何?
// 2、如果p2这里new 抛异常会如何?
// 3、如果div调用这里又会抛异常会如何?
int* p1 = new int;
int* p2 = nullptr;
try
{
int* p2 = new int;
try
{
cout << div() << endl;
}
catch (...)
{
delete p1;
delete p2;
throw;
}
}
catch (...)
{
}
delete p1;
delete p2;
}
int main()
{
try
{
Func();
}
catch (exception& e)
{
cout << e.what() << endl;
}
return 0;
}
本来只想对div()的异常进行捕获,但前边的new如果失败也会抛出异常,导致try/catch层层嵌套,加之异常自带乱跳属性,极有可能造成内存泄漏。可以使用智能指针解决该问题。
二、使用智能指针解决上述问题
1、RAII
RAII是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。对象构造时获取资源,对象析构时自动释放资源。(可以利用局部对象出作用域自动调用析构函数完成对象资源的清理)
2、像指针一样
为了让智能指针像指针一样支持*、->等运算符,还需要在智能指针类中去重载这些运算符。
3、智能指针=RAII+运算符重载
可以把new出来的资源交给智能指针的对象进行管理,对象出了作用域会调用析构函数对资源进行释放。智能指针可以减少内存泄漏的风险。
template <class T>
class SmartPtr
{
public:
SmartPtr(T* ptr)
:_ptr(ptr)
{}
~SmartPtr()
{
if(_ptr)
delete[] _ptr;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T& operator[](size_t pos)
{
return _ptr[pos];
}
private:
T* _ptr;
};
int div()
{
throw invalid_argument("除0错误");
}
void Func()
{
SmartPtr<int> sp1(new int[10]);
SmartPtr<int> sp2(new int[2]);
cout << sp1[0] << endl;
cout << div() << endl;
}
int main()
{
try
{
Func();
}
catch (exception& e)
{
cout << e.what() << endl;
}
return 0;
}
三、C++98的auto_ptr
auto_ptr的拷贝构造和赋值运算符重载会将用于拷贝/赋值的对象清空,对象悬空。很多公司会明确要求禁止使用auto_ptr。
四、C++11的unique_ptr和shared_ptr
1、unique_ptr唯一指针
unique_ptr直接将拷贝构造和赋值给禁用了。简单粗暴。
unique_ptr(const unique_ptr&) = delete;
unique_ptr<T>& operator=(const unique_ptr&) = delete;
2、shared_ptr共享指针
使用构造函数构造一个shared_ptr的对象,这个对象将会获得一个指向资源的指针和一个指向堆区的计数器。每当拷贝构造或赋值时,让这个计数器++。每当减少一个指向该资源的对象,计数器--,直到计数器为零才释放资源及计数器。
namespace jly
{
template <class T>
class shared_ptr
{
public:
shared_ptr(T* ptr)//构造
:_ptr(ptr)
, _pCount(new int(1))
{}
~shared_ptr()
{
Release();
}
shared_ptr(const shared_ptr& sp)//拷贝构造
:_ptr(sp._ptr)
, _pCount(sp._pCount)
{
++(*_pCount);
}
shared_ptr& operator= (const shared_ptr& sp)//赋值运算符重载
{
//需要先判断资源的地址是不是一样,有可能是相同的资源赋值有可能是不同的资源赋值
if (_ptr != sp._ptr)//资源不一样
{
Release();
_ptr = sp._ptr;
_pCount = sp._Pcount;
++(*_pCount);
}
//资源一样的话不用动
return *this;
}
void Release()
{
if (--(*_pCount) == 0)//计数减到零,delete资源
{
delete _pCount;
delete _ptr;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
int* _pCount;
};
}
2.1shared_ptr是否线程安全
说到计数器,就应该想到多线程场景。一旦有多个线程同时访问计数器,对其进行++--操作,势必会发生计数紊乱的问题。需要使用互斥锁对计数器加锁保护:
namespace jly
{
template <class T>
class shared_ptr
{
public:
shared_ptr(T* ptr)//构造
:_ptr(ptr)
, _pCount(new int(1))
,_pMutex(new mutex)
{}
~shared_ptr()
{
Release();
}
shared_ptr(const shared_ptr& sp)//拷贝构造
:_ptr(sp._ptr)
, _pCount(sp._pCount)
,_pMutex(sp._pMutex)
{
_pMutex->lock();
++(*_pCount);
_pMutex->unlock();
}
shared_ptr& operator= (const shared_ptr& sp)//赋值运算符重载
{
//需要先判断资源的地址是不是一样,有可能是相同的资源赋值有可能是不同的资源赋值
if (_ptr != sp._ptr)//资源不一样
{
Release();
_ptr = sp._ptr;
_pCount = sp._Pcount;
_pMutex = sp._pMutex;
_pMutex->lock();
++(*_pCount);
_pMutex->unlock();
}
//资源一样的话不用动
return *this;
}
void Release()
{
bool flag = false;
_pMutex->lock();
if (--(*_pCount) == 0)//计数减到零,delete资源
{
flag = true;
delete _ptr;
delete _pCount;
}
_pMutex->unlock();
if (flag==true)//最后一次记得释放锁
delete _pMutex;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
int* _pCount;
T* _ptr;
mutex* _pMutex;
};
}
struct Date
{
int _year=0;
int _month=0;
int _day=0;
};
int main()
{
int n = 1000000;
jly::shared_ptr<Date> sp1(new Date);
mutex mtx;
thread t1([&]()
{
for (int i = 0; i < n; ++i)
{
jly::shared_ptr<Date> sp2 = sp1;
mtx.lock();
(sp2->_day)++;
(sp2->_month)++;
(sp2->_year)++;
mtx.unlock();
}
});
thread t2([&]()
{
for (int i = 0; i < n; ++i)
{
jly::shared_ptr<Date> sp3 = sp1;
mtx.lock();
(sp3->_day)++;
(sp3->_month)++;
(sp3->_year)++;
mtx.unlock();
}
});
t1.join();
t2.join();
std::cout << (sp1->_day) << " " << (sp1->_month) << " " << (sp1->_year) << std::endl;//cout不明确
return 0;
}
共享指针只对计数器来说是线程安全的,但是对于共享指针指向的资源,却是线程不安全的。如上方代码,将sp1赋值给sp2和sp3,并在lambda表达式中对date中的成员变量进行++,这时资源的是线程不安全的,多线程形式使用共享指针改动资源时需要额外申请一把互斥锁进行保护。
2.2shared_ptr的死穴——循环引用
使用shared_ptr一旦出现循环引用的现象,将会造成内存泄漏。为了解决这一问题,可以使用weak_ptr。
3、weak_ptr弱指针
弱指针通常和共享指针搭配使用,解决共享指针循环引用问题。
weak_ptr支持无参的构造、支持拷贝构造、shared_ptr的拷贝构造。但是不支持使用指针进行构造,不支持RAII。
使用weak_ptr解决shared_ptr的循环引用问题。weak_ptr本身支持shared_ptr的拷贝与赋值,且不会增加引用计数。(weak_ptr不参与资源的管理)
五、定制删除器
析构的方式多种多样,例如数组和文件指针的释放方式不一样。这个时候就需要使用定制删除器进行对象的析构。
template <class T>
struct Delete
{
void operator()(const T* ptr)
{
delete[] ptr;
}
};
int mian()
{
shared_ptr<int> sp1(new int[10], Delete<int>());
shared_ptr<string> sp2(new string[10], Delete<string>());
shared_ptr<string> sp3(new string[10], [](string* ptr) {delete[] ptr; });
shared_ptr<FILE> sp4(fopen("test.txt", "r"), [](FILE* ptr) {fclose(ptr); });
return 0;
}
namespace jly
{
template<class T>
class default_delete
{
public:
void operator()(T* ptr)
{
delete ptr;
}
};
//default_delete<T>作为默认删除器
template<class T, class D = default_delete<T>>
class weak_ptr
{
public:
};
}
外部有传入可调用对象就使用外部传入的,否则使用默认删除器。