智能指针
目录
一、智能指针的使用场景
new完对象后,也使用了delete
但是因为抛异常,后面的delete没有执行,导致内存泄漏
需要new完对象后捕获异常,捕获到异常后delete内存,再把异常抛出
但因为new本身也可能抛异常
cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
#include <string>
#include <exception>
double Divide(int a, int b)
{
// 当b == 0时抛出异常
if (b == 0)
{
throw "Divide by zero condition!";
}
else
{
return (double)a / (double)b;
}
}
void Func()
{
//如果发生除0错误抛出异常,另外下面的array和array2没有得到释放
//这里捕获异常后并不处理异常,异常还是交给外面处理,这里捕获了再重新抛出去
//但是如果array2new的时候抛异常呢,就还需要套一层捕获释放逻辑,这里更好解决方案是智能指针
int* array1 = new int[10];
int* array2 = new int[10];
try
{
int len, time;
cin >> len >> time;
cout << Divide(len, time) << endl;
}
catch (...)
{
cout << "delete []" << array1 << endl;
cout << "delete []" << array2 << endl;
delete[] array1;
delete[] array2;
throw; //异常重新抛出,捕获到什么抛出什么
}
//...
cout << "delete []" << array1 << endl;
delete[] array1;
cout << "delete []" << array2 << endl;
delete[] array2;
}
int main()
{
try
{
Func();
}
catch (const char* errmsg)
{
cout << errmsg << endl;
}
catch (const exception& e)
{
cout << e.what() << endl;
}
catch (...)
{
cout << "未知异常" << endl;
}
return 0;
}
二、RAII和智能指针
**RAII(Resource Acquisition Is Initialization):**资源获取立即初始化
一种管理资源的类的设计思想
**本质:**利用对象的生命周期来管理获取到的动态资源,避免资源泄露
(注:这里的资源指的是内存、文件指针、网络链接、互斥锁...)
RAII在获取资源是把资源委托给一个对象,控制对资源的访问
资源在对象的生命周期内始终保持有效,在对象析构时被释放
既保障了资源的正常释放、又可以避免资源泄露问题
**智能指针:**一个类模板,用来实现RAII的设计思路,方便资源访问
(注:智能指针类像迭代器一样,重载operator*/operator->/operator[]等运算符)
cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
#include <string>
#include <exception>
template<class T>
class SmartPtr
{
public:
//RAII
SmartPtr(T* ptr)
:_ptr(ptr)
{
}
~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;
};
double Divide(int a, int b)
{
// 当b == 0时抛出异常
if (b == 0)
{
throw "Divide by zero condition!";
}
else
{
return (double)a / (double)b;
}
}
void Func()
{
//这里使用RAII的智能指针类管理new出来的数组以后,程序简单多了
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;
}
int main()
{
try
{
Func();
}
catch (const char* errmsg)
{
cout << errmsg << endl;
}
catch (const exception& e)
{
cout << e.what() << endl;
}
catch (...)
{
cout << "未知异常" << endl;
}
return 0;
}
三、智能指针的问题
cpp
int main()
{
//需要sp1和sp2共同管理资源,浅拷贝
//但是析构多次的问题需要被解决
SmartPtr<int> sp1 = new int[10];
SmartPtr<int> sp2(sp1);
return 0;
}
四、C++标准库的智能指针
C++标准库中的智能指针都在<memory>头文件
智能指针有好多种类型,除了weak_ptr,他们都符合RAII
原理上而言主要是解决智能指针拷贝时的思路不同

4.1.auto_ptr
C++98时设计出来的智能指针
拷贝时把被拷贝对象资源的管理权转移给拷贝对象
导致被拷贝的对象悬空,发生访问报错的问题,不建议使用
cpp
struct Date
{
int _year;
int _month;
int _day;
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
,_month(month)
,_day(day)
{}
~Date()
{
cout << "~Date()" << endl;
}
};
int main()
{
auto_ptr<Date> ap1(new Date);
//拷贝时,管理权限转移,被拷贝对象ap1悬空
auto_ptr<Date> ap2(ap1);
//空指针访问:ap1对象已经悬空
//ap1->_year++;
return 0;
}
4.2.唯一指针(unique_ptr)
C++11时设计出来的智能指针
不支持拷贝,只支持移动
建议在不需要拷贝的场景使用
cpp
int main()
{
unique_ptr<Date> up1(new Date);
//不支持拷⻉
//unique_ptr<Date> up2(up1);
//支持移动,但是移动后up1也悬空,所以使用移动要谨慎
unique_ptr<Date> up3(move(up1));
return 0;
}
4.3.共享指针(shared_ptr)
C++11时设计出来的智能指针
支持拷贝,也支持移动
可以在需要拷贝的场景时使用,底层用引用计数实现
cpp
int main()
{
shared_ptr<Date> sp1(new Date);
//支持拷贝
shared_ptr<Date> sp2(sp1);
shared_ptr<Date> sp3(sp2);
cout << sp1.use_count() << endl;
sp1->_year++;
cout << sp1->_year << endl;
cout << sp2->_year << endl;
cout << sp3->_year << endl;
//支持移动
//但是移动后sp1也悬空,所以使用移动要谨慎
shared_ptr<Date> sp4(move(sp1));
return 0;
}
4.4.弱指针(weak_ptr)
C++11时设计出来的智能指针
不支持RAII ,无法用它直接管理资源,解决share_ptr的循环引用缺陷
4.5.删除器
智能指针在析构时默认进行delete释放资源
如果不是new出来的资源,交给智能指针管理,析构时会崩溃
智能指针在构造时给一个删除器
**删除器:**一个可调用的对象,在对象中实现你想要的释放资源方式
cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
struct Date
{
int _year;
int _month;
int _day;
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
,_month(month)
,_day(day)
{}
~Date()
{
cout << "~Date()" << endl;
}
};
template < class T>
void DeleteArrayFunc(T * ptr)
{
delete[] ptr;
}
template < class T>
class DeleteArray
{
public:
void operator()(T * ptr)
{
delete[] ptr;
}
};
class Fclose
{
public:
void operator()(FILE * ptr)
{
cout << "fclose:" << ptr << endl;
fclose(ptr);
}
};
int main()
{
//这样实现程序会崩溃
unique_ptr<Date> up1(new Date[10]);
shared_ptr<Date> sp1(new Date[10]);
//解决方案1:特化
//因为new[]经常使用,所以unique_ptr和shared_ptr
//实现了一个特化版本,这个特化版本析构时用的delete[]
unique_ptr<Date[]> up1(new Date[5]);
shared_ptr<Date[]> sp1(new Date[5]);
//解决方案2:删除器
//仿函数对象做删除器
//unique_ptr<Date, DeleteArray<Date>> up2(new Date[5], DeleteArray<Date>());
//unique_ptr和shared_ptr支持删除器的方有所不同
//unique_ptr是在类模板参数支持的
//shared_ptr是构造函数参数支持的
//使用仿函数unique_ptr可以不在构造函数传递
//因为仿函数类型构造的对象直接就可以调用
//但是下面的函数指针和lambda的类型不可以
unique_ptr<Date, DeleteArray<Date>> up2(new Date[5]);
shared_ptr<Date> sp2(new Date[5], DeleteArray<Date>());
//函数指针做删除器
unique_ptr<Date, void(*)(Date*)> up3(new Date[5], DeleteArrayFunc<Date>);
shared_ptr<Date> sp3(new Date[5], DeleteArrayFunc<Date>);
//lambda表达式做删除器
auto delArrOBJ = [](Date* ptr) {delete[] ptr;};
unique_ptr<Date, decltype(delArrOBJ)> up4(new Date[5], delArrOBJ);
shared_ptr<Date> sp4(new Date[5], delArrOBJ);
//实现其他资源管理的删除器
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);});
return 0;
}
4.6.make_share
cpp
int main()
{
shared_ptr<Date> sp1(new Date(2024, 9, 11));
shared_ptr<Date> sp2 = make_shared<Date>(2024, 9, 11);
auto sp3 = make_shared<Date>(2024, 9, 11);
shared_ptr<Date> sp4;
// if (sp1.operator bool())
if (sp1)
cout << "sp1 is not nullptr" << endl;
if (!sp4)
cout << "sp1 is nullptr" << endl;
// 报错
shared_ptr<Date> sp5 = new Date(2024, 9, 11);
unique_ptr<Date> sp6 = new Date(2024, 9, 11);
return 0;
}
五、智能指针的原理
5.1.auto_ptr原理
拷贝时转移资源管理权给被拷贝的对象
cpp
template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr)
:_ptr(ptr)
{}
auto_ptr(auto_ptr<T>& sp)
:_ptr(sp._ptr)
{
//管理权转移
sp._ptr = nullptr;
}
auto_ptr<T>& operator=(auto_ptr<T>& ap)
{
//检测是否为自己给自己赋值
if (this != &ap)
{
//释放当前对象中资源
if (_ptr)
{
delete _ptr;
}
//转移ap中资源到当前对象中
_ptr = ap._ptr;
ap._ptr = NULL;
}
return *this;
}
~auto_ptr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
//像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
5.2.unique_ptr原理
不支持拷贝,只支持移动
cpp
template <class T>
class unique_ptr
{
public:
explicit unique_ptr(T * ptr)
:_ptr(ptr)
{}
~unique_ptr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
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(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;
};
5.3.share_ptr原理
设计引用计数,一份资源就需要一个引用计数
所以引用计数用静态成员的方式是无法实现的
要使用堆上动态开辟的方式
构造智能指针对象时来一份资源,就要new一个引用计数
增加share_ptr指向资源就++引用计数
当shared_ptr对象析构时就--引用计数
引用计数减到0时代表当前析构的share_ptr是最后一个管理资源的对象,则析构资源

cpp
#define _CRT_SECURE_NO_WARNINGS 1
template < class T>
class shared_ptr
{
public:
explicit shared_ptr(T * ptr = nullptr)
: _ptr(ptr)
,_pcount(new int(1))
{}
template<class D>
shared_ptr(T * ptr, D del)
:_ptr(ptr)
,_pcount(new int(1))
,_del(del)
{}
shared_ptr(const shared_ptr<T>&sp)
:_ptr(sp._ptr)
,_pcount(sp._pcount)
,_del(sp._del)
{
++(*_pcount);
}
void release()
{
if (--(*_pcount) == 0)
{
// 最后一个管理的对象,释放资源
_del(_ptr);
delete _pcount;
_ptr = nullptr;
_pcount = nullptr;
}
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr != sp._ptr)
{
release();
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
_del = sp._del;
}
return *this;
}
~shared_ptr()
{
release();
}
T * get() const
{
return _ptr;
}
int use_count() const
{
return *_pcount;
}
T & operator*()
{
return *_ptr;
}
T * operator->()
{
return _ptr;
}
private:
T * _ptr;
int* _pcount;
function<void(T*)> _del = [](T* ptr) {delete ptr;};
};
5.4.weak_ptr原理
weak_ptr不支持RAII,也不支持访问资源
绑定到share_ptr时,不增加share_ptr的引用计数,可以解决循环引用问题
cpp
template < class T>
class weak_ptr
{
public:
weak_ptr()
{}
weak_ptr(const shared_ptr<T>&sp)
: _ptr(sp.get())
{}
weak_ptr<T>&operator=(const shared_ptr<T>&sp)
{
_ptr = sp.get();
return *this;
}
private:
T* _ptr = nullptr;
};
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;
cout << wp.use_count() << endl;
// sp1和sp2都指向了其他资源,则weak_ptr就过期了
sp1 = make_shared<string>("222222");
cout << wp.expired() << endl;
cout << wp.use_count() << endl;
sp2 = make_shared<string>("333333");
cout << wp.expired() << endl;
cout << wp.use_count() << endl;
wp = sp1;
//std::shared_ptr<string> sp3 = wp.lock();
auto sp3 = wp.lock();
cout << wp.expired() << endl;
cout << wp.use_count() << endl;
*sp3 += "###";
cout << *sp1 << endl;
return 0;
}
六、share_ptr循环引用问题
cpp
struct ListNode
{
int _data;
std::shared_ptr<ListNode> _next;
std::shared_ptr<ListNode> _prev;
~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;
return 0;
}

右边的节点什么时候释放
左边节点中的_next管理右边节点,_next析构后,右边的节点释放
_next什么时候析构
_next是左边节点的成员,左边节点释放后,_next析构
左边的节点什么时候释放
右边节点中的_prev管理左边节点,_prev析构后,左边的节点释放
_prev什么时候析构
_prev是右边节点的成员,右边节点释放后,_prev析构
以上逻辑形成了循环引用,导致内存泄漏问题
weak_ptr可以解决这个问题
cpp
struct ListNode
{
int _data;
//这里改成weak_ptr,当n1->_next = n2,绑定shared_ptr时
//不增加n2的引⽤计数,不参与资源释放的管理,就不会形成循环引用
std::weak_ptr<ListNode> _next;
std::weak_ptr<ListNode> _prev;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
std::shared_ptr<ListNode> n1(new ListNode);
std::shared_ptr<ListNode> n2(new ListNode);
n1->_next = n2;
n2->_prev = n1;
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
//weak_ptr不支持管理资源,不支持RAII
//weak_ptr是专⻔绑定shared_ptr,不增加引用计数,作为⼀些场景的辅助管理
std::weak_ptr<ListNode> wp(new ListNode);
return 0;
}
七、share_ptr的线程安全问题
share_ptr的引用计数对象在堆上,如果多个shared_ptr对象在多个线程中
进行share_ptr的拷贝析构时会访问修改引用计数,就会存在线程安全问题
所以shared_ptr引用计数是需要加锁或者原子操作保证线程安全
share_ptr指向的对象也是有线程安全问题的,但这个对象的线程安全不归share_ptr管,也管不了
下面的程序会崩溃,或者A资源没释放
bit::share_ptr引用计数从int*改成atomic<int>*就可以保证引用计数的线程安全问题
cpp
struct AA
{
int _a1 = 0;
int _a2 = 0;
~AA()
{
cout << "~AA()" << endl;
}
};
int main()
{
bit::shared_ptr<AA> p(new AA);
const size_t n = 100000;
mutex mtx;
auto func = [&]()
{
for (size_t i = 0; i < n; ++i)
{
//这里智能指针拷贝会++计数
bit::shared_ptr<AA> copy(p);
{
unique_lock<mutex> lk(mtx);
copy->_a1++;
copy->_a2++;
}
}
};
thread t1(func);
thread t2(func);
t1.join();
t2.join();
cout << p->_a1 << endl;
cout << p->_a2 << endl;
cout << p.use_count() << endl;
return 0;
}
八、内存泄漏
8.1.内存泄漏的概念
因为疏忽或错误造成程序未能释放已经不再使用的内存
一般是忘记释放或者发生异常释放程序未能执行导致
并不是指内存在物理上的消失
而是应用程序分配某段内存后
因为设计错误,失去对该段内存的控制,造成内存浪费
8.2.内存泄漏的危害
普通程序运行一会就结束,出现内存泄漏问题不大
进程正常结束,页表的映射关系解除,物理内存也可以释放
长期运行的程序出现内存泄漏,影响很大
如操作系统、后台服务、长期运行的客户端等
不断出现内存泄漏会导致可用内存不断变少,各种功能响应越来越慢,最终卡死
cpp
int main()
{
//申请⼀个1G未释放,这个程序多次运行也没啥危害
//因为程序马上就结束,进程结束各种资源也就回收了
char* ptr = new char[1024 * 1024 * 1024];
cout << (void*)ptr << endl;
return 0;
}
8.3.内存泄漏的避免
申请的内存空间记得匹配的去释放
用智能指针管理资源,采用RAII思想自己造轮子处理
定期使用内存泄漏工具检测