C++ —— 智能指针
1、内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存,⼀般是忘记释放或者发生异常释放程序未能执行导致的。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。(普通程序是不怕内存泄漏的,系统也是不怕内存泄漏的)int main()//申请一个1G未释放,这个程序多次运行也没啥危害//因为程序马上就结束,进程结束各种资源
目录
1. 智能指针的使用场景分析
下⾯程序中我们可以看到,new了以后,我们也delete了,但是因为抛异常导,后⾯的delete没有得到执行,所以就内存泄漏了,所以我们需要new以后捕获异常,捕获到异常后delete内存,再把异常抛出,但是因为new本⾝也可能抛异常,连续的两个new和下面的Divide都可能会抛异常,让我们处理起来很麻烦。智能指针放到这样的场景里面就让问题简单多了。
以下代码再使用上一篇文章的方法的时候:捕获异常,释放内存,再重新抛出异常的方式,此时就不太起作用了。
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没有得到释放。
//所以这里捕获异常后并不处理异常,异常还是交给外面处理,这里捕获了再重新抛出去。
//但是如果array2 new的时候抛异常呢,就还需要套一层捕获释放逻辑,这里更好解决方案
//是智能指针,否则代码太戳了
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;
}
是因为这段程序还有一个缺陷:new本身也会抛异常
- int* array1 = new int[10];
//抛异常,直接就到外层的main或者是处理异常的那部分中去了,无资源释放,因为都抛异常了
- cout << Divide(len, time) << endl;
//Divide抛异常也是没问题的
- int* array2 = new int[10];
// 抛异常,还要释放前面两个,一个是第一个new,一个是Divide,所以再套一层try catch,
去捕获第二个new抛异常,在套几个new呢?还要再套几层try catch吗?—— 所以引出智能指针的这样一个东西来解决此问题。
2. RAII和智能指针的设计思路
- RAII是Resource Acquisition Is Initialization(资源请求立即初始化)的缩写,他是⼀种管理资源的类的设计思想,本质是⼀种利用对象生命周期来管理获取到的动态资源,避免资源泄漏,这⾥的资源可以是内存、文件指针、网络连接、互斥锁等等。RAII在获取资源时把资源委托给⼀个对象,接着控制对资源的访问,资源在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源,这样保障了资源的正常释放,避免资源泄漏问题。
- 智能指针类除了满足RAII的设计思路,还要方便资源的访问,所以智能指针类还会想迭代器类⼀样,重载 operator*/operator->/operator[] 等运算符,方便访问资源。
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
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;
}
出了作用域自动调用析构:

SmartPtr<int> sp1 = new int[10]; //抛异常,sp1,sp2未被实例化,直接跳到抛异常的地方,没有资源要被释放
SmartPtr<int> sp2 = new int[10]; //抛异常,sp2未被实例化,但是sp1被实例化,抛异常,直接跳到抛异常的地方去,但是栈展开的时候,栈帧还是正常结束的,sp1会正常调用它的析构cout << Divide(len, time) << endl; //抛异常,前两个new是会正常析构的,因为资源都交给了智能指针,会自动释放,保存这个资源。本质就是构造的时候保存资源,出了作用域就会调用析构,析构就会释放这个资源
3. C++标准库智能指针的使用
- 智能指针在C++98的时候就已经写出来了。C++标准库中的智能指针都在<memory>这个头文件下面,我们包含<memory>就可以是使⽤了,智能指针有好几种,除了weak_ptr他们都符合RAII和像指针⼀样访问的行为,原理上而言主要是解决智能指针拷贝时的思路不同。(真正的核心问题是拷贝的问题)
SmartPtr<int> sp3(sp1); //用sp1去拷贝sp3,此时析构就会出事

因为我们没有写拷贝构造,默认就是浅拷贝,让sp1和sp3指向同一块资源,但是这里也不能深拷贝,因为这里和之前的容器不一样,之前的容器vector、list、map存储数据的空间就是属于容器本身的,拷贝这些容器就需要将这些数据和空间都重新拷贝一份,完成深拷贝,智能指针模拟的是指针的行为,sp1只是帮忙管理资源的,sp3(sp1)用一个指针拷贝另一个指针,希望两个指针共同管理同一块资源,共同管理的话就会发生析构多次的问题的出现。所以这一块:SmartPtr<int> sp3(sp1); 就是一个坑。所以智能指针就开启了它的发展历史:
- auto_ptr是C++98时设计出来的智能指针,他的特点是拷贝时把被拷贝对象的资源的管理权转移给拷贝对象,这是⼀个非常糟糕的设计,因为他会到被拷贝对象悬空,访问报错的问题,C++11设计出新的智能指针后,强烈建议不要使⽤auto_ptr。其它C++11出来之前很多公司也是明令禁止使⽤这个智能指针的。
(真正的问题:在解决拷贝的时候叫做自动指针,也就是自动释放,出了作用域自动释放,拷贝的设计非常糟糕,设计了一个管理权转移,非常糟糕)
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);
return 0;
}
运行结果:

以上代码析构是没有问题的,但是在将ap1拷贝给ap2时,会将ap1给置空,是有问题的:

不知道的人,还以为ap1依然存在,就去访问了,但是此时ap1已经不存在了:
int main()
{
//……
//语法上是支持的
ap1->_day++;
return 0;
}
此时就有大坑:

- unique_ptr是C++11设计出来的智能指针,他的名字翻译出来是唯⼀指针,他的特点的不支持拷贝,只支持移动。如果不需要拷贝的场景就非常建议使用他。
int main()
{
unique_ptr<Date> up1(new Date);
//不支持拷贝
//unique_ptr<Date> up2(up1);
//支持移动,但是移动后up1也悬空(主观动作),所以使用移动要谨慎
//unique_ptr<Date> up3(move(up1));
return 0;
}

- shared_ptr是C++11设计出来的智能指针,他的名字翻译出来是共享指针,他的特点是支持拷贝,也支持移动。如果需要拷贝的场景就需要使用他了。底层是用引用计数的方式实现的。
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;
}

问题:
直接用 shared_ptr 就好了,为什么还要用unique_ptr呢?
shared_ptr 也是有代价的,管理引用计数也是有一定的消耗和成本的,尤其是在多线程的环境下面,引用计数还会涉及到加锁之类的问题来保证引用计数的线程安全,也是付出了一定的代价的。如果是不用支持拷贝的话,用unique_ptr更高效一些。
对以上三个智能指针使用的总结:
不需要拷贝用unique_ptr,需要拷贝用shared_ptr,无论什么场景都不要用auto_ptr
- weak_ptr是C++11设计出来的智能指针,他的名字翻译出来是弱指针,他完全不同于上面的智能指针,他不支持RAII,也就意味着不能用它直接管理资源,weak_ptr的产生本质是要解决shared_ptr的⼀个循环引用导致内存泄漏的问题。具体细节下⾯我们再详细描述。
- 智能指针析构时默认是进行delete释放资源,这也就意味着如果不是new出来的资源,交给智能指针管理,析构时就会崩溃。智能指针支持在构造时给⼀个删除器,所谓删除器本质就是⼀个可调用对象(可调用对象是什么都行:参数指针、lambda、包装器,对于shared_ptr是在构造的时候给),这个可调用对象中实现你想要的释放资源的方式比,当构造智能指针时,给了定制的删除器,在智能指针析构时就会调⽤删除器去释放资源。因为new[]经常使用,所以为了简洁⼀点,unique_ptr和shared_ptr都特化了⼀份[]的版本,使用时unique_ptr<Date[]> up1(new Date[5]);shared_ptr<Date[]> sp1(new Date[5]); 就可以管理new []的资源。
- template <class T, class... Args> shared_ptr<T> make_shared
(Args&&... args);
make_shared和make_pair有点类似,是一个可以变模板参数:

int main()
{
//new一个日期类对象,给给shared_ptr。自己new
shared_ptr<Date> sp1(new Date(2026, 4, 13));
//构造日期类的参数直接传给make_shared,会返回一个shared_ptr。
//参数传给它,帮你在内部new,new出来的东西给给给智能指针
shared_ptr<Date> sp2 = make_shared<Date>(2026, 4, 13);
//简写的方式,make_shared返回的是shared_ptr
auto sp3 = make_shared<Date>(2026, 4, 13);
shared_ptr<Date> sp4;
return 0;
}
有人说,第二种写法:shared_ptr<Date> sp2 = make_shared<Date>(2026, 4, 13);更高效一点。例如下图:

按照我们的理解的话,上图中是分开的,new一份资源,再new一份引用计数,导致智能指针太多了的话,引用计数就是内存碎片,大量的去开辟小块内存,项目中大量的之智能指针,就有大量的引用计数,就是4个字节的小内存,就是大量内存碎片的问题,效率也有问题,大量的向系统申请小块内存,就会有效率问题,为了缓解这样的问题就有这样一些解决方案:
第一个就是允许在构造的时候传个内存池过去:(shared_ptr)![]()
第二个就是使用make_shared,就是将构造Date的参数给给我,会将两个空间开到一起,开个日期类对象是12字节,引用计数是4字节,直接将两个合到一起,开16字节,头部存4字节的引用计数
- shared_ptr 除了⽀持用指向资源的指针构造,还支持make_shared 用初始化资源对象的值直接构造。
- shared_ptr 和 unique_ptr 都支持了operator bool的类型转换(operator bool可以对类型进行重载,跟普通的运算符不一样,普通的运算符都是运算符,这个是可以让shared_ptr 强转成bool类型),如果智能指针对象是⼀个空对象没有管理资源,则返回false,否则返回true,意味着我们可以直接把智能指针对象给if判断是否为空。

如果想强转成int,就是operator int。
上述的 explicit operator bool() const noexcept; 是没有返回值的,因为bool就是它的返回值。为什么不是operator+运算符呢?因为运算符被占用了。
std::shared_ptr::operator bool 判断 shared_ptr 有没有管理资源,不传参数,构造的 shared_ptr 调用它的默认构造,默认构造管理的是空指针。
int main()
{
//new一个日期类对象,给给shared_ptr。自己new
shared_ptr<Date> sp1(new Date(2026, 4, 13));
//构造日期类的参数直接传给make_shared,会返回一个shared_ptr。
//参数传给它,帮你在内部new,new出来的东西给给给智能指针
shared_ptr<Date> sp2 = make_shared<Date>(2026, 4, 13);
//简写的方式,make_shared返回的是shared_ptr
auto sp3 = make_shared<Date>(2026, 4, 13);
shared_ptr<Date> sp4;
if(sp1.operator bool())
//if (sp1)
cout << "sp1 is not nullptr" << endl;
if (!sp4)
cout << "sp4 is nullptr" << endl;
return 0;
}

int main()
{
//报错,Date* 会生成一个临时对象,临时对象再去拷贝构造给给 sp5
//构造+拷贝构造 直接优化成直接构造,老编译有可能不优化
shared_ptr<Date> sp5 = new Date(2026, 4, 13);
unique_ptr<Date> sp6 = new Date(2026, 4, 13);
return 0;
}
基于这样的原因,不能这样写 (添加了explicit,就不允许隐式类型转换):

所以我们再写的时候也最好将explicit加上:
template<class T>
class shared_ptr
{
public:
explicit shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
,_pcount(new int(1))
{}
//……
}
- shared_ptr 和 unique_ptr 都得构造函数都使⽤explicit 修饰,防止普通指针隐式类型转换成智能指针对象。
4. 智能指针的原理
在C++98到C++11之间的时间中产生了一个特殊库:boost,不是C++11的标准库,boost是一个第三方的库,第三方库是由单独的源代码,下载下来根据使用方法来使用。C++11中的unique_ptr就是boost库中的scoped_ptr/scoped_array,C++11中的shared_ptr就是boost库中的shared_ptr/shared_array,C++11中的weak_ptr就是boost库中的weak_ptr,但是有些小细节改动了一下。同样右值引用的移动语义、bind、包装器也是boost中的,C++11吸收了。
4.1 auto_ptr 的实现:(了解)
// auto_ptr的实现
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;
};
4.2 unique_ptr的实现:
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;
};
4.3 shared_ptr的实现:
引用计数的实现不是那么容易实现的:
将_count变为静态成员:static int _count;确实是共享同一个,但是他是整个类的所有对象共享同一个,在以下的情况下就是不行的:
ysy::shared_ptr<Date> sp1(new Date);
ysy::shared_ptr<Date> sp2(sp1);
ysy::shared_ptr<Date> sp3(new Date);
上面的情况是需要有两个引用计数的,sp1和sp2共用一个,sp3单独一个,但是当我们将_count变量定义为static int _count;时,sp1、sp2、sp3共用这同一个_count变量。
那应该如何定义这个引用计数呢?
各自里面弄一个指针:int* _pcount,有一个指针指向资源,资源在堆上去new一个出来,有资源就配一个计数。然后各自指向这个计数。所有的资源都是通过构造来进行管理,构造的时候说明就来了一份资源。
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
,_pcount(new int(1))
{}
//拷贝构造
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
,_pcount(sp._pcount)
{
++(*_pcount);
}
~shared_ptr()
{
if (--(*_pcount) == 0)
{
delete _ptr;
delete _pcount;
}
}
//像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
//static int _count; //静态成员共享同一个,但是不行
int* _pcount;
};
}
运行结果:

两次析构没问题。
4.4 shared_ptr 赋值的实现:
//sp1 = sp3 (sp3为sp,sp1为this)
//赋值考虑的问题最多
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
return *this;
}
上述写的赋值的代码时最容易写错的,赋值的特点是两个已经存在的对象,直接让sp1指向sp3指向的资源的话,sp1之前的资源是不管了吗?如果sp1之前的资源只有sp1管理的话,那么这里就会有内存泄漏的问题,直接delete sp1之前的资源是不合理的。因为sp2也指向sp1原来指向的资源,直接delete sp1之前的资源的话,此时sp2指向的资源也被释放!!!sp2莫名其妙就指向野指针了!!!
//sp1 = sp3 (sp3为sp,sp1为this)
//赋值考虑的问题最多
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
delete _ptr; //直接释放sp1之前的资源是不合理的
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
return *this;
}
正确的写法:
template<class T>
class shared_ptr
{
public:
//……
//sp1 = sp3 (sp3为sp,sp1为this)
//赋值考虑的问题最多
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (--(*_pcount) == 0)
{
delete _ptr;
delete _pcount;
}
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
return *this;
}
~shared_ptr()
{
if (--(*_pcount) == 0)
{
delete _ptr;
delete _pcount;
}
}
//像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
//static int _count; //静态成员共享同一个,但是不行
int* _pcount;
};
}

int main()
{
ysy::shared_ptr<Date> sp1(new Date);
ysy::shared_ptr<Date> sp2(sp1);
ysy::shared_ptr<Date> sp3(new Date);
sp1 = sp3;
return 0;
}

int main()
{
ysy::shared_ptr<Date> sp1(new Date);
ysy::shared_ptr<Date> sp2(sp1);
ysy::shared_ptr<Date> sp3(new Date);
sp1 = sp3;
sp2 = sp3;
return 0;
}


此时sp1、sp2、sp3都去管同一块资源了,sp2原来指向的资源没有智能指针管了,会被析构。资源的释放不一定是析构的时候释放,也有可能在赋值的时候释放资源,因为那块资源没有指针管理了,总得释放。
还有一点比较坑的就是防止自己给自己赋值的情况:
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
,_pcount(new int(1))
{}
//拷贝构造
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
{
++(*_pcount);
}
void release()
{
if (--(*_pcount) == 0)
{
delete _ptr;
delete _pcount;
}
}
//sp1 = sp3 (sp3为sp,sp1为this)
//赋值考虑的问题最多
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();
}
//……
};
}
int main()
{
ysy::shared_ptr<Date> sp1(new Date);
ysy::shared_ptr<Date> sp2(sp1);
ysy::shared_ptr<Date> sp3(new Date);
sp1 = sp1;
sp1 = sp2;
sp1 = sp3;
sp2 = sp3;
return 0;
}


new[ ]申请的必须要用delete[ ]去释放,不然就是坑。
在boost库中的解决方案是单独解决了一个shared_array的东西,shared_array和shared_ptr析构的不同,一个是delete,一个是delete[],shared_array重点实现重载operator[ ]。第一种方法就是定制删除器。
shared_ptr和unique_ptr针对new []给出了最简单的实现方式
int main()
{
std::shared_ptr<Date[]> sp1(new Date[10]);
std::unique_ptr<Date[]> up1(new Date[10]);
return 0;
}
但是上面的方案多少具有局限性,malloc的就解决不了,还是给出了一种统一的解决方案:

shared_ptr是在构造的时候给,构造的时候自己去实例化。
定制删除器只要用到的是这个,在构造函数传:(传仿函数、函数指针都可以,自动推导)
![]()
int main()
{
//std::shared_ptr<Date[]> sp1(new Date[10]);
//std::unique_ptr<Date[]> up1(new Date[10]);
//定制删除器,可以是函数指针/仿函数/lambda
//将仿函数对象传给DeleteArray<Date>(),底层会将仿函数对象保存起来,释放资源的时候就会调仿函数对象来释放
shared_ptr<Date> sp2(new Date[5],DeleteArray<Date>());
//也可以传递函数指针给他,只是这里是模板的函数指针,对T进行实例化一下
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) { //lambda
cout << "fclose:" << ptr << endl;
fclose(ptr);
});
//DeleteArray<Date>传类型,传仿函数
unique_ptr<Date, DeleteArray<Date>> up2(new Date[5]);
//传函数指针做unique_ptr,两个地方都得传递,构造函数得传,这儿也得传
//函数指针定义的类型不能直接调用,是一个空的函数指针
unique_ptr<Date,void(*)(Date*)> up3(new Date[5],DeleteArrayFunc<Date>);
//传lambda也是,两个地方都得传递,decltype可以推导一个对象的类型
unique_ptr<Date, decltype(delArrOBJ)> up4(new Date[5], delArrOBJ);
return 0;
}
unique_ptr不是在构造的时候传自动去推导,而是在类模板实例化的时候传:

可不可以让我们自己写的shared_ptr也实现定制删除器呢?
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
,_pcount(new int(1))
{}
//定制删除器
//提供一个对应的构造,成员函数也可以是一个函数模板
template<class D>
shared_ptr(T* ptr,D del) //可能是函数指针、仿函数、lambda
:_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)
{
//delete _ptr;
_del(_ptr);//定制删除器,包装器去进行释放,调用对应的operator()
delete _pcount;
}
}
//sp1 = sp3 (sp3为sp,sp1为this)
//赋值考虑的问题最多
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();
}
//像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
//static int _count; //静态成员共享同一个,但是不行
int* _pcount;
function<void(T*)> _del; //包装器
};
}
//函数
template<class T>
void DeleteArrayFunc(T* ptr)
{
delete[] ptr;
}
//仿函数
template<class T>
class DeleteArray
{
public:
void operator()(T* ptr)
{
delete[] ptr;
}
};
//lambda
class Fclose
{
public:
void operator()(FILE* ptr)
{
cout << "fclose:" << ptr << endl;
fclose(ptr);
}
};
int main()
{
//std::shared_ptr<Date[]> sp1(new Date[10]);
//std::unique_ptr<Date[]> up1(new Date[10]);
//定制删除器,可以是函数指针/仿函数/lambda
//将仿函数对象传给DeleteArray<Date>(),底层会将仿函数对象保存起来,释放资源的时候就会调仿函数对象来释放
ysy::shared_ptr<Date> sp2(new Date[5],DeleteArray<Date>());
//也可以传递函数指针给他,只是这里是模板的函数指针,对T进行实例化一下
ysy::shared_ptr<Date> sp3(new Date[5], DeleteArrayFunc<Date>);
//也可以用一个lambda
auto delArrOBJ = [](Date* ptr) {delete[] ptr; };
ysy::shared_ptr<Date> sp4(new Date[5], delArrOBJ);
//实现其他资源管理的删除器
ysy::shared_ptr<FILE> sp5(fopen("Test.cpp", "r"), Fclose()); //传一个仿函数的匿名对象
ysy::shared_ptr<FILE> sp6(fopen("Test.cpp", "r"), [](FILE* ptr) { //lambda
cout << "fclose:" << ptr << endl;
fclose(ptr);
});
return 0;
}
运行结果:

此时不传删除器,好像又出问题了:
ysy::shared_ptr<Date> sp7(new Date);
不传删除器调用的是第一个:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
,_pcount(new int(1))
{}
function<void(T*)> _del;
这里默认是没有给值,没有给值就用默认值初始化:

里面就没有可调用对象,没有可调用对象调用它时此时就会抛异常,此时给个缺省值就好了,给一个默认的lambda就好了:
function<void(T*)> _del = [](T* ptr) {delete ptr;};
4.5 完整的代码,仅供参考:
#include<iostream>
#include<functional>
using namespace std;
// auto_ptr的实现
namespace ysy
{
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;
};
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;
};
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
,_pcount(new int(1))
{}
//定制删除器
//提供一个对应的构造,成员函数也可以是一个函数模板
template<class D>
shared_ptr(T* ptr,D del) //可能是函数指针、仿函数、lambda
:_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)
{
//delete _ptr;
_del(_ptr);//定制删除器,包装器去进行释放,调用对应的operator()
delete _pcount;
}
}
//sp1 = sp3 (sp3为sp,sp1为this)
//赋值考虑的问题最多
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();
}
//像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
//static int _count; //静态成员共享同一个,但是不行
int* _pcount;
function<void(T*)> _del = [](T* ptr) {delete ptr; }; //包装器
};
}
//函数
template<class T>
void DeleteArrayFunc(T* ptr)
{
delete[] ptr;
}
//仿函数
template<class T>
class DeleteArray
{
public:
void operator()(T* ptr)
{
delete[] ptr;
}
};
//lambda
class Fclose
{
public:
void operator()(FILE* ptr)
{
cout << "fclose:" << ptr << endl;
fclose(ptr);
}
};
int main()
{
//std::shared_ptr<Date[]> sp1(new Date[10]);
//std::unique_ptr<Date[]> up1(new Date[10]);
//定制删除器,可以是函数指针/仿函数/lambda
//将仿函数对象传给DeleteArray<Date>(),底层会将仿函数对象保存起来,释放资源的时候就会调仿函数对象来释放
ysy::shared_ptr<Date> sp2(new Date[5],DeleteArray<Date>());
//也可以传递函数指针给他,只是这里是模板的函数指针,对T进行实例化一下
ysy::shared_ptr<Date> sp3(new Date[5], DeleteArrayFunc<Date>);
//也可以用一个lambda
auto delArrOBJ = [](Date* ptr) {delete[] ptr; };
ysy::shared_ptr<Date> sp4(new Date[5], delArrOBJ);
//实现其他资源管理的删除器
ysy::shared_ptr<FILE> sp5(fopen("Test.cpp", "r"), Fclose()); //传一个仿函数的匿名对象
ysy::shared_ptr<FILE> sp6(fopen("Test.cpp", "r"), [](FILE* ptr) { //lambda
cout << "fclose:" << ptr << endl;
fclose(ptr);
});
ysy::shared_ptr<Date> sp7(new Date);
return 0;
}
5. shared_ptr 和 weak_ptr
5.1 shared_ptr循环引用问题
shared_ptr 相比 unique_ptr 好用是好用,但是就是有两大缺陷:
1. shared_ptr循环引用问题 2.shared_ptr的线程安全问题
以下代码就会产生循环引用问题,循环引用问题就会导致内存泄露问题:
struct ListNode
{
int _data;
//ListNode* _next;
//ListNode* _prev;
//将_next变为智能指针,将_prev变为智能指针
std::shared_ptr<ListNode> _next;
std::shared_ptr<ListNode> _prev;
// 这里改成weak_ptr,当n1->_next = n2;绑定shared_ptr时
// 不增加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; //_next原生指针,n2是智能指针对象,那么就将_next变为智能指针
n2->_prev = n1; //_prev原生指针,n1是智能指针对象,那么就将_prev变为智能指针
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
// weak_ptr
// weak_ptr
//std::weak_ptr<ListNode> wp(new ListNode);
return 0;
}
将_next变为智能指针,将_prev变为智能指针,此时就坑了,刚开始的引用计数都是1,怎么都变为了2:

分析过程:



总结一下:
什么样的场景会导致上面的循环引用呢?
两个对象,两个对象里面,各自有一个shared_ptr的智能指针指向对方,只要形成这样的局面就必然会形成循环引用。
如何解决呢?
最初点是因为是原生的指针不是智能指针给不过去,用shared_ptr会形成循环引用,引入weak_ptr来解决这个问题。weak_ptr不遵循RAII
5.2 weak_ptr
- weak_ptr不支持RAII,也不⽀持访问资源,所以我们看文档发现weak_ptr构造时不支持绑定到资源,只支持绑定到shared_ptr,绑定到shared_ptr时,不增加shared_ptr的引⽤计数,那么就可以解决上述的循环引用问题。

struct ListNode
{
int _data;
//ListNode* _next;
//ListNode* _prev;
//将_next变为智能指针,将_prev变为智能指针
//std::shared_ptr<ListNode> _next;
//std::shared_ptr<ListNode> _prev;
// 这里改成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);
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
n1->_next = n2; //_next原生指针,n2是智能指针对象,那么就将_next变为智能指针
n2->_prev = n1; //_prev原生指针,n1是智能指针对象,那么就将_prev变为智能指针
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
// weak_ptr
// weak_ptr
//std::weak_ptr<ListNode> wp(new ListNode);
return 0;
}
weak_ptr最大的特点就是不会增加引用计数,不参与资源的释放管理,还是会互相指向,但是不增加引用计数,就不会形成循环引用,也不会有坑。
运行结果:
weak_ptr的实现:
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;
};
weak_ptr是不支持解引用的,只是单纯的指向。operator*,operator->都没有重载

template<class T>
class shared_ptr
{
public:
//……
int use_count()const
{
return *_pcount;
}
T* get()const
{
return _ptr;
}
private:
T* _ptr;
//static int _count; //静态成员共享同一个,但是不行
int* _pcount;
function<void(T*)> _del = [](T* ptr) {delete ptr; }; //包装器
};
struct ListNode
{
int _data;
ysy::weak_ptr<ListNode> _next;
ysy::weak_ptr<ListNode> _prev;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
// 循环引用 —— 内存泄漏
ysy::shared_ptr<ListNode> n1(new ListNode);
ysy::shared_ptr<ListNode> n2(new ListNode);
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
n1->_next = n2; //_next原生指针,n2是智能指针对象,那么就将_next变为智能指针
n2->_prev = n1; //_prev原生指针,n1是智能指针对象,那么就将_prev变为智能指针
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
// weak_ptr
// weak_ptr
//std::weak_ptr<ListNode> wp(new ListNode);
return 0;
}
运行结果:

库里面实现的 shared_ptr 和 weak_ptr的实现是比我们实现的复杂不少,因为在我们自己实现的方法中weak_ptr是不支持引用计数的,但是在库中也是有引用计数的。
- weak_ptr也没有重载operator*和operator->等,因为他不参与资源管理,那么如果他绑定的shared_ptr已经释放了资源,那么他去访问资源就是很危险的 (有可能指向的是野指针) 。weak_ptr⽀持expired检查指向的资源是否过期,use_count也可获取shared_ptr的引用计数,weak_ptr想访问资源时,可以调用lock返回⼀个管理资源的shared_ptr,如果资源已经被释放,返回的shared_ptr是⼀个空对象,如果资源没有释放,则通过返回的shared_ptr访问资源是安全的。

std::weak_ptr::expired 判断是否过期,如果过期了就返回true,没有过期就返回false

能拿到std::weak_ptr::use_count,不增加引用计数,但是是可以拿到你的引用计数的,说明远远不是刚刚我们自己实现的那么简单
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;
return 0;
}
运行结果:

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>("33333");
cout << wp.expired() << endl;
cout << wp.use_count() << endl;
return 0;
}
资源释放不一定是生命周期到了,也有可能是因为被赋值了

此时再去访问的话就有坑,因为已经成为野指针了,如果真的要去访问的话,应该这样去访问,用lock函数,参与管理,但不是自己参与管理,而是增加一个shared_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>("33333");
cout << wp.expired() << endl;
cout << wp.use_count() << endl;
//再去访问的话就有坑,因为已经成为野指针了
//真的要去访问的话,应该这样去访问,用lock函数
wp = sp1;
auto sp3 = wp.lock();
cout << wp.expired() << endl;
cout << wp.use_count() << endl;
sp1 = sp2;
return 0;
}
运行结果:

从上面的与运行结果中说明引用计数是不能释放的,里面就要求一个方式指向一个引用计数,boost库中是由四五个类来实现的,不增加引用计数,但是要知道现在的引用计数是多少。
weak_ptr想访问这个资源,不是直接访问的,而是lock一下产生另外一个智能指针,跟你共同管理之后,用sp3这个智能指针去修改,sp1也会被修改
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>("33333");
cout << wp.expired() << endl;
cout << wp.use_count() << endl;
//再去访问的话就有坑,因为已经成为野指针了
//真的要去访问的话,应该这样去访问,用lock函数
wp = sp1;
auto sp3 = wp.lock();
cout << wp.expired() << endl;
cout << wp.use_count() << endl;
*sp3 += "###";
cout << *sp1 << endl;
sp1 = sp2;
return 0;
}
运行结果:
![]()
6.shared_ptr的线程安全问题
两个智能指针指向同一块资源,它们放在两个线程里面,这个时候就会有线程安全的问题,两个线程同时对引用计数++、--,此时的引用计数就有线程安全的问题,保证引用计数的线程安全有两种方案:
1.加锁
2.原子操作
int main()
{
ysy::shared_ptr<AA> p(new AA);
const size_t n = 100000;
mutex mtx;
auto func = [&]()
{
for (size_t i = 0; i < n; ++i)
{
// 这里智能指针拷贝会++计数
ysy::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;
}

此时,对shared_ptr就需要对引用计数进行加锁,但是这种方式太重
此时还有一种简单的方法:将int* _pcount变为atomic<int>* _pcount,就是线程安全的了,atomic是用单独的用cs实现的类,类去访问的时候去调用它的operator ++和--,它是线程安全的
template<class T>
class shared_ptr
{
public:
//……
private:
T* _ptr;
//int* _pcount;
atomic<int>* _pcount;
function<void(T*)> _del = [](T* ptr) {delete ptr; }; //包装器
};

智能指针保证它里面的引用计数是线程安全的,但是他指向的资源不是线程安全的,多个线程进行++的时候,要进行加锁,加锁的方式如下图所示:

7. C++11和boost中智能指针的关系
- Boost库是为C++语⾔标准库提供扩展的⼀些C++程序库的总称,Boost社区建⽴的初衷之⼀就是为C++的标准化⼯作提供可供参考的实现,Boost社区的发起⼈Dawes本⼈就是C++标准委员会的成员之⼀。在Boost库的开发中,Boost社区也在这个⽅向上取得了丰硕的成果,C++11及之后的新语法和库有很多都是从Boost中来的。
- C++ 98 中产生了第⼀个智能指针auto_ptr。
- C++ boost给出了更实用的scoped_ptr/scoped_array和shared_ptr/shared_array和weak_ptr等。
- C++ TR1,引入了shared_ptr等,不过注意的是TR1并不是标准版。
- C++ 11,引入了unique_ptr和shared_ptr和weak_ptr。需要注意的是unique_ptr对应boost的scoped_ptr。并且这些智能指针的实现原理是参考boost中的实现的。
总结:
智能指针最主要的要去解决的问题就是异常安全,解决C++里面的内存泄漏,所以在项目里为了更好的防止内存泄露,可以大量的使用智能指针,不拷贝使用unique_ptr,要拷贝使用shared_ptr,shared_ptr稍微要注意的是循环计数引用,只要不出现循环引用的场景就不会出现内存泄漏
8. 内存泄露
8.1 什么是内存泄漏,内存泄漏的危害
1、什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存,⼀般是忘记释放或者发生异常释放程序未能执行导致的。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。(普通程序是不怕内存泄漏的,系统也是不怕内存泄漏的)
int main()
{
//申请一个1G未释放,这个程序多次运行也没啥危害
//因为程序马上就结束,进程结束各种资源也就回收了
char* ptr = new char[1024 * 1024 * 1024];
cout << (void*)ptr << endl;
return 0;
}
运行结果:
是能申请成功的。多次运行的话也是会不断的申请成功,但是并没有进行一次的释放。如果一个程序正常的结束,没有释放的内存最终也是会释放的。进程正常结束就算你没释放,他也是会帮你释放的。内存泄漏的危害到底是什么?第一,进程不正常结束(僵尸进程);第二,有很多的服务(例如某团、某滴之类的)是长期进行的,只有在对某个软件进行升级的时候才会关闭,不在正常公告里面的时间点关闭的话就是事故。
2、内存泄漏的危害:普通程序运行⼀会就结束了出现内存泄漏问题也不大,进程正常结束,页表的映射关系解除,物理内存也可以释放。长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服
务、长时间运行的客户端等等,不断出现内存泄漏会导致可用内存不断变少,各种功能响应越来越慢,最终卡死。
8.2 如何检测内存泄漏(了解)
linux下内存泄漏检测:linux的检测工具
windows下使⽤第三方工具:windows下的内存泄露检测工具—VLD
8.3 如何避免内存泄漏
- 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps:这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下⼀条智能指针来管理才有保证。
- 尽量使用智能指针来管理资源,如果自己场景比较特殊,采用RAII思想自己造个轮子管理。
- 定期使⽤内存泄漏工具检测,尤其是每次项目快上线前,不过有些⼯具不够靠谱,或者是收费。
- 总结⼀下:内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工具。
更多推荐


所有评论(0)