C++智能指针解析
普通指针的不足
- new和new[]的内存需要用delete和delete[]释放
- 程序员的主观失误,忘了或漏了释放
- 程序员也不确定何时释放(例如:多个线程共享一个对象的时候,就没办法确定什么时候释放内存)
因为堆区的内存一定要手工释放,否则会发生内存泄漏
普通指针的释放
- 类内的指针,在析构函数中释放
- C++内置数据类型,如何释放?
- new出来的类,本身如何释放?
智能指针的设计思路
- 智能指针是类模版,在栈上创建智能指针对象
- 把普通指针交给智能指针对象
- 智能指针对象过期时,调用析构函数释放普通指针的内存
智能指针的类型
- auto_ptr是C++98的标准,C++17已弃用
- unique_ptr, shared_ptr和weak_ptr是C++11标准的
智能指针unique_ptr
unique_ptr独享它指向的对象,也就是说,同时只有一个unique_ptr指向同一个对象,当这个unique_ptr被销毁时,指向的对象也随即被销毁。
#include #include #include #include class AA{ public: AA(){ std::cout<<\"调用构造函数AA()。\\n\"; } AA(const std::string &name):m_Name(name){ std::cout<<\"调用构造函数AA\"<<m_Name<<\"\\n\"; } ~AA(){ std::cout<<\"调用了析构函数~AA\"<<m_Name<<\"\\n\"; } std::string m_Name;};int main(){ AA *p=new AA(\"西施\"); //模版参数AA表示需要管理的普通指针的基类型是AA,p表示被管理的指针,p指向了new出来的对象地址 //间接的意思是让智能指针pu1来管理对象 std::unique_ptr pu1(p);}
可以看到,在main程序中,没有使用delete语句,也销毁了AA的对象。原因是,智能指针是类,其也有析构函数,在它的析构函数中,使用了delete语句,所以,有了如下效果
智能指针的使用也很简单,它重载了*和->操作符,就像使用普通指针一样使用智能指针
#include #include #include #include class AA{ public: AA(){ std::cout<<\"调用构造函数AA()。\\n\"; } AA(const std::string &name):m_Name(name){ std::cout<<\"调用构造函数AA\"<<m_Name<<\"\\n\"; } ~AA(){ std::cout<<\"调用了析构函数~AA\"<<m_Name<<\"\\n\"; } std::string m_Name;};int main(){ AA *p=new AA(\"西施\"); //模版参数AA表示需要管理的普通指针的基类型是AA,p表示被管理的指针,p指向了new出来的对象地址 //间接的意思是让智能指针pu1来管理对象 std::unique_ptr pu1(p); std::cout<<\"m_Name=\"<<(*pu1).m_Name<<\"\\n\"; std::cout<<\"m_Name=\"<m_Name<<\"\\n\";}
初始化有三种方法
方法一:
unique_ptr p0(new AA(\"西施\")); //分配内存并初始化
方法二:
unique_ptr p0=make_unique(\"西施\");//C++14标准
方法三:
AA *p=new AA(\"西施\");unique_ptr p0(p); //用已存在的地址初始化
第三种方式不是一种好方法,但是它符合初学者的思维习惯。在实际开发中,第一种方法更常用。
这种方法把new出来的对象作为unique_ptr构造函数的参数,根本不需要定义普通指针,有了智能指针,大家基本上不需要普通指针了。
第一第三种方法实际上是一样的,用的是同一个构造函数,构造函数的参数是地址,因此new返回的是对象的地址,p中存放的也是对象的地址,都是地址。
其不能进行拷贝构造例如下面这样是非法的:
unique pu3;pu3=pu1;//错误,不能用=对unique_ptr进行赋值
原因就是因为如果unique_ptr对象允许复制,那么就会出现多个unique_ptr对象指向同一块内存的情况。当其中的一个unique_ptr对象过期的时候,释放内存,其他的unique_ptr对象过期的时候,又会释放内存,结果就是对同一块内存释放多次,就成了操作野指针,因此干脆把拷贝构造函数和赋值函数禁用掉。
注意事项:
- 不要用同一个裸指针初始化多个unique_ptr对象
- get()方法返回裸指针
- 不要用unique_ptr管理不是new分配的内存
注意:裸指针就是普通指针,也叫原始指针
因此第一种是最安全的,因为第一种初始化的方法看不到原始指针。
但是在实际开发中,如果使用了智能指针,最好再也不要用裸指针。
int main(){ // AA *p=new AA(\"西施\"); // //模版参数AA表示需要管理的普通指针的基类型是AA,p表示被管理的指针,p指向了new出来的对象地址 // //间接的意思是让智能指针pu1来管理对象 // std::unique_ptr pu1(p); AA *p=new AA(\"西施\"); std::unique_ptr pu1(p); std::cout<<\"裸指针的值是: \"<<p<<\"\\n\"; std::cout<<\"pu输出的结果是: \"<<pu1<<\"\\n\"; std::cout<<\"pu.get()输出的结果是: \"<<pu1.get()<<\"\\n\"; std::cout<<\"pu的地址是: \"<<&pu1<<\"\\n\";}
void func(std::unique_ptr &pp){ std::cout<<\"name=\"<m_Name<<\"\\n\";}int main(){ std::unique_ptr pu1(new AA(\"西施\")); func(pu1);}
这里只能传引用,智能指针是对象,有地址,但是最好不要传地址,很麻烦
更多技巧
(1)将一个unique_ptr赋给另一个时,如果源unique_ptr是一个临时右值,编译器允许这样做,如果源unique_ptr将存在一段时间,编译器禁止这样做。一般用于函数的返回值。
unique_ptr p0;p0=unique_ptr(new AA(\"西瓜\"));
(2)用nullptr给unique_ptr赋值,将释放对象,unique_ptr=nullptr;
int main(){ std::unique_ptr pu1(new AA(\"西施\")); std::cout<<\"赋值前。\\n\"; if(pu1!=nullptr) std::cout<<\"pu不是空的。\\n\"; pu1 = nullptr; std::cout<<\"赋值后。\\n\"; if(pu1==nullptr) std::cout<<\"pu是空的。\\n\";
(3)release()释放对指针的控制权,将unique_ptr置为空,返回裸指针。(可用于把unique_ptr传递给子函数,并且在子函数中释放对象)
//函数fun2()需要一个指针,并且对这个指针负责void func2(const AA *a){ std::cout<m_Name<<\"\\n\"; delete a;}int main(){ std::unique_ptr pu1(new AA(\"西施\")); //func1(pu1.get());//函数func()需要一个指针,但不对这个指针负责 func2(pu1.release());//需要这个指针,并且会对这个指针负责 std::cout<<\"调用函数完成。\\n\"; if(pu1==nullptr) std::cout<<\"它为空指针\\n\"; }
(4)std::move()可以转移对指针的控制权。(可用于把unique_ptr传递给子函数,并且在子函数中释放对象)。
//函数func4()需要一个unique_ptr,并且会对这个unique_ptr负责void func4(const std::unique_ptr &a){ std::cout<m_Name<<\"\\n\";}int main(){ std::unique_ptr pu(new AA(\"西施\")); //func1(pu1.get());//函数func()需要一个指针,但不对这个指针负责 // func2(pu1.release());//需要这个指针,并且会对这个指针负责 // std::cout<<\"调用函数完成。\\n\"; // if(pu1==nullptr) std::cout<<\"它为空指针\\n\"; func4(std::move(pu)); std::cout<<\"函数调用完成\"<<\"\\n\"; if(pu==nullptr) { std::cout<<\"pu是空指针。\\n\"; } }
#include #include #include #include class AA{ public: AA(){ std::cout<<\"调用构造函数AA()。\\n\"; } AA(const std::string &name):m_Name(name){ std::cout<<\"调用构造函数AA\"<<m_Name<<\"\\n\"; } ~AA(){ std::cout<<\"调用了析构函数~AA\"<<m_Name<<\"\\n\"; } std::string m_Name;};//函数fun1()需要一个指针,并且只是用一下这个指针,但不对这个指针负责,也就是说释放资源的事情,他不管void func1(const AA *a){ std::cout<m_Name<<\"\\n\";}//函数fun2()需要一个指针,并且对这个指针负责void func2(const AA *a){ std::cout<m_Name<<\"\\n\"; delete a;}//函数func3()需要一个unique_ptr,不会对这个unique_ptr负责void func3(const std::unique_ptr &a){ std::cout<m_Name<<\"\\n\";}//函数func4()需要一个unique_ptr,并且会对这个unique_ptr负责void func4(const std::unique_ptr &a){ std::cout<m_Name<<\"\\n\";}int main(){ std::unique_ptr pu(new AA(\"西施\")); //func1(pu1.get());//函数func()需要一个指针,但不对这个指针负责 // func2(pu1.release());//需要这个指针,并且会对这个指针负责 // std::cout<<\"调用函数完成。\\n\"; // if(pu1==nullptr) std::cout<<\"它为空指针\\n\"; func4(std::move(pu)); std::cout<<\"函数调用完成\"<<\"\\n\"; if(pu==nullptr) { std::cout<<\"pu是空指针。\\n\"; } }
(5)unique_ptr也可像普通指针那样,当指向一个类继承体系的基类对象时,也具有多态性质,,如果裸指针管理基类对象和派生类对象那样。
(6)unique_ptr不是绝对安全,如果程序中调用exit()退出,全局的unique_ptr可以自动释放,但局部的unique_ptr无法释放。
(7)unique_ptr提供了支持数组的具体化版本。
数组版本的unique_ptr,重载了操作符[],操作符[]返回的是引用,可以作为左值使用。
#include #include #include #include class AA{ public: AA(){ std::cout<<\"调用构造函数AA()。\\n\"; } AA(const std::string &name):m_Name(name){ std::cout<<\"调用构造函数AA\"<<m_Name<<\"\\n\"; } ~AA(){ std::cout<<\"调用了析构函数~AA\"<<m_Name<<\"\\n\"; } std::string m_Name;};int main(){ //普通数组指针 AA *parr1=new AA[2];//普通数组指针 //AA *parr1=new AA[2]{std::string(\"西施\"),std::string(\"冰冰\")}; parr1[0].m_Name=\"西施1\"; std::cout<<parr1[0].m_Name<<\"\\n\"; parr1[1].m_Name=\"西施2\"; std::cout<<parr1[1].m_Name<<\"\\n\"; delete[] parr1; //unique_ptr数组 std::unique_ptr parr2(new AA[2]); parr2[0].m_Name=\"西施1\"; std::cout<<parr2[0].m_Name<<\"\\n\"; parr2[1].m_Name=\"西施2\"; std::cout<<parr2[1].m_Name<<\"\\n\"; }
智能指针shared_ptr
shared_ptr共享它指向的对象,多个shared_ptr可以指向(关联)相同的对象,在内部采用计数机制方式来实现。
当新的shared_ptr与对象关联时,引用计数增加1。
当shared_ptr超出作用域时,引用计数减1。当引用计数变为0时,则表示没有任何shared_ptr与对象关联,则释放该对象。
#include #include #include #include class AA{ public: AA(){ std::cout<<\"调用构造函数AA()。\\n\"; } AA(const std::string &name):m_Name(name){ std::cout<<\"调用构造函数AA\"<<m_Name<<\"\\n\"; } ~AA(){ std::cout<<\"调用了析构函数~AA\"<<m_Name<<\"\\n\"; } std::string m_Name;};int main(){ AA *p=new AA(\"西施\"); std::shared_ptr p0(p); std::cout<<\"p0.use_count()=\"<<p0.use_count()<<\"\\n\";//显示p0引用计数的值 std::cout<m_Name<<\"\\n\";}
除了上面与unique_ptr一样的三种初始化方法之外,还有第四种方法,即用已存在的shared_ptr初始化,计数加1
shared_ptr p0(new AA(\"西施\"));shared_ptr p1(p0);shared_ptr p1=p0;
int main(){ AA *p=new AA(\"西施\"); std::shared_ptr p0(p); std::cout<<\"p0.use_count()=\"<<p0.use_count()<<\"\\n\";//显示p0引用计数的值 std::cout<m_Name<<\"\\n\"; std::shared_ptr p1(p0); std::cout<<\"p1.use_count()=\"<<p1.use_count()<<\"\\n\";//显示p0引用计数的值 std::cout<m_Name<<\"\\n\"; std::shared_ptr p2(p0); std::cout<<\"p2.use_count()=\"<<p2.use_count()<<\"\\n\";//显示p0引用计数的值 std::cout<m_Name<<\"\\n\";}
共享不是复制,资源只有一份,没有被复制,shared_ptr可以有多个,它们的原始指针都是一样的。
shared_ptr支持赋值,左值的shared_ptr的计数器将减1,右值的shared_ptr的计数器将加1.
left=right;
#include #include #include #include class AA{ public: AA(){ std::cout<<\"调用构造函数AA()。\\n\"; } AA(const std::string &name):m_Name(name){ std::cout<<\"调用构造函数AA\"<<m_Name<<\"\\n\"; } ~AA(){ std::cout<<\"调用了析构函数~AA\"<<m_Name<<\"\\n\"; } std::string m_Name;};int main(){ std::shared_ptr pa0(new AA(\"西施a\")); std::shared_ptr pa1=pa0; std::shared_ptr pa2=pa0; std::cout<<\"pa0.use_count()=\"<<pa0.use_count()<<\"\\n\"; std::shared_ptr pb0(new AA(\"西施b\")); std::shared_ptr pb1=pb0; std::cout<<\"pb0.use_count()=\"<<pb0.use_count()<<\"\\n\"; pb1=pa1; std::cout<<\"pa0.use_count()\"<<pa0.use_count()<<\"\\n\"; std::cout<<\"pb0.use_count()\"<<pb0.use_count()<<\"\\n\";}
执行这一行代码的时候,pb0在指向资源a的同时,也会释放资源b。
std::move()可以转移对原始指针的控制权,还可以将unique_ptr转移为shared_ptr
reset()改变与资源的关联关系。
pp.reset();//解除与资源的关系,资源的引用计数减少1pp.reset(new AA(\"bbb\"));//解除与资源的关系,资源的引用计数减1。关联新资源。
swap()交换两个shared_ptr的控制权
void swap(shared_ptr &_Right);
智能指针的删除器
在默认情况下,智能指针过期的时候,用 delete原始指针 释放它管理的资源。
程序员可以自定义删除器,改变智能指针释放资源的行为。
删除器可以是全局函数,仿函数和Lambda表达式,形参为原始指针。
#include #include #include #include class AA{ public: AA(){ std::cout<<\"调用构造函数AA()。\\n\"; } AA(const std::string &name):m_Name(name){ std::cout<<\"调用构造函数AA\"<<m_Name<<\"\\n\"; } ~AA(){ std::cout<<\"调用了析构函数~AA\"<<m_Name<<\"\\n\"; } std::string m_Name;};int main(){ //这一行代码我们没有指定删除器,因此其会用缺省的删除器。缺省删除器的行为就是把原始指针delete掉 std::shared_ptr pa1(new AA(\"西施a\")); }
也可以自定义删除器,自定义删除器的方法有三种,首先看普通函数deletefunc(),自定义删除器的目的是为了在删除资源的时候还做一点其他事情,做什么不重要,对于初学者,知道有这回事就行了
#include #include #include #include class AA{ public: AA(){ std::cout<<\"调用构造函数AA()。\\n\"; } AA(const std::string &name):m_Name(name){ std::cout<<\"调用构造函数AA\"<<m_Name<<\"\\n\"; } ~AA(){ std::cout<<\"调用了析构函数~AA\"<<m_Name<<\"\\n\"; } std::string m_Name;};void deletefunc(AA *a){ std::cout<<\"自定义删除器(全局函数).\\n\";}int main(){ std::shared_ptr pa1(new AA(\"西施a\"),deletefunc);//删除器,普通函数}
删除器也可以用仿函数,之前介绍STL算法的时候,仿函数我们也用过很多次
#include #include #include #include class AA{ public: AA(){ std::cout<<\"调用构造函数AA()。\\n\"; } AA(const std::string &name):m_Name(name){ std::cout<<\"调用构造函数AA\"<<m_Name<<\"\\n\"; } ~AA(){ std::cout<<\"调用了析构函数~AA\"<<m_Name<<\"\\n\"; } std::string m_Name;};class deleteclass{ public: void operator()(AA *a){ std::cout<<\"自定义删除器(仿函数).\\n\"; delete a; }};int main(){ std::shared_ptr pa(new AA(\"西施\"),deleteclass());}
同样,其删除器也可以使用lambda表达式
#include #include #include #include class AA{ public: AA(){ std::cout<<\"调用构造函数AA()。\\n\"; } AA(const std::string &name):m_Name(name){ std::cout<<\"调用构造函数AA\"<<m_Name<<\"\\n\"; } ~AA(){ std::cout<<\"调用了析构函数~AA\"<<m_Name<<\"\\n\"; } std::string m_Name;};auto deletelamb=[] (AA *a){ std::cout<<\"自定义删除器\\n\"<<\"\\n\"; delete a;};int main(){ std::shared_ptr pa(new AA(\"西施\"),deletelamb);}
智能指针weak_ptr
1.shared_ptr内部维护了一个共享的引用计数器,多个shared_ptr可以指向同一个资源。
如果出现了循环引用的情况,引用计数永远无法归0,资源不会被释放。
#include #include #include #include class AA{ public: AA(){ std::cout<<\"调用构造函数AA()。\\n\"; } AA(const std::string &name):m_Name(name){ std::cout<<\"调用构造函数AA\"<<m_Name<<\"\\n\"; } ~AA(){ std::cout<<\"调用了析构函数~AA\"<<m_Name<<\"\\n\"; } std::string m_Name; std::shared_ptr m_p;};class BB{ public: BB(){ std::cout<<\"调用构造函数BB()。\\n\"; } BB(const std::string &name):m_Name(name){ std::cout<<\"调用构造函数BB\"<<m_Name<<\"\\n\"; } ~BB(){ std::cout<<\"调用了析构函数~BB\"<<m_Name<<\"\\n\"; } std::string m_Name; std::shared_ptr m_p;};int main(){ std::shared_ptr pa=std::make_shared(\"西施a\"); std::shared_ptr pb=std::make_shared(\"西施b\"); pa->m_p=pb; pb->m_p=pa;}
weak_ptr是为了配合shared_ptr而引入的,它指向一个由shared_ptr管理的资源但不影响资源的生命周期。也就是说,将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。
不论是否weak_ptr指向,如果最后一个指向资源的shared_ptr被销毁,资源就会被释放。
weak_ptr更像是shared_ptr的助手而不是智能指针。
#include #include #include #include class AA{ public: AA(){ std::cout<<\"调用构造函数AA()。\\n\"; } AA(const std::string &name):m_Name(name){ std::cout<<\"调用构造函数AA\"<<m_Name<<\"\\n\"; } ~AA(){ std::cout<<\"调用了析构函数~AA\"<<m_Name<<\"\\n\"; } std::string m_Name; std::weak_ptr m_p;};class BB{ public: BB(){ std::cout<<\"调用构造函数BB()。\\n\"; } BB(const std::string &name):m_Name(name){ std::cout<<\"调用构造函数BB\"<<m_Name<<\"\\n\"; } ~BB(){ std::cout<<\"调用了析构函数~BB\"<<m_Name<<\"\\n\"; } std::string m_Name; std::weak_ptr m_p;};int main(){ std::shared_ptr pa=std::make_shared(\"西施a\"); std::shared_ptr pb=std::make_shared(\"西施b\"); pa->m_p=pb; pb->m_p=pa;}
如何使用weak_ptr
weak_ptr没有重载->和*操作符,不能直接访问资源。
有以下成员函数:
(1)operator=();//把shared_ptr或weak_ptr赋值给weak_ptr (2) expired();//判断它指资源是否过期(已经被销毁) (3) lock();//返回shared_ptr,如果资源已经过期,返回空的shared_ptr (4)reset();//将当前weak_ptr指针置为空 (5)swap();//交换
每次在使用weak_ptr的时候,需要使用expired()函数判断资源是否过期
weak_ptr的精髓
- weak_ptr不控制对象的生命周期,但是,他知道对象是否还活着
- 用lock()函数把它可以提升到shared_ptr,如果对象还活着,返回有效的shared_ptr,如果对象已经死了,则提升失败,返回一个空的shared_ptr.
- 提升的行为(lock())是线程安全的
#include #include #include #include class AA{ public: AA(){ std::cout<<\"调用构造函数AA()。\\n\"; } AA(const std::string &name):m_Name(name){ std::cout<<\"调用构造函数AA\"<<m_Name<<\"\\n\"; } ~AA(){ std::cout<<\"调用了析构函数~AA\"<<m_Name<<\"\\n\"; } std::string m_Name; std::weak_ptr m_p;};class BB{ public: BB(){ std::cout<<\"调用构造函数BB()。\\n\"; } BB(const std::string &name):m_Name(name){ std::cout<<\"调用构造函数BB\"<<m_Name<<\"\\n\"; } ~BB(){ std::cout<<\"调用了析构函数~BB\"<<m_Name<<\"\\n\"; } std::string m_Name; std::weak_ptr m_p;};int main(){ std::shared_ptr pa=std::make_shared(\"西施a\"); //然后用一个语句块 { std::shared_ptr pb=std::make_shared(\"西施b\"); pa->m_p=pb; pb->m_p=pa; std::shared_ptr pp=pa->m_p.lock(); if(pp==nullptr) { std::cout<m_p已过期\\n\"; } else{ std::cout<m_p.lock()->m_name=\"<m_p.lock()->m_Name<m_p.expired()==true) { std::cout<m_p已过期。\\n\"; } else{ std::cout<m_p.lock()->m_name\"<m_p.lock()->m_Name<<\"\\n\"; } }}