> 技术文档 > C++智能指针解析

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\"; } }}