智能指针使用场景分析
如果没有智能指针,按照传统的方式,开辟空间后,会在最后释放空间, 那么下面的这串代码,为了防止抛异常后,资源未被释放,就需要进行catch...捕获但如果有arrayn,n为无穷大,那么就要进行n-1次捕获,不仅代码繁琐,且如果少了一次,还会造成内存泄漏
cpp
double Divide(int a, int b)
{
if (b == 0)
{
throw "Divide by zero condition!";
}
return (double)a / (double)b;
}
void Func()
{
// 异常可能导致array1和array2内存泄漏
int* array1 = new int[10];
int* array2 = new int[10]; // 如果这里抛出异常,array1会泄漏
try
{
int len, time;
cin >> len >> time;
cout << Divide(len, time) << endl;
}
catch (...)
{
// 需要手动释放所有资源
cout << "delete []" << array1 << endl;
delete[] array1;
cout << "delete []" << array2 << endl;
delete[] array2;
throw;
}
// 正常路径也需要释放
cout << "delete []" << array1 << endl;
delete[] array1;
cout << "delete []" << array2 << endl;
delete[] array2;
}
解决方法
RAII的设计思想
RAII是Resource Acquisition Is Initialization的缩写,他是⼀种管理资源的类的设计思想,本质是
⼀种利⽤对象⽣命周期来管理获取到的动态资源,避免资源泄漏 RAII在获取资源时把资源委托给⼀个对象,接着控制对资源的访问, 资源在对象的⽣命周期内始终保持有效,最后在对象析构的时候释放资源,这样保障了资源的正常释放,避免资源泄漏问题
俗话:即将资源交给一个类来管理,在这个类生命结束时,会自动调用析构函数
cpp
template<class T>
class SmartPtr
{
public:
// RAII:构造函数获取资源
SmartPtr(T* ptr) : _ptr(ptr) {}
// RAII:析构函数释放资源
~SmartPtr()
{
cout << "delete[] " << _ptr << endl;
delete[] _ptr;
}
// 重载运算符,模拟指针行为
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
T& operator[](size_t i) { return _ptr[i]; }
private:
T* _ptr;
};
// 使用RAII智能指针
void Func()
{
SmartPtr<int> sp1 = new int[10];
SmartPtr<int> sp2 = new int[10];
for (size_t i = 0; i < 10; i++)
{
sp1[i] = sp2[i] = i;
}
int len, time;
cin >> len >> time;
cout << Divide(len, time) << endl;
// 无论是否发生异常,资源都会自动释放
}
C++标准库
C++标准库中的智能指针都在<memory>, 除了weak_ptr他们都符合RAII和像指针⼀样访问的⾏为,原理上⽽⾔主要是解 决智能指针拷⻉时的思路不同。
|------------|-------------------|---------------|---|
| auto_ptr | 拷贝时转移所有权 | 不推荐使用 | |
| unique_ptr | 独自占有所有权,不可拷贝,仅可移动 | 不需要拷贝的场景 | |
| shared_ptr | 共享所有权,支持拷贝,引用计数 | 需要拷贝,共享所有权的场景 | |
| weak_ptr | | | |
似乎shared_ptr比unique_ptr,但shared_ptr有额外空间,效率更低,且内存碎片泄漏的风险
解决内存碎片化的解决方法
shared_ptr 除了⽀持⽤指向资源的指针构造,还⽀持 make_shared ⽤初始化资源对象的值
直接构造。
虽然开辟的字节个数一样,但make_shared将原本开辟的两个空间,合并在一块,减少内存块
cpp
int main()
{
std::shared_ptr<Date> sp1(new Date(2024, 9, 11));
shared_ptr<Date> sp2 = make_shared<Date>(2024, 9, 11);
shared_ptr<Date> sp4;
return 0;
}
operator bool
cpp
int main()
{
std::shared_ptr<Date> sp1(new Date(2024, 9, 11));
shared_ptr<Date> sp2 = make_shared<Date>(2024, 9, 11);
shared_ptr<Date> sp4;
// if (sp1.operator bool())
if (sp1) //true
cout << "sp1 is not nullptr" << endl;
//if (!sp4)
if (!sp4.operator bool()) //false
cout << "sp4 is nullptr" << endl;
//shared_ptr<Date> sp5 = new Date(2024, 9, 11);
//unique_ptr<Date> sp6 = new Date(2024, 9, 11);
return 0;
}
智能指针的原理及实现
auto_ptr的思路是拷⻉时转移资源管理权给被拷⻉对象,这种思路是不被认可
的,也不建议使⽤。unique_ptr的思路是不⽀持拷⻉。
cpp
template<class T>
class unique_ptr {
unique_ptr(T*ptr)
:_ptr(ptr)
{}
~unique_ptr()
{
delete _ptr;
}
// 像指针一样使用
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
//拷贝与赋值不可使用
unique_ptr(const unique_ptr<T>&sp) = delete;
unique_ptr<T> operator=(const unique_ptr<T>&sp) = delete;//类型相同
unique_ptr(const unique_ptr<T>&& sp)
:_ptr(sp._ptr)
{
sp._ptr = nullptr;
}
unique_ptr<T>& operator=(unique_ptr<T>&& sp)
{
delete _ptr;//不释放,则旧空间就再也没有机会了,内存泄漏
_ptr = sp._ptr;
sp._ptr = nullptr;
}
private:
T* _ptr;
};
重点 shard_ptr的设计(引用计数设计)
这里是一份资源包含多个对象,每个对象都共用一个引用计数,一份资源需要一份引用计数,所以静态成员无法实现,只能在堆上进行开辟空间,new一个引用计数出来

cpp
template<class T>
class shared_ptr {
public:
shared_ptr(T*_ptr)
:_ptr(ptr)
,_pcount(new int(1))
//没有删除前,_del依旧走初始化列表,私有已经定好
{ }
template<class D>
shared_ptr(T* ptr, D* del)
: _ptr(ptr)
, _pcount(new int(1))
,_del(del)
{ }
shared_ptr(shared_ptr<T>&sp)
:_ptr(sp._ptr)
,_pcount(sp._pcount)
, _del(del)
{
++_pcount;
}
void release()
{
if (--(*_pcount) == 0)
{
_del(_ptr);//在外面释放_ptr
delete _pcound;
__pcount = _ptr = nullptr;
}
}
~shared_ptr()
{
release();
}
shared_ptr<T>& operator =(shared_ptr<T>& sp)
{
if (._ptr != sp._ptr)//防止为同一份资源进行赋值
{
if (--(*_pcount) == 0)
{
release();
}
_ptr = sp._ptr;
_pcount = sp._pcount;
++_pcount;
_del = sp._del;
}
return*this;
}
T* get() const
{
return _ptr;
}
int use_count() const
{
return *_pcount;
}
// 像指针一样使用
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
shared_ptr<>
private:
T* _ptr;
int* _pcount;
functional<void(T*)> _del = [](T* ptr) {delete ptr; };//lambda无返回值,传的参数为T*
};
删除器
智能指针析构时默认是进⾏delete释放资源,这也就意味着如果不是new出来的资源,交给智能指
针管理,析构时就会崩溃
因为new[]经常使⽤,所以为了简洁⼀点,
unique_ptr和shared_ptr都特化了⼀份[]的版本,使⽤时 unique_ptr<Date[]> up1(new Date[5]);shared_ptr<Date[]> sp1(new Date[5]); 就可以管理new []的资源。
cpp
template<class T>
void DeleteArrayFunc(T* ptr)
{
delete[] ptr;
}
int main()
{
std::shared_ptr<Date> sp1(new Date);
std::shared_ptr<Date[]> sp2(new Date[10]);
// 定制删除器 都可以,相对建议lambda
std::shared_ptr<Date> sp3(new Date[10], [](Date* ptr) {delete[] ptr; });
std::shared_ptr<Date> sp4(new Date[5], DeleteArrayFunc<Date>);
std::shared_ptr<FILE> sp5(fopen("Test.cpp", "r"), Fclose());
shared_ptr<FILE> sp6(fopen("Test.cpp", "r"), [](FILE* ptr) {
//cout << "fclose:" << ptr << endl;
fclose(ptr);
});
std::unique_ptr<Date> up1(new Date);
std::unique_ptr<Date[]> up2(new Date[10]);
// 定制删除器 建议仿函数
std::unique_ptr<FILE, Fclose> up3(fopen("Test.cpp", "r"));
auto fcloseFunc = [](FILE* ptr) {fclose(ptr); };
std::unique_ptr<FILE, decltype(fcloseFunc)> up4(fopen("Test.cpp", "r"), fcloseFunc);
return 0;
}
上面的代码仔细观察,外我们似乎可以发现 unique_ptr的删除器使用更为麻烦,是因为使用该类型的删除器要在模板参数进行声明
定制该类型的删除器,使用仿函数时,不用传两个参数,模板会自动推导
但这里使用lambda需要传两个模板参数,是因为lambda的类型无法自动推导,且模板参数还要使用decltype这个专属语法词
cpp
// 定制删除器 建议仿函数
std::unique_ptr<FILE, Fclose> up3(fopen("Test.cpp", "r"));
auto fcloseFunc = [](FILE* ptr) {fclose(ptr); };
std::unique_ptr<FILE, decltype(fcloseFunc)> up4(fopen("Test.cpp", "r"), fcloseFunc);
shared_ptr循环引⽤问题
shared_ptr在大部分情况下适用,但若遇到循环引用就会导致资源未被释放,内存泄漏,我们结合代码看下面两张图


cpp
struct ListNode
{
int _data;
/*ListNode* _next;
ListNode* _prev;*/
/*std::shared_ptr<ListNode> _next;
std::shared_ptr<ListNode> _prev;*/
//上面对ListNode的类型为shared_ptr,而不是原生指针ListNode*,是为了保持 _next _prev n1 n2类型一致,下面才可以进行赋值
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
// 循环引用 -- 内存泄露
std::shared_ptr<ListNode> n1(new ListNode);
std::shared_ptr<ListNode> n2(new ListNode);
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
n1->_next = n2;
n2->_prev = n1;
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
return 0;
}
上面的代码与图,n1和n2析构后,管理两个节点的引⽤计数减到1
- 右边的节点什么时候释放呢,左边节点中的_next管着呢,_next析构后,右边的节点就释放了。
- _next什么时候析构呢,_next是左边节点的的成员,左边节点释放,_next就析构了。 3. 左边节点什么时候释放呢,左边节点由右边节点中的_prev管着呢,_prev析构后,左边的节点就释
放了。 - _prev什么时候析构呢,_prev是右边节点的成员,右边节点释放,_prev就析构了。
⾄此逻辑上成功形成回旋镖似的循环引⽤,谁都不会释放就形成了循环引⽤,导致内存泄漏
解决方法:weak_ptr
weak_ptr不支持RAII,也不支持访问资源,但可以使用shared_ptr进行构造,绑定到shared_ptr时,不增加shared_ptr的引⽤计数,那么就可以解决上述的循环引⽤问题。

cpp
/ 这里改成weak_ptr,当n1->_next = n2;绑定shared_ptr时
// 不增加n2的引用计数,不参与资源释放的管理,就不会形成循环引用了
std::weak_ptr<ListNode> _next;
std::weak_ptr<ListNode> _prev;
weak_ptr
因为weak_ptr不参与资源管理,所以没有重载*和->,但如果绑定的shard_ptr已经过期了,weak_ptr再去访问就危险了
weak_ptr⽀持expired检查指向的资源是否过期,use_count也可获取shared_ptr的引⽤计数,weak_ptr想访问资源时,可以调⽤lock返回⼀个管理资源的shared_ptr,如果资源已经被释放,返回的shared_ptr是⼀个空对象,如果资源没有释放,则通过返回的shared_ptr访问资源是安全的。(调用lock,相当于自己又生成了一个shared_ptr)
expired 用于检测所管理的对象是否已经释放, 如果已经释放, 返回 true; 否则返回 false.
cpp
int main()
{
std::shared_ptr<string> sp1(new string("111111"));
std::shared_ptr<string> sp2(sp1);
std::weak_ptr<string> wp = sp1;
cout << wp.expired() << endl;//0
cout << wp.use_count() << endl;//2
// sp1和sp2都指向了其他资源,则weak_ptr就过期了
sp1 = make_shared<string>("222222");
cout << wp.expired() << endl;//0
cout << wp.use_count() << endl;//1
sp2 = make_shared<string>("333333");
cout << wp.expired() << endl;//1
cout << wp.use_count() << endl;//0
wp = sp1;
//std::shared_ptr<string> sp3 = wp.lock();
auto sp3 = wp.lock();
cout << wp.expired() << endl;//0
cout << wp.use_count() << endl;//2
sp1 = make_shared<string>("4444444");
cout << wp.expired() << endl;//0
cout << wp.use_count() << endl;//1
return 0;
}
内存泄漏
何为内存泄漏
内存泄漏指因为疏忽或错误造成程序未能释放已经不再使⽤的内存,⼀般是忘记释
放或者发⽣异常释放程序未能执⾏导致的
危害
⻓期运⾏的程序出现内存泄漏,影响很⼤,如操作系统、后台服
务、⻓时间运⾏的客⼾端等等,不断出现内存泄漏会导致可⽤内存不断变少,各种功能响应越来越
慢,最终卡死。
如何检查内存泄漏
Linux下检查:Linux下几款C++程序中的内存泄露检查工具_c++内存泄露工具分析-CSDN博客
windows使用第三方检查:windows下的内存泄露检测工具VLD使用_windows内存泄漏检测工具-CSDN博客
如何避免
养成良好的编码规范,申请的内存空间记着匹配的去释放。
尽量使用智能指针,如果不可,也要使用RAII思想
定期使用内存泄漏工具检测
总结
智能指针
RAII思想是C++智能指针的核心思想
unique_ptr适用于独占所有权,不进行拷贝的场景
shared_ptr使用于共享所有权的场景
weak_ptr用于解决循环引用问题(不引用计数)
内存泄漏
内存泄漏很有可能造成事故,要避免
规范代码的编写
使用智能指针
使用RAII思想
使用第三方工具检测