智能指针:C++内存管理的利器
本文摘要: C++智能指针是解决内存泄漏问题的利器,主要通过RAII(资源获取即初始化)设计思想实现资源管理。标准库提供了三种智能指针:unique_ptr(独占所有权,禁止拷贝)、shared_ptr(共享所有权,引用计数)和weak_ptr(解决循环引用问题)。文章详细介绍了智能指针的使用方法,包括如何管理数组资源、定制删除器、以及make_shared的用法。特别强调了智能指针在异常安全中的
·
智能指针的使用场景
- new 了之后,也 delete 了,但是因为抛出异常导致后面的 delete 没有执行,所以就导致内存泄露了,我们需要 new 以后捕获异常,捕获到异常后 delete 内存,再把异常抛出,但是因为 new 本身也能抛出异常,连续的两个 new 和下面的Divide 都会抛出异常,处理起来很麻烦
double Divide(int a, int b)
{
// 当b == 0时抛出异常
if (b == 0)
{
throw "Division by zero condition!";
}
return (double)a / (double)b;
}
void Func()
{
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 << "Unkown Exception" << endl;
}
return 0;
}
RAII和智能指针的设计思路
- RAII 是一种管理资源的类的设计思想
- RAII 在获取资源时把资源委托给一个对象,接着控制对资源的访问,资源在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源,这样保障了资源的正常释放,避免资源泄漏问题
- 智能指针除了满足 RAII 的设计思路,还要方便资源的访问,所以还要重载 operator*/operator->/operator[] 运算符
- 普通的指针 int* p1 = new int(10); int* p2 = p1; p1和p2指向同一块内存,是浅拷贝,会导致重复析构;智能指针就是解决这个问题的
C++标准库智能指针的使用
- 在==<memory>==这个头文件下
- C++98的 auto_ptr 强烈不推荐使用,就不写了
- C++11:
- unique_ptr,唯一指针,特点是不支持拷贝,只支持移动,如果不需要拷贝的场景就建议使用它
- shared_ptr,共享指针,支持拷贝,也支持移动,如果需要拷贝的场景就需要使用它。底层是用引用技术的方式实现的
- weak_ptr,弱指针,不同于上面的智能指针,它不支持 RAII。也就是说不能用它直接管理资源,weak_ptr 的产生本质是要解决 shared_ptr 的循环引用导致的内存泄漏问题
- unique_ptr 和 shared_ptr 移动后会悬空
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()
{
unique_ptr<Date> up1(new Date);
//不支持拷贝
//unique_ptr<Date> up2(up1); //错误写法
//支持移动,但是移动后 up1 为空,所以使用移动要谨慎
unique_ptr<Date> up3(move(up1));
shared_ptr<Date> sp1(new Date);
//支持拷贝
shared_ptr<Date> sp2(sp1);
shared_ptr<Date> sp3(sp1);
cout << sp1.use_count() << endl; //获取当前指向同一对象的 shared_ptr 数量(引用计数)
//sp1\sp2\sp3指向同一对象
sp1->_year++;
cout << sp1->_year << endl; //2
cout << sp2->_year << endl; //2
cout << sp3->_year << endl; //2
//支持移动,但是移动后sp1悬空,所以移动要谨慎
shared_ptr<Date> sp4(move(sp1));
return 0;
}
- 智能指针析构时默认是进行 delete 释放资源。如果不是 new 出来的资源,交给智能指针管理,析构时就回崩溃。
- 智能指针支持在构造时给一个删除器,实现想要的释放资源的方式
- 因为new[]经常使⽤,所以 unique_ptr 和 shared_ptr 都特化了⼀份[]的版本,使用时 unique_ptr<Date[]> up1(new Date[5]); shared_ptr<Date[]> sp1(new Date[5]); 就可以管理new[]的资源,不需要传定制的删除器
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和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>());
//解决方案3
//函数指针做删除器
unique_ptr<Date, void(*)(Date*)> up3(new Date[5], DeleteArrayFunc<Date>); //在类的声明传
shared_ptr<Date> sp3(new Date[5], DeleteArrayFunc<Date>); //在构造函数传
//解决方案4,最推荐
//lambda表达式做删除器
//unique_ptr定制删除器推荐用仿函数.shared_per都可以,相对建议lambda
auto delArrOBJ = [](Date* ptr) {delete[] ptr; };
unique_ptr<Date, decltype(delArrOBJ)> up4(new Date[5], delArrOBJ); //decltype推导()中表达式类型
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;
}
- shared_ptr 除了支持用指向资源的指针构造,还支持 make_shared 用初始化资源对象的值直接构造
- shared_ptr 和 unique_ptr 都支持了 operator bool 的类型转换,如果智能指针对象是一个空对象没有管理资源,则返回 false,否则返回true。可以直接把智能指针对象给 if 判断是否为空
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)
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;
}
智能指针原理
- unique_ptr 的思路不支持拷贝
- shared_ptr 采用引用计数的设计,因为这里一份资源就需要一个引用计数,所以引用计数采用静态成员的方式是无法实现的,要使用堆上动态开辟的方式,构造智能指针对象时来一份资源,就要 new 一个引用计数出来
- 多个 shared_ptr 指向资源时就 ++引用计数,shared_ptr 对象析构时就 --引用计数,引用计数减到0时代表当前析构的 shared_ptr时最后一个管理资源的对象,则析构资源
namespace ssp
{
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;
return *this;
}
private:
T* _ptr;
};
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; } //定制删除器
};
}
shared_ptr和weak_ptr
shared_ptr循环引用问题
- 在循环引用的场景下会导致资源没得到释放,导致内存泄漏,使用weak_ptr解决这种问题
- 引用计数的值是有多少个指针指向它,prev指向它也会使计数+1

- 他们两个互相指向对方,想要析构必须等对方先析构,导致了循环引用问题
- 把_next和_prev改成weak_ptr,weak_ptr绑定到shared_ptr时不会增加它的引用计数,_next和_prev不参与资源释放管理逻辑,就成功打破了循环引用

weak_ptr
- weak_ptr不支持RAII,也不支持访问资源,只支持绑定到shared_ptr,且不增加shared_ptr的引用计数
- weak_ptr支持expired检查指向的资源是否过期,use_count也可获取shared_ptr的引用计数
- weak_ptr想访问资源时,可以调用lock返回⼀个管理资源的shared_ptr,如果资源已经被释放,返回的shared_ptr是⼀个空对象,如果资源没有释放,则通过返回的shared_ptr访问资源是安全的
int main()
{
std::shared_ptr<string> sp1(new string("11111"));
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>("22222");
cout << wp.expired() << endl;
cout << wp.use_count() << endl;
sp2 = make_shared<string>("22222");
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;
}
shared_ptr的线程安全问题
- shared_ptr本身是线程安全的,但是指向的对象有线程安全的问题
- 如果多个shared_ptr对象在多个线程中,进行shared_ptr的拷贝析构时会访问修改引⽤计数,就会存在线程安全问题,所以shared_ptr引⽤计数是需要加锁或者 原⼦操作保证线程安全的
- bit::shared_ptr引⽤计数从int*\改成==atomic<int>*==就可以保证引⽤计数的线程安全问题,或者使⽤互斥锁加锁也可以
C++11和boost中智能指针的关系
- Boost库是为C++语言标准库提供扩展的⼀些C++程序库的总称
- C++boost给出了更实用的scoped_ptr/scoped_array和shared_ptr/shared_array和weak_ptr等
内存泄漏
什么是内存泄漏
- 内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存,⼀般是忘记释放或者发生异常释放程序未能执行导致的
内存泄露的危害
- 普通程序运行⼀会就结束了出现内存泄漏问题也不⼤,进程正常结束,页表的映射关系解除,物理内存也可以释放。
- 长期运行的程序出现内存泄漏,影响很大。不断出现内存泄漏会导致可用内存不断变少,各种功能响应越来越 慢,最终卡死
如何避免内存泄漏
- 1、事前预防型。如智能指针等。
- 2、事后查错型。如泄漏检测⼯具
更多推荐

所有评论(0)