智能指针三部曲:unique·shared·weak 的三角恋
目录
一、智能指针的引入
什么是内存泄露
内存泄漏分类
内存泄露的危害
内存泄露的情况
二、智能指针是什么
三、auto_ptr
四、unique_ptr
六、循环引用
七、weak_ptr
八、总结
一、智能指针的引入
为什么有智能指针?
智能指针是为了来避免内存泄露的问题
什么是内存泄露
内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内
存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费
内存泄漏分类
- 堆内存泄漏(Heap leak)
堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一
块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分
内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。
- 系统资源泄漏
指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定
内存泄露的危害
长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死
内存泄露的情况
void func()
{}void f()
{pair<string, string>* p1 = new pair<string, string>;func();//如果func()中抛异常,影响执行流,就有可能没有去走 delete p1 这一步delete p1;
}int main()
{f();return 0;
}
- 如果func()中抛异常,影响执行流,就有可能没有去走 delete p1 这一步
- 此时就没有释放p1的内存空间,就导致内存泄露
我们也许会想到抛异常的方法来解决这个问题
void f()
{pair<string, string>* p1 = new pair<string, string>;func();//如果func()中抛异常,影响执行流,就有可能没有去走 delete p1 这一步//delete p1;try{div();}catch (...){delete p1;cout << "delete: " << p1 << endl;throw;}delete p1;
}
如果多几个呢?
void func()
{}void f()
{pair<string, string>* p1 = new pair<string, string>;func();//如果func()中抛异常,影响执行流,就有可能没有去走 delete p1 这一步//delete p1;pair<string, string>* p1 = new pair<string, string>;//如果多几个呢pair<string, string>* p2 = new pair<string, string>;pair<string, string>* p3 = new pair<string, string>;//如果p3的new抛异常,要释放p1和p2,div抛异常要释放p1,p2,p3,太麻烦了try{div();}catch (...){delete p1;cout << "delete: " << p1 << endl;throw;}delete p1;
}
- 如果p3的new抛异常,要释放p1和p2,div抛异常要释放p1,p2,p3,太麻烦了
接下来我们来看智能指针是什么
二、智能指针是什么
智能指针 : 把一个类封装一个指针,利用类自动调用析构函数这个特性,在析构函数中释放指针
这样不管怎么样,指针指向的空间都能得到释放 ,用这个类去代替指针 ,为了能很好的代替指针 ,
这个类要实现模拟一个指针的各种功能的函数
智能指针(ps)
- 感觉就是利用了类,不管是函数结束,还是出了作用域,抛异常 这个对象之后自动调用析构函数这个特性,
- 把指针封装到一个类里面,然后用类代替这个指针的各种行为(还不是为利用类的这个特性)但是一个类不能完全代替指针,所以要实现各种重载函数去应对使用指针的各种场景,
- 智能指针就相当于利用类自动调用析构函数的特性,不管怎么样,指针都能得到释放
class SmartPtr//现在是只能传pair<string,string> 的指针,把智能指针改成模板的形式就可以传各种类型的值,让编译器去实例化出函数
{
public:SmartPtr(pair<string, string>* ptr):_ptr(ptr){ }~SmartPtr(){delete _ptr;}
private:pair<string, string>* _ptr;
};void f()
{pair<string, string>* p1 = new pair<string, string>;SmartPtr sp1(p1);//无论是这个函数结束,还是出了作用域,抛异常,这个对象都会调用它的析构函数释放指针//换个写法SmartPtr sp1(new pair<string, string>);//sp1就是是"指针"// 不管是什么场景下 ,都能解放我们的双手,让我们不用手动的去调用析构函数//现在是只能传pair<string,string> 的指针,把智能指针改成模板的形式就可以传各种类型的值,让编译器去实例化出函数div();//不管抛不抛异常,p1都能得到释放}
- 无论是这个函数结束,还是出了作用域,抛异常,这个对象都会调用它的析构函数
- 不管是什么场景下 ,都能解放我们的双手,让我们不用手动的去调用析构函数
- 现在是只能传pair<string,string> 的指针,把智能指针改成模板的形式就可以传各种类型的值,让编译器去实例化出函数
template<class T>
class SmartPtr
{
public://RAII //1.RAII管控资源释放//2.像指针一样//3.拷贝问题SmartPtr(T* ptr):_ptr(ptr){ }~SmartPtr(){delete _ptr;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;
};
把智能指针这种做法叫做RAII: 借助一个对象的生命周期,来控制资源的释放
在对象的生命周期内,资源有效,对象生命周期结束,自动释放资源
智能指针面临的三个问题 :
- RAII管控资源释放
- 像指针一样
- 拷贝问题
关键是第三个问题 ,如何做到拷贝
int main()
{SmartPtr<string> sp1(new string("xxxxxx"));SmartPtr<string> sp2(new string("yyyyyy"));sp1 =sp2;return 0;
}
- 这样会直接出错的
由于我们没有主动实现拷贝函数 ,类里面会实现拷贝函数 ,类里面的拷贝函数是浅拷贝, sp1存储sp2的指针,导致sp1的空间没有去释放,而sp2的空间被释放了两次
三、auto_ptr
这里我们就要实现一下智能指针,我们来自己实现一下各种智能指针,由于库里面的智能指针跟我们会冲突,所以说我们要放在自己的命名空间namespace txf 里
提醒,构造是用指针构造 类 里的指针 ,拷贝是同类型的拷贝 ,用类去拷贝类要返回类
template<class T>
class auto_ptr
{
public://RAII 所有智能指针的特性:// //1.RAII管控资源释放//2.像指针一样//3.拷贝问题auto_ptr(T* ptr):_ptr(ptr){}~auto_ptr(){cout << "delete:" << _ptr << endl;delete _ptr;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}//ap3(ap1)auto_ptr(auto_ptr<T>& ap):_ptr(ap._ptr){ap._ptr = nullptr;//这里一定要置空,如果不置空的话,就会有两个对象指向同一块空间,//那么在对象销毁的时候,会对同一块空间释放两次,程序就会崩溃}
private:T* _ptr;
};
- ap._ptr = nullptr 这里一定要置空,如果不置空的话,就会有两个对象指向同一块空间 , 那么在对象销毁的时候,会对同一块空间释放两次,程序就会崩溃
auto_ptr会产生一个问题 ,场景如下:
class A
{
public:A(int a = 0):_a(a){cout << "A(int a=0)" << endl;}~A(){cout << this;cout << "~A" << endl;}int _a;
};int main()
{ //ap1就是A类型的指针了txf::auto_ptr<A> ap1(new A(1));//只用给类型txf::auto_ptr<A> ap2(new A(2));//如果我们去拷贝一下呢txf::auto_ptr<A> ap3(ap1);ap1->_a++;直接导致崩溃return 0;
}
- ap1->_a++;直接导致崩溃,因为ap1这个指针置空了,但是如果别人不知道还去用它去访问,就会直接导致崩溃
- 管理权转移ap1直接为空了
拷贝时会把被拷贝对象的资源管理权转移给拷贝对象,导致被拷贝对象悬空
ap1->_a++;直接导致崩溃
所以拷贝完之后不能再用被拷贝对象,这也是ap1的缺陷 ,但这不符合指针啊
四、unique_ptr
template<class T>
class unique_ptr
{
public://RAII 所有智能指针的特性:// //1.RAII管控资源释放//2.像指针一样//3.拷贝问题unique_ptr(T* ptr):_ptr(ptr){}~unique_ptr(){cout << "delete:" << _ptr << endl;delete _ptr;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}//怎样不让拷贝//1,只声明不实现,并且声明成私有
private://way1:unique_ptr(unique_ptr<T>& up);//way2:// unique_ptr(unique_ptr<T>& up)=delete;//还要把赋值封一下unique_ptr<T>& operator=(unique_ptr<T>& up) = delete;//如果不封的话,系统会自动生成 ,系统生成的是浅拷贝,浅拷贝会造成两个问题:// 一个问题是,两个对象都调用析构函数,对同一块空间析构两次,// 第二个问题是有一个对象的空间会被泄露掉private:T* _ptr;
};
-
怎样不让拷贝
-
方法一 : 只声明不实现,并且声明成私有
private: unique_ptr(unique_ptr<T>& up); -
方法二 : unique_ptr(unique_ptr<T>& up)=delete;
这一行显式地把 unique_ptr 的“拷贝构造”函数删掉了。
关键词 = delete 是 C++11 的新语法,它告诉编译器:
“这个函数存在,但任何人胆敢调用它,就直接报错!”
-
还要把赋值封一下
unique_ptr<T>& operator=(unique_ptr<T>& up) = delete;
如果不封的话,系统会自动生成 ,系统生成的是浅拷贝,浅拷贝会造成两个问题:
- 一个问题是,两个对象都调用析构函数,对同一块空间析构两次,
- 第二个问题是有一个对象的空间会被泄露掉
相当于他是直接把赋值和拷贝直接给砍掉了,它适用于不发生赋值和拷贝的场景
五、shared_ptr
这是用途最广的智能指针,它能够实现拷贝和构造
template<class T>class shared_ptr{public://RAII 所有智能指针的特性:// //1.RAII管控资源释放//2.像指针一样//3.拷贝问题shared_ptr(T* ptr):_ptr(ptr),_pcount(new int(1)){}~shared_ptr(){cout << "delete:" << _ptr << endl;delete _ptr;delete _pcount;}T* get()const {return _ptr;}int use_count() const{return *_pcount;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}//sp3(sp1)shared_ptr(shared_ptr<T>& sp):_ptr(sp._ptr),_pcount(sp._pcount){(*_pcount)++;}//参考sp1=sp5的情况shared_ptr<T>& operator=(shared_ptr<T>& sp){//但是不能自己传,自己自己传自己的话,就会先调用析构函数,然后再赋值,这样你空间都没了if (_ptr == sp._ptr)return *this;if (--(*_pcount) == 0){delete _ptr;delete _pcount;}_ptr = sp._ptr;_pcount = sp._pcount;(*_pcount)++;}private:T* _ptr;int* _pcount;//int _pcount 如果是这样,每个对象的_pcount都是独立的};
- 为了解决,auto_ptr的问题,他在这里引入了一个计数的int* _pcount
- 我们思考一下,能用int count吗,不能这样,这样每个对象之间都是独立的,sp3的_pcount--不影响sp1的,这样是不对的,指向同一个空间的指针的_pcount应该是相通的
- 那能用static静态成员变量吗 , 答案是不行 ,static静态成员变量这个成员变量是类实例化出所有对象所共享的,我们的期望是一个资源伴随着一个_pcount
- 这样当有两个对象指向同一个空间的时候,_pcount标为二,当其中一个对象的析构时候,_pcount减一,只有当_pcount减为零的时候,那个空间才会被释放掉,所以说当一个对象析构的时候,那个空间是不会被释放掉的,因为还有一个指针指向他
int main()
{txf::shared_ptr<A> sp1(new A(1));//new int; 分配了一个int大小的内存,并返回指向这块内存的指针txf::shared_ptr<A> sp2(new A(2));txf::shared_ptr<A> sp3(sp1);sp1->_a++;sp3->_a++;txf::shared_ptr<A> sp4(sp2);txf::shared_ptr<A> sp5(sp4);return 0;
}
六、循环引用
但是shared_他也有不足之处,我们看如下场景
class A
{
public:A(int a = 0):_a(a){cout << "A(int a=0)" << endl;}~A(){cout << this;cout << "~A" << endl;}int _a;
};
struct Node
{A _val;Node* _next;Node* _prev;
};
int main()
{txf::shared_ptr<Node> sp1(new Node);txf::shared_ptr<Node> sp2(new Node);sp1->_next = sp2;sp2->_prev = sp1;return 0;
}
问题在于,一个类包装上一个指针 ,类中要实现出这个指针的各种使用场景的函数还是不容易的,相当于用一个类去模拟指针的行为 ,既然是智能指针,就要做到指针的作用,但在这个场景中,明显类型是不匹配的
我们可以把_next和_prev换成shared_ptr
struct Node
{A _val;txf::shared_ptr<Node> _next;txf::shared_ptr<Node> _prev;
};
看似好像没问题
但程序直接崩掉了,这是因为此事发生,内存泄露这个问题已叫做,循环引用
这就导致sp1的空间有两个指针指向着它
sp1和sp2析构后:
_prev管着左边的节点,_next管着右边的节点
->什么时候_prev析构要看右边节点析构时候,_prev才析构,
->右边节点什么时候析构 ,_next析构时候,右边节点才会析构,
->那么_next什么时候析构呢?要看左边的节点析构 ,_next才会析构
->左边节点,什么时候析构要看_prev析构时候左边节点什么时候析构,
->那么这又绕回来了_prev什么时候析构?
像是我管着你,你管着我
此时就引出了weak_ptr,他不是RAII智能指针,他是专门用来解决share_ptr循环引用问题的,他不是单独使用的
七、weak_ptr
他解决的原理是不增加引用计数_pcount,他可以访问资源,但不参与资源释放的管理,这相当于让_next _prev用weak_ptr不让他们参与到资源的管理中,降低了他们的权限。当然,weak_ptr也只能在这种情况来使用了,因为_pcount的增加,就是为了解决auto_ptr中的资源的问题,这里刚好不需要他们参与资源的释放,所以用weak_pre, 去掉_pcount 刚好
template<class T>class weak_ptr{public://RAII 所有智能指针的特性:// //1.RAII管控资源释放//2.像指针一样//3.拷贝问题weak_ptr():_ptr(nullptr){}T& operator*(){return *_ptr;}T* operator->(){return _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;};
八、总结
每个智能指针都要符合三个部分的功能,除了auto_ptr
- 第一个部分是RAII : 构造函数的时候把这个指针保存起来,析构函数的时候,把那个指针给释放掉
- 第一个部分: 像指针一样
- 第三个就是拷贝问题
拷贝问题
- auto_ptr : 产生的比较早,他的拷贝实际上是一个管理权的转移,这样就会导致被拷贝对象被置空,建议不要使用它
- unique_ptr : 它这个智能指针,是禁止拷贝方式,是简单粗暴,但是他相比于做到像指针一样就有缺陷了,但是如果是不需要拷贝的场景,就建议使用它
- shared_ptr : 引用计数支持拷贝 ,可以在需要拷贝的时候用这个智能指针,但是它有可能会构成引用循环,引用循环就会导致内存泄露
- weak_ptr : 专门解决share_ptr的循环引用问题,不能单独使用, 它的参数列表的类型都是share_ptr
最后再总结一下C++智能指针的主要类型和特性
std::unique_ptr
独占所有权: std::unique_ptr 对所拥有的对象拥有独占所有权,即同一时间内只能有一个 std::unique_ptr 指向给定的对象。
不可复制: std::unique_ptr 不能被复制,只能被移动。这意味着你不能通过赋值或拷贝构造函数来共享 std::unique_ptr 。
自动释放:当 std::unique_ptr 被销毁时,它所指向的对象也会被自动销毁。
自定义删除器:可以指定自定义删除器来释放资源。
用途:适用于资源独占的场景,如文件句柄、网络连接等。
2. std::shared_ptr
共享所有权: std::shared_ptr 允许多个智能指针共享对同一对象的所有权。
引用计数: std::shared_ptr 使用引用计数机制来跟踪有多少个 shared_ptr 指向同一个对象。当最后一个 shared_ptr 被销毁时,对象才会被释放。
可复制: std::shared_ptr 可以被复制,每次复制都会增加引用计数。
循环引用问题:如果存在循环引用,即两个或多个 shared_ptr 相互引用,会导致内存泄漏。
用途:适用于需要共享资源的场景,如多个对象需要访问同一数据。
3. std::weak_ptr
非拥有引用: std::weak_ptr 是一种非拥有引用,它指向一个由 std::shared_ptr 管理的对象,但不增加引用计数。
解决循环引用: std::weak_ptr 可以用来解决 std::shared_ptr 的循环引用问题。
需要转换为 shared_ptr:要使用 std::weak_ptr 所指向的对象,需要将其转换为 std::shared_ptr 。
用途:适用于需要访问共享资源但不拥有资源的场景,如观察者模式中的观察者。
4. std::auto_ptr(已废弃)
C++98 遗留: std::auto_ptr 是 C++98 中引入的智能指针,但在 C++11 中已被废弃,并在 C++17 中被移除。
独占所有权:与 std::unique_ptr 类似, std::auto_ptr 也拥有独占所有权。
复制语义问题: std::auto_ptr 的复制语义会导致所有权转移,这在某些情况下会导致问题。
用途:由于已被废弃,不建议在新代码中使用。
完结。