今天主要是总结下我刚学习的智能指针,分为四方面进行智能指针的整理。注意:本次使用的编译语言为C++,编译器为VS2013。下面直接开始今天的正题吧
为什么要使用智能指针?
首先我们先来看一段代码:
int main() {
try {
int* p = new int[10];// 动态分配内存失败
throw std::exception();//获取异常
delete[] p;// 在异常抛出前的代码中进行内存释放
}
catch (...) {
std::cout << "Exception caught!" << std::endl;
}
_CrtDumpMemoryLeaks();//VS提供的一种函数,用于检测内存泄漏并输出信息
return 0;
}
此时,当我们查看编译器输出时,会出现以下提示:
这就说明,我们的程序出现了内存泄漏。除了上述异常中断内存释放,我们在写一些大型的项目时,也会出现遗漏释放new开辟的空间,new和delete不匹配等原因,导致内存泄漏或野指针的情况。
内存泄漏
在讲解智能指针之前,我们需要了解什么是内存泄漏?它的危害又是什么?
内存泄漏:指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
简单来说,就是内存没有正确的释放,导致内存空间一直被占据,无法在被程序使用,从而造成内存资源浪费的情况。
内存泄漏会导致系统运行速度变慢,甚至会导致系统崩溃。在长时间运行的程序中,内存泄漏问题会越来越严重,最终可能导致系统无法正常运行,需要重启系统或者重新启动程序。因此,内存泄漏是一个非常严重的问题,需要及时检测和解决。因此我们在运行程序中需要警惕,避免出现内存泄漏。针对内存泄漏,C++也提供了一种指针可以帮助我们有效的解决内存泄漏的问题,那么就是智能指针。
智能指针
智能指针是一种在C++语言中用来管理动态内存的工具。它可以自动管理内存的生命周期,避免一些常见的内存管理错误,如内存泄漏和野指针等问题。智能指针实际上是一个封装了指针的类,它重载了指针相关的运算符,并提供了一些额外的功能,如自动内存分配、自动释放、引用计数等。
C++11标准库中提供了两种常用的智能指针:unique_ptr和shared_ptr,但是在这之前,其实还有一种智能指针,虽然我们不会使用它,但我们还是可以先了解一下该指针,再去了解现在所使用的指针。
auto_ptr
auto_ptr是 C++98 标准库中的一个智能指针,它允许程序员管理动态分配的对象,同时避免内存泄漏和空指针引用等问题。
auto_ptr的主要特点是在析构时自动释放它所管理的对象,从而避免手动释放内存的麻烦。同时,拷贝构造函数和赋值运算符重载会将原先的指针置为nullptr,从而避免悬空指针的问题。
我们来看一个具体的缺陷:
int main() {
std::auto_ptr<int> p1(new int(42));
std::auto_ptr<int> p2(p1);
std::cout << *p1 << std::endl;// 输出结果不确定,可能会崩溃
return 0;
}
该问题主要是,p1将自己资源的所有权都交给了p2,此时的p1就变为了野指针,当我们在访问p1时,程序就可能崩溃或者输出不确定的结果。
以下是auto_ptr指针的简单实现:
template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr = nullptr)
:_ptr(ptr)
, _owner(false)
{
if (_ptr)
{
_owner = true;
}
}
~auto_ptr()
{
if (_ptr&&_owner)
{
delete _ptr;
_ptr = nullptr;
_owner = false;
}
}
//具有指针类似的行为
T* operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
//资源转移
auto_ptr(auto_ptr<T>& ap)//拷贝构造
:_ptr(ap._ptr)
, _owner(ap._owner)
{
ap._owner = false;
delete ap._ptr;
}
auto_ptr<T>& operator=(auto_ptr<T>& ap)
{
if (this != &ap)
{
if (_ptr&&_owner)//查看当前指针所在位置是否有资源,有需要删除
{
delete _ptr;
}
_ptr = ap._ptr;//接手别人的资源
_owner = ap._owner;
ap._owner = false;
}
return *this;
}
private:
T* _ptr;
bool _owner;//资源释放的权限
};
由于设计的缺陷,C++11标准中已经弃用了该指针,并在C++17标准中被完全移除了。现在我们更推荐使用unique_ptr和shared_ptr指针。
unique_ptr
unique_ptr是C++11标准中的一种智能指针,其主要特点就是拥有资源的独占所有权,即同一时间,只能有一个unique_ptr指针指向同一个对象,当unique_ptr被销毁时,它锁管理的对象也会被销毁。这样也就可以避免犯auto_ptr的错误,更好的避免内存泄漏等问题。
简单来说,unqiue_ptr指针就是防止一个指针拷贝另一个指针,因此,在封装该指针时,就会禁止生成拷贝构造函数或者赋值运算符重载。
在C++11中,防止生成某种函数的方法就是在函数后面加上=delete,我们来看具体实现和结果:
此时,运行程序:
此时编译器就会告诉我们,引用的是一个已删除的函数。
其次,我们还可以将拷贝构造函数和赋值运算符重载权限设置为私有的,那么我们在外部也不能调用该函数,实现上述效果。
以下是unqiue_ptr指针的简单实现:
template<class T>
class DFDef//删除器
{
public:
void operator()(T*& ptr)
{
if (ptr)
{
delete ptr;
ptr = nullptr;
}
}
};
template<class T, class DF = DFDef<T>>//默认处理方式
class unique_ptr
{
public:
unique_ptr(T* ptr = nullptr)
:_ptr(ptr)
{}
~unique_ptr()
{
if (_ptr)
{
DF()(_ptr);
_ptr = nullptr;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T* Get()
{
return _ptr;
}
//C++11中实现防拷贝
unique_ptr(const unique_ptr<T>&) = delete;
unique_ptr<T> operator=(const unique_ptr<T>&) = delete;
private:
T* _ptr;
};
该指针的实现相对简单,主要是通过=delete来限制拷贝构造函数和赋值运算符重载自动生成。但是,对于大型项目而言,unique_ptr指针还是存在它的缺陷,无法实现资源的共享;并且,由于无法实现资源共享,那么在移动对象时,也需要提供移动构造函数和移动运算符重载,大大增加代码的复杂程度,因此C++11还提供了一种智能指针来解决上述问题。
shared_ptr
shared_ptr是C++11标准库中的智能指针,用于管理动态分配的内存。其特点在于可以自动进行内存的引用计数和释放,实现内存的共享,从而避免内存泄漏和悬挂指针等问题,提高代码的安全性和可维护性。
对于shared_ptr的用法,就和上述的两种智能指针用法类似,那么接下来我们来看下底层原理。对于shared_ptr的计数器,很多人想到的都是static修饰的静态成员变量,但是在这里静态成员变量却不能满足我们的要求。我们来看个示例:
#include<memory>
//构建一个static修饰的计数器
namespace lzx
{
template<class T>
class shared_ptr
{
public:
shared_ptr(T* a = nullptr)
:_a(a)
{
if (_a)
{
_b = 1;
}
}
~shared_ptr()
{
if (_a && 0 == --_b)
{
delete _a;
_a = nullptr;
}
}
T& operator*()
{
return *_a;
}
T* operator->()
{
return _a;
}
shared_ptr(shared_ptr<T>& sp)
:_a(sp._a)
{
_b++;
}
private:
int* _a;
static int _b;//计数器
};
template<class T>
int shared_ptr<T>::_b = 0;
}
void Test()
{
lzx::shared_ptr<int> a(new int(10));
lzx::shared_ptr<int> b(a);//此时计数器变为2
lzx::shared_ptr<int> c(new int(10));
}
int main()
{
Test();
_CrtDumpMemoryLeaks();//检测是否有数据丢失
return 0;
}
这里我来解释下代码的作用,class类封装了一个static修饰的计数器,当有指针指向同一对象时,计数器++。从测试Test()函数可以看出,a和b指向同一地址空间,c单独指向一个地址空间;那么按照我们的想法,资源是正常释放的,但是,当我们运行时就会发现,有4字节的资源泄漏了:
这是为什么呢??
我们进一步调试代码,发现当对象c创建后,a和c的计数器都会++,也就是说这个const修饰的计数器作用域类的全部对象,只要有对象被创建,不管是不是指向同一空间,计数器都会++,这也就导致了之后的a,b对象内存并没有被释放,从而造成内存泄漏。
因此,我们这里不能使用static修饰的静态成员变量作为计数器,会有很大错误!!这里,我们可以使用一个指针指向一块空间来充当计数器,这样,在赋值的过程中,只有指向相同资源的对象才会指向相同的计数器,下面我们来简单实现以下shared_ptr指针:
template<class T>
class DFDef//删除器
{
public:
void operator()(T*& ptr)
{
if (ptr)
{
delete ptr;
ptr = nullptr;
}
}
};
template<class T,class DF=DFDef<T>>//默认删除方式
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
, _pcount(nullptr)
, _pMutex(nullptr)
{
if (_ptr)
{
_pcount = new int(1);
_pMutex = new mutex();
}
}
~shared_ptr()
{
Release();
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
//解决浅拷贝问题:引用计数
shared_ptr(const shared_ptr<T>& sp)//拷贝构造函数
:_ptr(sp._ptr)
, _pcount(sp._pcount)
, _pMutex(sp._pMutex)
{
AddRef();
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)//赋值运算赋重载
{
if (this != &sp)
{
Release();
//共享资源和计数++
_ptr = sp._ptr;
_pcount = sp._pcount;
_pMutex = sp._pMutex;
++(*_pcount);
}
}
private:
void AddRef()//指向对象指针+1
{
_pMutex->lock();
++(*_pcount);
_pMutex->unlock();
}
void Release()//释放对象
{
bool flag = false;
_pMutex->lock();//保证原子操作
if (_ptr && 0 == --(*_pcount))//若减完计数器为0,则释放对象
{
DF()(_ptr);
delete _pcount;
_pcount = nullptr;
flag = true;
}
_pMutex->unlock();
if (flag)//若为最后一个指针,释放锁空间
{
delete _pMutex;
_pMutex = nullptr;
}
}
private:
T* _ptr;
int* _pcount;//计数必须为静态成员变量
mutex* _pMutex;//锁机制
};
这里我给大家画个图更好理解代码:
这样也就实现了在相同类中,不同对象分别计数的功能,之后也就不会出现上述内存泄漏的问题。
相对的,shared_ptr指针也有它的缺陷:
- 引用计数器需要动态开辟内存空间,增加了内存开销和运行时的效率
- 引用计数的方式可能会导致循环引用问题,如果两个对象互相持有 ,它们的引用计数永远不会变为 0,从而导致内存泄漏。
- 不能管理动态数组,因为它使用而不是来释放内存,可能导致未定义的行为。
因此,使用上述智能指针需要谨慎使用,使用哪种类型的智能指针需要适合它的场景,并不是shared_ptr最好,需要考虑对应的场景。
标签:泄漏,auto,智能,内存,shared,ptr,指针 From: https://blog.51cto.com/u_15209404/6442256