我们经常会遇到指针忘记释放的问题,有时也不可避免,例如捕捉异常时会改变执行流,本来在程序结束前写好了释放,最终没有执行,造成内存泄漏。
有一种解决方法,使用RAII(resource acquisition is initialisition)技术,即使用局部对象控制资源,这就是智能指针。
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期管理资源的编程技术,特别用于处理资源分配和释放。在C++中,RAII的工作原理基于以下两个关键概念:
资源与对象的绑定:在RAII中,资源(如动态分配的内存、文件句柄、锁等)被封装在一个对象中。当对象被创建时,资源被分配;当对象的生命周期结束时,其析构函数负责释放资源。
自动资源管理:由于资源是通过对象管理的,资源的分配和释放是自动进行的。对象的析构函数在对象离开其作用域时自动被调用,无论是由于正常的程序流程还是因为异常。
智能指针(如std::unique_ptr
、std::shared_ptr
)是RAII原则的经典应用。它们封装了对动态分配内存的指针,确保在智能指针对象销毁时自动释放内存。这样,智能指针帮助避免了内存泄漏——即忘记释放分配的内存,这在使用原始指针时是一个常见问题。
通过使用智能指针,开发者不需要显式地调用delete
,大大减少了内存泄漏的风险。这使得代码更安全、更易于维护,并且提高了异常安全性。
auto_ptr
在现代C++中被废弃,主要是因为它在设计上存在一些关键的缺陷,尤其是与所有权和对象复制相关的问题:
所有权转移:auto_ptr
的一个主要问题是它的复制语义。当一个auto_ptr
对象被另一个auto_ptr
复制时,所有权(即对内存资源的控制权)会从一个对象转移到另一个对象。这意味着原始的auto_ptr
对象将变为空(null),丧失对资源的控制权。
悬空指针风险:由于所有权的转移,使用auto_ptr
容易导致悬空指针问题。一旦auto_ptr
对象被复制,原始指针就会失效,但是代码可能仍然尝试使用它,这可能导致不可预测的行为和程序崩溃。
与STL容器的不兼容:auto_ptr
不能安全地用在标准模板库(STL)容器中,因为这些容器的元素经常需要被复制和赋值,而auto_ptr
的复制语义会导致问题。
例如:
int main()
{
auto_ptr pi1(new int(1));
auto_ptr pi2(pi1);
*pi2 = 10;//程序会崩溃
return 0;
}
模拟实现
template
class auto_ptr
{
private:
T* _ptr;
public:
auto_ptr(T* ptr)
:_ptr(ptr)
{
}
~auto_ptr()
{
std::cout << "delete:" << ' ' << _ptr << std::endl;
delete _ptr;
}
T operator* ()
{
return *_ptr;
}
T* operator-> ()
{
return _ptr;
}
auto_ptr(auto_ptr& ap)
{
_ptr = ap._ptr;
ap._ptr = nullptr;
}
auto_ptr& operator= (auto_ptr& ap)
{
this->_ptr = ap._ptr;
ap._ptr = nullptr;
return *this;
}
};
std::unique_ptr
在 C++ 中是一种智能指针,用于管理动态分配的内存。它保证同一时间只有一个 unique_ptr
拥有对某个对象的所有权,从而确保当 unique_ptr
被销毁时,它指向的对象也会被自动销毁
void testup()
{
unique_ptr uq1(new int(1));
unique_ptr uq2(new int(2));
//unique_ptr uq3(uq1);//报错
}
模拟实现:
template
class unique_ptr
{
private:
T* _ptr;
public:
unique_ptr(T* ptr)
:_ptr(ptr)
{
}
~unique_ptr()
{
delete _ptr;
}
T operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
unique_ptr(const unique_ptr& up) = delete;
unique_ptr operator= (const unique_ptr& up) = delete;
};
std::shared_ptr
是一种智能指针,用于实现多个指针对象之间的资源共享。以下是关于如何使用 shared_ptr
进行资源共享以及其优势和限制的探讨:
shared_ptr
允许多个指针实例共同拥有一个对象的所有权。当最后一个拥有该对象的 shared_ptr
被销毁或重置时,对象被删除。shared_ptr
使用引用计数机制来跟踪有多少个 shared_ptr
实例指向同一个资源。每当新的 shared_ptr
指向该资源或某个 shared_ptr
被销毁时,计数相应增加或减少。shared_ptr
间形成循环引用,会导致内存泄漏。这种情况可以通过使用 weak_ptr
来解决。void testsp()
{
mwk::shared_ptr sp1(new int(1));
mwk::shared_ptr sp2(new int(2));
mwk::shared_ptr sp3(sp1);
sp3 = sp3;
sp1 = sp3;
mwk::shared_ptr sp4(new int(4));
sp4 = sp2;
}
模拟实现:
template
class shared_ptr
{
private:
T* _ptr;
int* _pcount;
public:
shared_ptr(T* ptr)
:_ptr(ptr)
,_pcount(new int(1))
{
std::cout << "new:" << *_ptr << ' ' << std::endl;
}
~shared_ptr()
{
if (--(*_pcount) == 0)
{
std::cout << "delete:" << *_ptr << ' ' << _ptr << std::endl;
delete _ptr;
delete _pcount;
}
}
T operator* ()
{
return *_ptr;
}
T* operator-> ()
{
return _ptr;
}
shared_ptr(const shared_ptr& sp)
:_ptr(sp._ptr)
,_pcount(sp._pcount)
{
++(*_pcount);
}
shared_ptr& operator=(const shared_ptr& sp)
{
if (this->_ptr != sp._ptr)
{
this->~shared_ptr();
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
}
return *this;
}
};
std::weak_ptr
在实际编程中主要用于解决 std::shared_ptr
可能产生的循环引用问题,并且用于观察共享对象而不拥有其所有权。以下是一些典型的 weak_ptr
使用场景:
打破循环引用:当两个对象互相通过 shared_ptr
持有对方时,会产生循环引用,导致内存泄漏。使用 weak_ptr
替换其中一个 shared_ptr
可以打破这种循环。
缓存实现:weak_ptr
可用于实现对象的缓存机制。缓存中的对象可以通过 shared_ptr
管理,而外部访问时使用 weak_ptr
,这样即使缓存的对象被删除,也不会影响整体程序逻辑。
观察者模式:在观察者模式中,weak_ptr
可用于安全地观察被观察对象,而不会延长其生命周期。
资源的可用性检查:由于 weak_ptr
不影响其指向的对象的生命周期,它可以用来检查资源是否仍然存在。可以通过将 weak_ptr
提升为 shared_ptr
来安全地访问资源,前提是资源仍然存在。
通过这些使用场景,weak_ptr
在避免资源泄漏和提供灵活的资源访问策略方面发挥着重要作用。
循环引用的例子:
class list
{
public:
std::shared_ptr _next;
std::shared_ptr _prev;
~list()
{
std::cout << "delete" << std::endl;
}
};
void testwp()
{
std::shared_ptr ls1(new list);
std::shared_ptr ls2(new list);
ls1->_next = ls2;
ls2->_prev = ls1;
}
ls1->next = ls2会ls2的计数变为2,ls2->prev = ls1会让ls1的计数变为2;
当析构ls1和ls2的时候,ls1和ls2的计数都变为1,并没有delete;只有当ls1->next析构时,ls2才会析构,当ls2->prev析构时,ls1才会析构;但是next和prev都是成员,只有ls1和ls2析构的时候才会析构。构成一个循环,无法delete。
解决方法就是使用weak_ptr
weak_ptr
通常与 shared_ptr
配合使用。它通过 shared_ptr
来创建,但不增加引用计数。这个例子心中,node1->_next = node2;和node2->_prev = node1;时weak_ptr的_next和_prev不会增加node1和node2的引用计数。
class list
{
public:
std::weak_ptr _next;
std::weak_ptr _prev;
~list()
{
std::cout << "delete" << std::endl;
}
};
void testwp()
{
std::shared_ptr ls1(new list);
std::shared_ptr ls2(new list);
std::cout << ls1.use_count() << std::endl;//1
std::cout << ls2.use_count() << std::endl;//1
ls1->_next = ls2;
ls2->_prev = ls1;
std::cout << ls1.use_count() << std::endl;//1
std::cout << ls2.use_count() << std::endl;//1
}