0.前言
在C++异常处理时,当程序抛出异常时,程序会直接跳转到最近的捕获区域进行异常的处理,这种处理方式保证了程序的不崩溃,只针对一个区域内的某种错误进行处理。
这样的代码看似很正常,但往往会忽略掉某些内存的处理。例如我们在某个函数调用前向内存申请了部分空间,本来内存的释放逻辑是在函数调用之后进行完成,但是如果函数内部出现异常错误,函数将直接跳转,申请的内存无法释放,就造成了内存泄漏的风险。
1.为什么需要智能指针?
我们首先分析如下代码:
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void func()
{
int* p1 = new int[10]; // 这里可能会抛异常,此时抛异常内存会分配失败,程序结束后不会造成内存泄漏。
int* p2 = new int[10]; // 这里可能会抛异常,此时抛异常会导致p2以及后面的内存分配失败,退出程序后,
//由于p1已经成功申请内存,但是C++没有内存回收机制,因此会造成内存泄漏。
int* p3 = new int[10]; // 这里可能会抛异常
try
{
div();
}
catch (...)
{
delete[] p1;
delete[] p2;
delete[] p3;
throw;
}
delete[] p1;
delete[] p2;
delete[] p3;
}
int main()
{
try
{
func();
}
catch (const exception& e)
{
cout << e.what() << endl;
// ...
}
return 0;
}
为了避免出现内存泄漏的情况,一般对于出现异常问题的异常处理函数中,需要对目前进程中已经分配的内存进行内存释放,如上示代码中的div( )函数所示。但实际项目开发过程中很多人会忘记在适当地方加上delete语句。
在当前的运行环境中,函数调用完毕后进程也就结束了,即便我们没有释放的内存也会在进程结束时一起归还给操作系统,因此这种内存泄漏并没有什么大的风险。 但是,服务器上的程序是一旦开机几乎不会停止的进程,如果我们的内存一直在泄漏,那么我们的机器将会越来越慢,最终因为内存不够而导致进程崩溃。
因此总结认为智能指针就是用来解决内存泄漏问题的。
2.什么是智能指针?
我们将指针托管给某个对象,在对象构造时获取资源,在对象析构时释放资源。这样我们的资源不用再手动进行释放,对象所需要的资源在生命周期内始终有效。智能指针是一种对普通指针进行构造的模板类,使用我们提供的类进行指针的托管。
实现方法可以参考下面类似代码:
template<class T>
class SmartPtr
{
public:
SmartPtr(T* pt)//构造函数
:_ptr(pt)
{}
~SmartPtr()//析构函数,释放内存空间
{
cout<<"delete "<<endl;
if (_ptr)
delete _ptr;
}
T& operator*() { return *(_ptr) ;}//运算符重载
T* operator->() { return _ptr ;}
private:
T* _ptr;
};
3.智能指针—auto_ptr
先观察如下的赋值语句:
auto_ptr< string> p1 (new string("I reigned lonely as a auto_ptr.”);
auto_ptr<string> p2;
p2= p1;
如果p1和p2是常规指针,则两个指针将指向同一个string对象。程序将试图删除同一个对象两次——一次是p1过期时,另一次是p2过期时。要避免这种问题,可以有多种方法:
- 定义赋值运算符:执行深拷贝,使得两个指针指向不同的对象,缺点是浪费空间。
- auto_ptr智能指针:auto_ptr智能指针在进行赋值时,将p1指针的所有权转交给p2指针,p1指针指向nullptr。
避免使用auto_ptr,因为当p1的指针所有权装给p2指针后,p1指针指向空,当再次访问指针时将会引发错误!!!一句话总结就是:避免潜在的内存崩溃问题。
4.智能指针—unique_ptr
下面来看使用unique_ptr的情况:
unique_ptr< string> p1 (new string("I reigned lonely as a unique_ptr.”); //#1
unique_ptr<string> p2; //#2
p2= p1; //#3
编译器认为语句#3非法,避免了p1不再指向有效数据,留下危险的悬挂指针的问题。因此,unique_ptr 比auto_ptr更安全。
unique_ptr 还有更聪明的地方,可以将一个临时的智能指针赋值给另一个unique_ptr指针,举例代码如下:
unique_ptr<string> demo(const char * s)
{
unique_ptr<string> temp (new string(s));
return temp;
}
int main()
{
unique_ptr<string> ps;
ps = demo('Uniquely special");
}
demo()返回一个临时unique_ptr ,然后ps接管了原本归返回的unique_ptr 所有的对象,而返回时临时的 unique_ptr 被销毁,也就是说没有机会使用 unique_ptr 来访问无效的数据,换句话来说,这种赋值是不会出现任何问题的,即没有理由禁止这种赋值。
unique_ptr<string> pu1(new string ("hello world"));
unique_ptr<string> pu2;
pu2 = pu1; // #1 not allowed
unique_ptr<string> pu3;
pu3 = unique_ptr<string>(new string("You")); // #2 allowed
其中#1留下悬挂的unique_ptr(pu1),这可能导致危害。而#2不会留下悬挂的unique_ptr ,因为它调用 unique_ptr 的构造函数,该构造函数创建的临时对象在其所有权让给 pu3 后就会被销毁。
您可能确实想执行类似于#1的操作,要安全的重用这种指针,可给它赋新值。C++有一个标准库函数std::move(),使用move后,原来的指针仍转让所有权变成空指针,让你能够将一个unique_ptr赋给另一个。
下面是一个使用前述demo()函数的例子,该函数返回一个unique_ptr 对象:
unique_ptr<string> ps1, ps2;
ps1 = demo("hello");
ps2 = move(ps1);
ps1 = demo("alexia");
cout << *ps2 << *ps1 << endl;
5.智能指针—share_ptr
shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。引用计数支持多个拷贝管理同一个资源,最后一个析构对象释放资源。
- shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享;
- 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一;
- 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
- 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了;
std::shared_ptr的循环引用
案例如下:
struct ListNode
{
int _data;
shared_ptr<ListNode> _prev;
shared_ptr<ListNode> _next;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
shared_ptr<ListNode> node1(new ListNode);//此时node1引用计数为1
shared_ptr<ListNode> node2(new ListNode);//此时node2引用计数为1
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
node1->_next = node2;//此时node2引用计数为2
node2->_prev = node1;//此时node1引用计数为2
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
//到了此处node1和node2对象不再使用,调用析构函数释放node1、node2。
//此时node1和node2的引用计数为1,但是还是无法释放
return 0;
}
- node1和node2两个智能指针对象指向两个节点,node1和node2的引用计数变成1。
- node1的_next指向node2,node2的_prev指向node1,node1和node2的引用计数变成2。
- node1和node2析构,引用计数减到1,但是node1->_next还指向node2节点。node2->_prev还指向node1节点。
- 即是说node1->_next析构了,node2才能被释放,node2->_prev析构了,node1才能被释放。
- 但是node1->_next属于node1的成员,node1释放了,node1->_next才会析构,而node1由node2->_prev管理,node2->_prev属于node2成员,所以这就叫循环引用,谁也不会释
解决方法是在引用计数的场景下,把节点中的_prev和_next成员改成weak_ptr。此时:
node1->_next = node2;
node2->_prev = node1;
weak_ptr的_next和 _prev不会增加node1和node2的引用计数。
6.智能指针—weak_ptr
std::weak_ptr 要与 std::shared_ptr 一起使用。 一个 std::weak_ptr 对象看做是 std::shared_ptr 对象管理的资源的观察者,它不影响共享资源的生命周期。
- 如果需要使用 weak_ptr 正在观察的资源,可以将 weak_ptr 提升为 shared_ptr。
- 当 shared_ptr 管理的资源被释放时,weak_ptr 会自动变成 nullptr。
各类智能指针的具体实现代码可以参考:C++智能指针详解_李 ~的博客-CSDN博客_c++ 智能指针定义