为什么要智能指针?
因为我们有时候会主动地在堆上申请空间,并且该申请空间还需要我们手动的释放,比如free(),delete等,但是我们有时难免会忘记对某个空间进行释放,这就造成了资源浪费和内存泄漏。内存泄露的危险还是极大地,什么是内存泄漏?内存泄漏是指因为疏忽或者错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为涉及错误,失去了对该段内存的控制,因而造成内存的浪费。
内存泄露的危害:长期运行的程序出现内存泄漏,影响很大,会影响操作系统,后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。
内存泄漏分类(了解)
C/C++程序中一般我们关心两种方面的内存泄漏:
堆内存泄漏(Heap leak)
堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。
系统资源泄漏
指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放
掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。
智能指针的使用及原理
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
它是把我们新申请的那部分空间交给一个对象去管理,而对象在声明周期结束后就会自动释放对象内的资源,这样就不存在资源泄露的问题了
通俗来讲该对象管理的就是指向该申请内存的指针。
#include <iostream>
using namespace std;
template<class T>
class SmartPtr
{
public:
SmartPtr(T* ptr = nullptr)
:_ptr(ptr)
{
cout << "构造函数" << typeid(T).name() << endl;
}
~SmartPtr()
{
cout << "析构函数" << typeid(T).name() << endl;
delete _ptr;
}
private:
T* _ptr;
};
int main()
{
SmartPtr<string> sp1(new string);
SmartPtr<int> sp2(new int);
return 0;
}
这样就是sp1,sp2声明周期结束了,他管理的对象string,int等申请的内存也会自动释放,就不需要我们再去显式的释放了。
上述的SmartPtr还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可以通过->去访问所指空间中的内容,因此:AutoPtr模板类中还得需要将* 、*->重载下,才可让其
像指针一样去使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
若要使用新开辟空间对应的指针,只需sp1->operator()即可。
#include <iostream>
using namespace std;
template<class T>
class SmartPtr
{
public:
SmartPtr(T* ptr = nullptr)
:_ptr(ptr)
{
cout << "构造函数" << typeid(T).name() << endl;
}
~SmartPtr()
{
cout << "析构函数" << typeid(T).name() << endl;
delete _ptr;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
struct Date
{
int _year;
int _month;
int _day;
};
int main()
{
SmartPtr<Date> sp1(new Date);
sp1->_year = 2023;
sp1->_month = 10;
sp1->_day = 10;
cout << (*sp1)._year << ' ' << (*sp1)._month << ' ' << (*sp1)._day << endl;
return 0;
}
注意这里的对结构体成员的赋值,比如对成员_year来说本来是需要用
sp1.operator->()->_year = 2023来进行赋值,但是编译器对这种冗余的行为做了处理,为了增加可读性省略了一个->
总结一下智能指针的原理:
- RAII特性
- 重载operator*和opertaor->,具有像指针一样的行为。
std::auto_ptr
C++98库中就提供了智能指针,但是它也具有一定的问题,我们接下来进行演示它的优缺点
首先说明auto_ptr是一个失败的设计,很多公司都禁止使用它
#include <iostream>
using namespace std;
namespace sw
{
template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr)
:_ptr(ptr)
{
}
//拷贝构造
auto_ptr(auto_ptr<T>& ap)
:_ptr(ap._ptr)
{
ap._ptr = nullptr;//拷贝构造后原对象成员指针_ptr改为nullptr
}
//赋值构造函数
auto_ptr<T>& operator=(const auto_ptr<T>& ap)
{
//检查是否是自己给自己赋值
if (&ap != *this)
{
//释放当前对象已有的资源
if (_ptr)
{
delete _ptr;
}
_ptr = ap._ptr;
ap._ptr = nullptr;
}
return *this;
}
~auto_ptr()
{
if (_ptr)
{
delete _ptr;
}
}
T* operator->()
{
return _ptr;
}
T& operator*()
{
return *_ptr;
}
private:
T* _ptr;
};
}
int main()
{
sw::auto_ptr<int> ap1(new int);
cout << typeid(ap1).name() << endl;
sw::auto_ptr<int> ap2(ap1);
sw::auto_ptr<int> ap3;
ap3 = ap1;
return 0;
}
为什么说他是一个失败的设计呢?
因为当我们完成拷贝构造或者赋值构造之后,原对象就是空的了,我们后续如果忘记了,再去使用这个对象时就会发生错误,也就是野指针问题。那么如何解决这种问题呢?那就需要用到C++11中更靠谱的unique_ptr了。
unique_ptr
unique_ptr解决拷贝后原对象为空的方法就是简单粗暴地防止拷贝。
//unique_ptr
#include <iostream>
using namespace std;
namespace sw
{
template<class T>
class unique_ptr
{
public:
unique_ptr(T* ptr = nullptr)
:_ptr(ptr)
{
}
//拷贝构造
unique_ptr(const auto_ptr<T>& ap) = delete;
//赋值构造函数
unique_ptr<T>& operator=(const unique_ptr<T>& ap) = delete;
~unique_ptr()
{
if (_ptr)
{
delete _ptr;
}
}
T* operator->()
{
return _ptr;
}
T& operator*()
{
return *_ptr;
}
private:
T* _ptr;
};
}
可以让函数只定义不实现,也可以直接删除该函数只需后面加=delete但一般选择后者,因为前者也可能会导致误用,但是这种过于粗暴的方法也不好,如果就是需要我们赋值或者拷贝构造又怎么办呢?因此在C++11中还有一个智能指针shared_ptr.
注:如果确定不会对这个对象进行拷贝,建议使用unique_ptr
shared_ptr
C++11中提供的更靠谱的并且支持拷贝的shared_ptr。
shared_ptr的原理是通过引用计数的方式来实现多个shared_ptr对象之间的资源共享。例如我们在学校放学时,老师总是会说最后一个走的同学关灯。与这个道理一样,就是同一份资源被多个对象共享,当这个对象是最后一个对象时,这个对象就要负责把资源清理掉,这样就不会导致同一块空间析构两次或多次的问题了。
1. shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。
2. 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减
一。
3. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
4. 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。
引用计数支持多个拷贝管理同一个资源,最后一个析构对象释放资源。
shared_ptr智能指针模拟实现重难点在于拷贝构造和拷贝赋值这两个函数
namespace sw
{
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr)
:_ptr(ptr)
,_pcount(new int(1))
{
}
//拷贝构造
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
,_pcount(++(*sp._pcount))
{
}
//赋值拷贝
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr == sp._ptr)
{
return *this;
}
if (--(*_ptr) == 0)
{
//如果当前对象是它的对象最后一个实例时,在变成新的对象前要进行释放此对象原有的资源
delete _ptr;
delete _pcount;
}
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
return *this;
}
int use_count() const
{
return *_pcount;
}
T* get() const
{
return _ptr;
}
~shared_ptr()
{
delete _ptr;
}
private:
T* _ptr;
int* _pcount;//开辟了一个空间用来存储有多少个对象在共享这份空间
//static int count;//思考:为什么不能用静态成员变量代替引用计数?
};
}
回答遇到的问题,为什么不用静态成员变量代替引用计数?
这是因为如果是多个对象管理了多个不同的空间,比如A,B,C,D不同的空间,那么在使用静态成员变量时,无论是哪个对象对这些空间进行共享,那么count都会增加,而只有count减到0时,才会对空间进行释放,这样就导致了只会释放A,B,C,D空间中的一个,从而导致其它几个空间都不会被释放。而用计数引用呢,我们会先判断管理的空间地址是否一致,如果一致就只会对相应的计数引用_pcount进行加减,这样就可以让不同的空间都有一个计数引用.
weak_ptr
shared_ptr并不是完全可以保证不会导致内存泄露的,就比如循环引用的情况。
以一个例子为例:例如双链表,_next,_prev
struct Node
{
shared_ptr<Node> _next;
shared_ptr<Node> _prev;
};
int main()
{
sp1->_next = sp2;
sp2->_prev = sp1;
return 0;
}
这样就会造成死循环,如何解决呢?
首先解释一下上面的结构体,成员为什么不是shared_ptr*,不是存放前后节点的指针吗?因为为了避免内存泄漏我们也选择把前后节点的指针去让智能指针来管理
此时就要用到weak_ptr,注意weak_ptr为若指针,是shared_ptr的小弟,不属于RAII智能指针,作用只有一个,就是用来专门解决shared_ptr的循环引用问题,它解决的原理就是不增加引用计数