目录
前言
接着【C++】异常详情请点击查看,今天继续学习【C++】C++11----智能指针
一、智能指针的使用场景
- 在异常部分,我们介绍了异常安全问题,可能因为抛异常导致内存泄漏问题,智能指针就能很好的解决这个问题
- 下面的代码,我们在Func函数中new了两个数组,通过异常的学习,我们可以在Func的catch函数中,释放两个数组空间之后再将异常抛出,这样就不会因为数组空间资源没有释放而造成内存泄漏问题
- 但是还有一个问题,new本身也可能会抛异常,这样我们就还得在new外层套一个try,catch来捕获异常
cpp
double Divide(int a, int b)
{
// 当b == 0时抛出异常
if (b == 0)
{
throw "Divide by zero condition!";
}
else
{
return (double)a / (double)b;
}
}
void Func()
{
int* array1 = new int[10];
int* array2 = new int[10]; //new抛异常
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[] 等运算符,方便访问资源
- 下面代码中,我们创建了一个SmartPtr智能指针类,成员变量是一个指针。这样在Func函数中,直接用SmartPtr类new数组,这样在函数结束时,new的两个数组生命周期结束,自动调用SmartPtr的析构函数释放资源,避免内存泄漏。
- 当sp1对象new的时候,new抛异常,sp1还没有申请成功,异常会直接抛出,不会造成内存泄漏;sp2对象new的时候同理
cpp
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;
}

三、C++标准库智能指针的使用
- C++标准库中的智能指针都在< memory>这个头文件下面,我们包含< memory>就可以是使用了,智能指针有好几种,除了weak_ptr他们都符合RAII和像指针一样访问的行为,原理上而言主要是解决智能指针拷贝时的思路不同
- auto_ptr是C++98时设计出来的智能指针,他的特点是拷贝时把被拷贝对象的资源的管理权转移给拷贝对象,但是它有一个问题,他会被拷贝对象悬空,访问报错的问题,C++11设计出新的智能指针后,强烈建议不要使用auto_ptr。其他C++11出来之前很多公司也是明令禁止使用这个智能指针
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;
}

- unique_ptr是C++11设计出来的智能指针,他的名字翻译出来是唯⼀指针,他的特点是不支持拷贝,只支持移动。如果不需要拷贝的场景就非常建议使用他
cpp
unique_ptr<Date> up1(new Date);
// 不⽀持拷⻉
//unique_ptr<Date> up2(up1);
// ⽀持移动,但是移动后up1也悬空,所以使⽤移动要谨慎
unique_ptr<Date> up3(move(up1));
- shared_ptr是C++11设计出来的智能指针,他的名字翻译出来是共享指针,他的特点是支持拷贝,也支持移动 。如果需要拷贝的场景就需要使用他了。底层是用
引用计数方式实现的
cpp
shared_ptr<Date> sp1(new Date);
// ⽀持拷⻉
shared_ptr<Date> sp2(sp1);
shared_ptr<Date> sp3(sp2);
cout << sp1.use_count() << endl; //计数,指向这块空间的指针个数,当为0时,释放空间
sp1->_year++;
cout << sp1->_year << endl;
cout << sp2->_year << endl;
cout << sp3->_year << endl;
// ⽀持移动,但是移动后sp1也悬空,所以使⽤移动要谨慎
shared_ptr<Date> sp4(move(sp1));

- weak_ptr是C++11设计出来的智能指针,他的名字翻译出来是弱指针,他完全不同于上面的智能指针,他不支持RAII,也就意味着不能用它直接管理资源,weak_ptr的产生本质是要解决shared_ptr的一个循环引用导致内存泄漏的问题
- shared_ptr 除了支持用指向资源的指针构造,还支持 make_shared 用初始化资源对象的值直接构造
template <class T, class... Args> shared_ptr<T> make_shared (Args&&... args);

cpp
int main()
{
shared_ptr<Date> sp1(new Date(2024, 9, 11));
shared_ptr<Date> sp2 = make_shared<Date>(2024, 9, 11);
return 0;
}

- shared_ptr 和 unique_ptr 都支持了operator bool的类型转换,如果智能指针对象是一个空对象没有管理资源,则返回false,否则返回true,意味着我们可以直接把智能指针对象给if判断是否为空


cpp
if (sp1)
cout << "sp1 is not nullptr" << endl;
if (!sp4)
cout << "sp4 is nullptr" << endl;
- shared_ptr 和 unique_ptr 的构造函数都使用explicit修饰,普通指针隐式类型转换成智能指针对象
四、智能指针的原理
- auto_ptr的思路是拷贝时转移资源管理权给被拷贝对象,这种思路是不被认可的,因此我们在这里不做演示,不建议使用
unique_ptr
- unique_ptr的思路是只支持移动(右值的移动构造和移动赋值),不支持拷贝(拷贝构造和拷贝赋值)
unique_ptr(const unique_ptr<T>&sp) = delete;unique_ptr<T>& operator=(const unique_ptr<T>&sp) = delete;
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;
};
shared_ptr
- shared_ptr中引用计数如何实现?一份资源就需要一个引用计数,所以如果引用计数使用静态成员的方式无法实现一份资源一个引用计数。
- 使用堆上动态开辟的方式 ,构造智能指针对象时说明有了一个资源,就要new一个引用计数出来(初始为1)。多个shared_ptr指向资源时就++引用计数,shared_ptr对象析构时就--引用计数,引用计数减到0时,代表当前析构的shared_ptr是最后一个管理资源的对象,则析构资源

cpp
namespace gy
{
template<class T>
class shared_ptr
{
public:
//默认构造
explicit shared_ptr(T* ptr = nullptr)
: _ptr(ptr)
, _pcount(new int(1))
{}
//sp1(sp2)拷贝构造
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
{
++(*_pcount);
}
//析构
~shared_ptr()
{
if (--(*_pcount) == 0)
{
// 最后⼀个管理的对象,释放资源
delete _ptr;
delete _pcount;
_ptr = nullptr;
_pcount = nullptr;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
int* _pcount;
};
}
int main()
{
gy::shared_ptr<Date> sp1(new Date);
gy::shared_ptr<Date> sp2(sp1);
gy::shared_ptr<Date> sp3(new Date);
return 0;
}

赋值运算符重载
- shared_ptr的赋值运算符重载我们需要注意,如下图所示:我们需要判断sp1指向资源是否需要释放问题,再将sp1指向sp3资源
- 同时赋值可能会自己给自己赋值的情况:1. sp1 = sp1 2. sp1 = sp2(同一个对象赋值;对象不同但是指向同一个资源)

cpp
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr != sp._ptr)
{
if (--(*_pcount) == 0)
{
delete _ptr;
delete _pcount;
}
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
}
return *this;
}
- 析构函数中也有类似逻辑,因此我们使用release函数来封装一下
cpp
void release()
{
if (--(*_pcount) == 0)
{
delete _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);
}
return *this;
}
~shared_ptr()
{
release();
}
get
- 获取私有成员变量_ptr
cpp
T* get()const
{
return _ptr;
}
实现定制删除器
- 智能指针析构时默认是进行delete释放资源 ,这也就意味着如果不是new出来的资源,交给智能指针管理,析构时就会崩溃。智能指针支持在构造时给一个
删除器,所谓删除器本质就是一个可调用对象,这个可调用对象中实现你想要的释放资源的方式,当构造智能指针时,给了定制的删除器,在智能指针析构时就会调用删除器去释放资源。因为new[]经常使用,所以为了简洁一点,unique_ptr和shared_ptr都特化了一份[]的版本,使用时 unique_ptr<Date[]> up1(new Date[5]);shared_ptr<Date[]> sp1(new Date[5]); 就可以管理new[]的资源 - unique_ptr和shared_ptr支持删除器的方式有所不同:unique_ptr是在类模板参数支持的,shared_ptr是构造函数参数支持的,使用仿函数unique_ptr可以不在构造函数传递,因为仿函数类型构造的对象直接就可以调用,但是函数指针和lambda的类型不可以
cpp
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()
{
//特化版本
shared_ptr<Date[]> sp1(new Date[5]);
//仿函数对象做删除器
shared_ptr<Date> sp2(new Date[5], DeleteArray<Date>());
//函数指针做删除器
shared_ptr<Date> sp3(new Date[5], DeleteArrayFunc<Date>);
//lambda表达式做删除器
auto delArrOBJ = [](Date* ptr) {delete[] ptr; };
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);
});
unique_ptr<Date, DeleteArray<Date>> up2(new Date[5]);
unique_ptr<Date, void(*)(Date*)> up3(new Date[5], DeleteArrayFunc<Date>);
unique_ptr<Date, decltype(delArrOBJ)> up4(new Date[5], delArrOBJ);
return 0;
}
- 实现shared_ptr的删除器,在构造函数参数中传入删除器
- 删除器我们定义在成员变量中,使用包装器function实现,因为删除器都是返回类型都是void类型,传入的都是T*的数据,因此我们使用function实现这些函数的包装
- 然后在变量声明部分将其初始化为
function<void(T*)> _del = [](T* ptr) {delete ptr;};,因为我们可能并不会传入一个删除器(比如是使用new创建出来的对象,只需要使用delete释放即可),因此我们需要将包装器初始化为delete释放空间,这样默认直接delete释放,传入删除器,则调用删除器(delete[])的释放空间的操作
cpp
namespace gy
{
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)
{
++(*_pcount);
}
void release()
{
if (--(*_pcount) == 0)
{
_del(_ptr);
delete _pcount;
_ptr = nullptr;
_pcount = nullptr;
}
}
//sp1 = sp3
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr != sp._ptr)
{
release();
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
}
return *this;
}
~shared_ptr()
{
release();
}
private:
T* _ptr;
int* _pcount;
function<void(T*)> _del = [](T* ptr) {delete ptr;};
};
}
cpp
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()
{
gy::shared_ptr<Date> sp2(new Date[5], DeleteArray<Date>());
//函数指针做删除器
gy::shared_ptr<Date> sp3(new Date[5], DeleteArrayFunc<Date>);
//lambda对象做删除器
auto delArrOBJ = [](Date* ptr) {delete[] ptr; };
gy::shared_ptr<Date> sp4(new Date[5], delArrOBJ);
//实现其他资源管理的删除器
gy::shared_ptr<FILE> sp5(fopen("Test.cpp", "r"), Fclose());
gy::shared_ptr<FILE> sp6(fopen("Test.cpp", "r"), [](FILE* ptr) {
cout << "fclose:" << ptr << endl;
fclose(ptr);
});
return 0;
}

- 默认析构方式
cpp
int main()
{
gy::shared_ptr<Date> sp1(new Date);
return 0;
}
weak_ptr
shared_ptr循环引用问题
- hared_ptr大多数情况下管理资源非常合适,支持RAII,也支持拷贝。但是在循环引用的场景下会导致资源没得到释放内存泄漏
- ListNode节点中,包含数据、_prev、_next,当我们创建两个节点,并且将一个节点的_next指针指向另一个节点,另一个节点的_prev指针指向第一个节点,这样会造成循环引用

cpp
struct ListNode
{
int _data;
shared_ptr<ListNode> _next;
shared_ptr<ListNode> _prev;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
// 循环引⽤ -- 内存泄露
shared_ptr<ListNode> n1(new ListNode);
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;
}
引用计数都是2,没有释放
- 现在n1和n2的_prev共同管理节点1,n1的_next和n2共同管理节点2,所以引用计数都是2
- 当n1和n2析构的时候,引用计数都减到1,节点并没释放。第一个节点的_next还指向着第二个节点,第二个节点的_prev还指向着第一个节点
- 对于上面由shared_ptr引起的循环引用,我们可以使用weak_ptr来解决这个问题。
- weak_ptr不支持RAII,也不支持访问资源,weak_ptr构造时不支持绑定到资源,只支持绑定到shared_ptr,绑定到shared_ptr时,不增加shared_ptr的引用计数,那么就可以解决上述的循环引用问题

- 在ListNode节点的结构体中,改用weak_ptr,当n1->_next = n2,绑定shared_ptr时,不增加n2的引用计数,不参与资源释放的管理,就不会形成循环引用了
cpp
struct ListNode
{
int _data;
std::weak_ptr<ListNode> _next;
std::weak_ptr<ListNode> _prev;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
shared_ptr<ListNode> n1(new ListNode);
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;
}

- weak_ptr也没有重载operator*和operator->等,因为他不参与资源管理,那么如果他绑定的shared_ptr已经释放了资源,那么他去访问资源就是很危险的。weak_ptr支持expired检查指向的资源是否过期(0表示没有过期),use_count也可获取shared_ptr的引用计数 ,weak_ptr想访问资源时,可以调用lock返回一个管理资源的shared_ptr,如果资源已经被释放,返回的shared_ptr是一个空对象,如果资源没有释放,则通过返回的shared_ptr访问资源是安全的(返回的shared_ptr指向该资源共同管理,引用计数+1)
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;
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;
}

五、内存泄漏
内存泄漏概念
- 内存泄漏:指因为疏忽或错误造成程序未能释放已经不再使用的内存,一般是忘记释放或者发生异常释放程序未能执行导致的。内存泄漏并不是指内存在物理上的消失,而是应用程分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费
内存泄漏的危害
- 一个进程马上结束,这个new资源没有被释放的情况,并不会造成很严重的危害,因为进程结束,进程的各种资源就回收了
- 长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务、长时间运行的客户端等等,不断出现内存泄漏会导致可用内存不断变少,各种功能响应越来越慢,最终卡死
如何检测内存泄漏
linux下内存泄漏检测:Linux下几款检测工具
windows下使用第三方工具:Windows下工具
如何避免内存泄漏
- 养成良好的编码规范,申请的内存空间记着匹配的去释放
- 尽量使用智能指针来管理资源,如果自己场景特殊,采用RAII思想自己造个轮子管理
- 定期使用内存泄漏工具检测,不过有些工具不够靠谱,或者是收费

