文章目录
- 一、什么是智能指针
- 二、智能指针的原理
- 三、智能指针的使用
- 1、[std::auto_ptr](https://legacy.cplusplus.com/reference/memory/auto_ptr/auto_ptr/)
- 2、[std::unique_ptr](https://legacy.cplusplus.com/reference/memory/unique_ptr/?kw=unique_ptr)
- 3、[std::shared_ptr](https://legacy.cplusplus.com/reference/memory/shared_ptr/?kw=shared_ptr)
- 4、[std::weak_ptr](https://legacy.cplusplus.com/reference/memory/weak_ptr/?kw=weak_ptr)
- 四、循环引用问题
- 五、内存泄漏
一、什么是智能指针
C++中的智能指针是一种特殊的模板类,用于自动管理动态分配的内存,帮助程序员避免内存泄漏和悬垂指针(指向曾经存在的对象,但该对象已经不再存在了)等问题。智能指针通过封装原始指针,并在适当的时候自动释放所指向的内存,从而简化了内存管理。
二、智能指针的原理
1、RALL
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内 存、文件句柄、网络连接、互斥量等等)的简单技术。 在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在 对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。
这种做法有两大好处:
不需要显式地释放资源。
采用这种方式,对象所需的资源在其生命期内始终保持有效。
智能指针是RAII思想的一种实现:智能指针通过封装裸指针,并在析构函数中自动释放资源,实现了RAII的要求。它们利用对象的生命周期来管理资源的生命周期,确保资源在不再需要时能够被及时释放。
2、智能指针具有和指针一样的功能
*
和 ->
。
三、智能指针的使用
1、std::auto_ptr
(1)介绍
C++98版本的库中就提供了
auto_ptr
的智能指针, 其拷贝和赋值使用的思想是管理权转移的思想。
(2)使用
void test_auto_ptr()
{
//用指针初始化
auto_ptr<int> ap1(new int(10));
cout << "ap1: " << *ap1 << endl;
//拷贝ap1
auto_ptr<int> ap2(ap1);
cout << "ap2: " << *ap2 << endl;
}
(3)简单模拟实现
template<class T>
class auto_ptr
{
public:
//构造
auto_ptr(T* ptr = nullptr) :_ptr(ptr)
{}
//拷贝构造 -- 控制权转移
auto_ptr( auto_ptr<T> & ap):_ptr(ap._ptr)
{
ap._ptr = nullptr;
}
//赋值重载 -- 控制权转移
auto_ptr<T>& operator=( auto_ptr<T>& ap)
{
if (_ptr != ap._ptr)
{
//转移前先将原来的数据清除
release();
_ptr = ap._ptr;
ap._ptr = nullptr;
}
}
//析构
~auto_ptr()
{
release();
}
//清除
void release()
{
delete _ptr;
_ptr = nullptr;
}
//解引用
T& operator*() const
{
return *_ptr;
}
//->
T* operator->() const
{
return _ptr;
}
private:
T* _ptr;
};
2、std::unique_ptr
(1)介绍
std::unique_ptr
表示对对象的独占所有权。它确保同一时间内只有一个std::unique_ptr
指向给定对象。当std::unique_ptr
被销毁(例如,离开作用域)时,它所指向的对象也会被删除。std::unique_ptr
不支持拷贝语义,但支持移动语义,允许所有权从一个std::unique_ptr
转移到另一个。
(2)使用
void test_unique_ptr()
{
//用指针初始化
unique_ptr<int> up1(new int(10));
cout << "up1: " << *up1 << endl;
//不支持
//unique_ptr<int> up2 = up1;
//支持移动语义
unique_ptr<int> up2(unique_ptr<int>(new int(10)));
cout << "up2: " << *up2 << endl;
}
(3)简单模拟实现
template<class T>
class unique_ptr
{
public:
//构造
unique_ptr(T* ptr = nullptr) :_ptr(ptr)
{}
//移动构造
unique_ptr(unique_ptr<T>&& up)
{
_ptr = up._ptr;
up._ptr = nullptr;
}
//析构
~unique_ptr()
{
release();
}
//清理
void release()
{
delete _ptr;
_ptr = nullptr;
}
//解引用
T& operator*() const
{
return *_ptr;
}
//->
T* operator->() const
{
return _ptr;
}
private:
//禁用掉拷贝和赋值重载
unique_ptr(const unique_ptr<T>& ap) = delete;
unique_ptr<T>& operator=(const auto_ptr<T>& ap) = delete;
T* _ptr;
};
3、std::shared_ptr
(1)介绍
shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。
std::shared_ptr
在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共 享。- 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减 一。
- 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源。
- 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对 象就成野指针了。
std::shared_ptr
支持拷贝和移动语义。
(2)使用
void test_shared_ptr()
{
//用指针初始化
shared_ptr<int> sp1(new int(10));
cout << "sp1: " << *sp1 << endl;
cout << "引用次数:" << sp1.use_count() << endl;
//支持拷贝
shared_ptr<int> sp2 = sp1;
cout << "sp2: " << *sp2 << endl;
cout << "引用次数:" << sp2.use_count() << endl; //use_count() 返回该指针被引用的次数
//支持移动语义
shared_ptr<int> sp3(shared_ptr<int>(new int(20)));
cout << "sp3: " << *sp3 << endl;
cout << "引用次数:" << sp3.use_count() << endl;
}
(3)简单模拟实现
template <class T>
class shared_ptr
{
public:
//构造
shared_ptr(T* ptr = nullptr) :_ptr(ptr), _pcount(new int(1))
{};
//传指针 + 删除器的构造
shared_ptr(T* ptr, std::function<void(T*)> del) :_ptr(ptr), _del(del),_pcount(new int(1))
{};
//拷贝构造
shared_ptr(const shared_ptr<T>& sp)
{
_ptr = sp._ptr;
_pcount = sp._pcount;
(*_pcount)++;
}
//赋值重载函数
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (sp._ptr == _ptr) return *this;
//赋值前清除
release();
_ptr = sp._ptr;
_pcount = sp._pcount;
(*_pcount)++;
return *this;
}
//析构函数
~shared_ptr()
{
release();
}
//删除
void release()
{
//当引用计数等于0时就需要清除
if (--(*_pcount) == 0 && _ptr != nullptr)
{
_del(_ptr);
delete _pcount;
_ptr = nullptr;
_pcount = nullptr;
}
}
//返回引用计数
int use_count() const
{
return *_pcount;
}
//解引用
T& operator*() const
{
return *_ptr;
}
//->
T* operator->() const
{
return _ptr;
}
//取指针
T* get()const
{
return _ptr;
}
//取引用计数的指针(这里是为了在实现weak_ptr用)
const int* getcount()const
{
return _pcount;
}
private:
//指针
T* _ptr;
//引用计数
int* _pcount;
//删除器
std::function<void(T*)> _del = [](T* ptr) {delete ptr; };//默认删除器
};
4、std::weak_ptr
(1)介绍
std::weak_ptr
是一种不拥有其所指对象的智能指针。它主要是为了配合std::shared_ptr
使用的,用来解决std::shared_ptr 之间可能产生的循环引用问题。std::weak_ptr
不增加对象的所有权计数,因此不会阻止对象的销毁。但是,它可以在不改变所有权计数的情况下检查对象是否仍然存在。
(2)使用
void test_weak_ptr()
{
//用指针初始化
shared_ptr<int> sp1(new int(10));
cout << "被weak_ptr引用前 sp1: " << sp1.use_count() << endl;
weak_ptr<int> wp(sp1);
cout << "被weak_ptr引用后 sp1: " << sp1.use_count() << endl;
}
(3)简单模拟实现
template <class T>
class weak_ptr
{
public:
//构造
weak_ptr()
:_ptr(nullptr){}
//用shared_ptr<T>构造
weak_ptr(const shared_ptr<T>& sp)
{
_ptr = sp.get();
_pcount = sp.getcount();
}
//拷贝构造
weak_ptr(const weak_ptr<T>& wp):_ptr(wp._ptr),_pcount(wp._pcount)
{
}
//赋值重载
weak_ptr<T>& operator=(const weak_ptr<T>& wp)
{
_ptr = wp._ptr;
_pcount = wp._pcount;
return *this;
}
//与shared_ptr<T>赋值重载
weak_ptr<T>& operator=(const shared_ptr<T>& sp)
{
_ptr = sp.get();
_pcount = sp.getcount();
return *this;
}
//取引用个数
int use_count() const
{
return *_pcount;
}
private:
//指针
T* _ptr;
//当前指针被引用的次数
const int* _pcount;
};
四、循环引用问题
循环引用会导致资源泄漏。
如:下面代码就会造成循环引用,导致资源泄漏
struct Node
{
int _val;
shared_ptr<Node> _next;
shared_ptr<Node> _prev;
Node(int val = 10) :_val(val) {}
};
void test_xu()
{
shared_ptr<Node> sp1(new Node);
shared_ptr<Node> sp2(new Node);
cout << "没互连 sp1:" << sp1.use_count() << endl;
cout << "没互连 sp2:" << sp2.use_count() << endl;
sp1->_next = sp2;
sp2->_prev = sp1;
cout << "互连后 sp1:" << sp1.use_count() << endl;
cout << "互连后 sp2:" << sp2.use_count() << endl;
}
分析:
解决办法:
使用节点的前驱和后继指针使用weak_ptr
智能指针就可以了,因为weak_ptr引用不会增加引用次数。
struct Node
{
int _val;
//shared_ptr<Node> _next;
//shared_ptr<Node> _prev;
weak_ptr<Node> _next;
weak_ptr<Node> _prev;
Node(int val = 10) :_val(val) {}
};
五、内存泄漏
1、 什么是内存泄漏,内存泄漏的危害
什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。 内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现 内存泄漏会导致响应越来越慢,最终卡死。
2 内存泄漏分类
C/C++程序中一般我们关心两种方面的内存泄漏:
堆内存泄漏(Heap leak): 堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一 块内存,用完后必须通过调用相应的free
或者delete
删掉。假设程序的设计错误导致这部分 内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。
系统资源泄漏: 指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放
掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。
3、如何避免内存泄漏
- 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps: 这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智 能指针来管理才有保证。
- 采用RAII思想或者智能指针来管理资源。
- 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
- 出问题了使用内存泄漏工具检测。ps:不过很多工具都不够靠谱,或者收费昂贵。 总结一下: 内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄 漏检测工具
4、使用智能指针解决内存泄漏的场景
int div()
{
int a, b;
cin >> a >> b;
if (b == 0) //抛异常
throw invalid_argument("除0错误");
return a / b;
}
void Func()
{
// 1、如果p1这里new 抛异常会如何?
// 2、如果p2这里new 抛异常会如何?
// 3、如果div调用这里又会抛异常会如何?
int* p1 = new int;
int* p2 = new int;
cout << div() << endl;
delete p1;
delete p2;
}
(1)如果
new p1
失败抛异常之后该函数结束跳转到上一个函数栈帧,p2
还没new
,所以没有内存泄漏。
(2)如果new p2
失败了抛异常之后该函数结束跳转到上一个函数栈帧,p1
已经new
出来了,p1
没来的及释放,这会导致内存泄漏。
(3) 如果div()
函数抛异常之后该函数结束就跳转到上一个函数栈帧了,就会导致p1
和p2
没来得及释放,这会导致内存泄漏。
使用智能指针后
shared_ptr<int> p1(new int);
shared_ptr<int> p2(new int);
(1)如果
new p2
失败抛异常之后该函数结束跳转到上一个函数栈帧,p1
智能指针生命周期结束,p1
智能指针会释放资源。
(2)如果div()
函数抛异常之后该函数结束就跳转到上一个函数栈帧了,p1、p2
智能指针生命周期结束,p1、p2
智能指针会释放资源。