博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
C++智能指针
阅读量:5091 次
发布时间:2019-06-13

本文共 12897 字,大约阅读时间需要 42 分钟。

C++智能指针

本文为《C++标准库》这本书的学习笔记

自C++11起,C++标准库提供两大类型的smart pointer:

  • Class shared_ptr 实现共享式概念。多个smart pointer可以指向相同对象,该对象和棋相关资源会在“最后一个reference被销毁”时被释放。为了应对复杂的情境,标准库提供了weak_ptr、 bad_weak_ptr和enable_shared_from_this等辅助类。
  • Class unique_ptr 实现独占式拥有或严格拥有概念,保证同一时间内只有一个smart pointer可以指向对象。

Class shared_ptr

Class shared_ptr提供了“当对象再也不被使用时就被清理”的共享式拥有语义。也就是说,多个shared_ptr可以共享同一个对象。对象的最后一个拥有者有责任销毁对象,并清理与该对象有关的所有资源。

使用shared_ptr

shared_ptr
pNico(new string("nico")); //OKshared_ptr
pNico = new string("nico"); //ERRORshared_ptr
pNico{new string("nico")}; //OK

使用赋值符,意味着需要一个隐式转化。但是,由于“接受单一pointer作为唯一实参”的构造函数是explicit,所以不能使用。然而新式初始化语法是被接受的。

也可以使用便捷函数make_shared():

shared_ptr
pNico = make_shared
("nico"); //OK

这种方式比较快,也比较安全,因为它使用一次而不是二次分配:一次针对对象,另外一次针对“shared pointer用以控制对象”的shared data。

另一种写法是,可以先声明shared pointer,然后对它赋值一个new pointer。然而你不可以使用assignment操作符,必须改用reset():

shared_ptr
pNico4;pNico4 = new string("nico"); //ERRORpNico4.reset(new string(nico)); //OK

定义一个Deleter

shared_ptr
pNico(new string("nico"), [](string* p) { cout << "delete " << *p << endl; delete p; });...pNico = nullptr; //whoMadecoffee是一个vector
>的容器,有3个元素whoMadeCoffee.resize(2);

上诉伪代码会打印出:

delete nico

对付Array

shared_ptr提供的default deleter调用的是delete,不是delete[]。所以如下调用是错误的:

std::shared_ptr
p(new int[10]);

所以必须自己定义一个deleter。可以传递一份函数,函数对象或lambda,让它们针对传入的寻常指针调用delete[]。例如:

std::shared_ptr
p(new int[10], [](int* p) { delete[] p; });

也可以使用为unique_ptr而提供的辅助函数作为deleter,其内部调用delete[]:

std::shared_ptr
p(new int[10], std::default_delete
());

注意,shared_ptr和unique_ptr以稍稍不同的方式处理deleter。

std::unique_ptr
p(new int[10]); //OKstd::shared_ptr
p(new int[10]); //ERROR

此外,对于unique_ptr,你必须明确给予第二个template实参:

std::unique_ptr
p(new int[10], [](int* p){ delete[] p; });

其他析构策略

如果清理工作不仅仅是删除内存,你必须明确给出自己的deleter。可以指定属于自己的析构策略。比如:

//伪代码class Json_t_Deleter{    void operator (json_t* ptJson) {        json_realease(ptJson);    }}char* strjson = "...";  //json字符串文本std::shared_ptr
ptJson( json_load(strjson), Json_t_Deleter() );

实现说明

在典型的实现中,std::shared_ptr 只保存两个指针:

  • 指向被管理对象的指针
  • 指向控制块(control block)的指针
    控制块是一个动态分配的对象,

其中包含:

  • 指向被管理对象的指针或被管理对象本身
  • 删除器
  • 分配器(allocator)
  • 拥有被管理对象的 shared_ptr 的数量
  • 引用被管理对象的 weak_ptr 的数量

通过 std::make_shared 和 std::allocate_shared 创建 shared_ptr 时,控制块将被管理对象本身作为其数据成员;而通过构造函数创建 shared_ptr 时则保存指针。

shared_ptr 持有的指针是通过 get() 返回的;而控制块所持有的指针/对象则是最终引用计数归零时会被删除的那个。两者并不一定相等。

shared_ptr 的析构函数会将控制块中的 shared_ptr 计数器减一,如果减至零,控制块就会调用被管理对象的析构函数。但控制块本身直到 std::weak_ptr 计数器同样归零时才会释放。

make_shared和shared_ptr的区别

struct A;std::shared_ptr p1 = std::make_shared();std::shared_ptr p2(new A);

区别是:std::shared_ptr构造函数会执行两次内存申请,而std::make_shared则执行一次。那个一次和两次的区别会带来什么不同的效果呢?

异常安全

考虑下面一段代码:

void f(std::shared_ptr &lhs, std::shared_ptr &rhs){...}

f(std::shared_ptr
(new Lhs()),std::shared_ptr
(new Rhs()));

因为C++允许参数在计算的时候打乱顺序,因此一个可能的顺序如下:

  1. new Lhs()
  2. new Rhs()
  3. std::shared_ptr
  4. std::shared_ptr

此时假设第2步出现异常,则在第一步申请的内存将没处释放了,上面产生内存泄露的本质是当申请数据指针后,没有马上传给std::shared_ptr,因此一个可能的解决办法是:

auto lhs = std::shared_ptr
(new Lhs());auto rhs = std::shared_ptr
(new Rhs());f(lhs, rhs);

而一个比较好的方法是使用 std::make_shared 。

f(std::make_shared
(), std::make_shared
());

make_shared的缺点

因为make_shared只申请一次内存,因此控制块和数据块在一起,只有当控制块中不再使用时,内存才会释放,但是weak_ptr却使得控制块一直在使用。

Class weak_ptr

shared_ptr会自动释放"不再被需要的对象"的相应资源,但是有些时候却存在问题:

  • cyclic reference(环式指向)。两个对象使用shared_ptr互相指向对方,这种情况下shared_ptr不会释放数据。
  • 如果你“明确想共享但不愿拥有”某对象的情况。你的语义是:reference的寿命比其所指的寿命更长。如果使用寻常pointer可能不会注意到它们指向的对象已经不再有效,导致“访问已被释放的数据”的风险。

于是标准库提供了class weak_ptr,允许你“共享但不拥有”某个对象。一旦最末一个拥有该对象的shared pointer失去了拥有权,任何weak pointer就会自动成空。因此,在default和copy构造函数之外,class weak_ptr只提供“接受一个shared_ptr”的构造函数。

你不能够使用操作符*和->访问weak_ptr指向的对象。而是必须建立一个shared_pointer。这是合理的设计,两个理由:

  1. 在weak pointer之外建立一个shared pointer可因此检查是否存在一个相应对象。如果不,操作会抛出异常或建立一个empty shared pointer。
  2. 当指向的对象正在被处理是,shared pointer无法被释放。

如下例子:

class A{ public:    int m_a;    shared_ptr m_ptB;};class B{public:    int m_b;    shared_ptr m_ptA;};void fn(){    shared_ptr ptA(new A);    shared_ptr ptB(new B);    ptA.m_ptB.reset(ptB);    ptB.m_ptA.reset(ptA);}

当fn函数执行完了以后,指向A对象和B对象的user_count都为1。如果,将class B中的shared_ptr<A> m_ptA;修改为weak_ptr<A> m_ptA就可以解决问题。

class B{public:    int m_b;    weak_ptr m_ptA;};

使用weak_pointer时,我们必须轻微改变被指向对象的访问方式。不应该使用以下调用形式:

m_ptA->m_a

应该在式子内加上lock():

m_ptA.lock()->m_a

这会导致新产生一个得自于weak_ptr m_ptA的shared_ptr。如果这时对象的最末拥有也在此时释放了对象----lock()会产生一个empty shared_ptr。这种情况下调用*或->操作符会引发不明确行为。

如果不确定隐身于weak pointer背后的对象是否存活,可以有以下选择:

  1. 调用expired(),它会在weak_ptr不再共享对象时返回true。这等同于检查use_count()是否为0,但速度较快。
  2. 可以使用相应的shared_ptr构造函数明确将weak_ptr转化为一个shared_ptr。如果对象已经不存在,该构造函数会抛出一个bad_weak_ptr异常,那是一个派生自std:exception的异常,其what()会产生"bad_weak_ptr"。
  3. 调用use_count(),询问相应对象的拥有者数量。返回0表示不存在任何有效对象。但是,通常只应为了吊事而调用use_count();C++标准库明确告诉我们:“use_count并不总是很有效率。”

误用Shared Pointer

使用者必须确保某对象只被一组shared pointer拥有。下列代码错误:

int* p = new int;shared_ptr
sp1(p);shared_ptr
sp2(p); //ERROR

问题出在sp1,sp2都会在对视p的拥有权时释放相应资源(即调用delete)。所以应该在创建对象和其相应资源的那一刻直接设立smart pointer:

shared_ptr
sp1(new int);shared_ptr
sp2(sp1); //OK

考虑一下代码:

shared_ptr
mom(new Person(name+"'s mom"));shared_ptr
dad(new Person(name+"'s dad"));shared_ptr
kid(new Person(name));ked->setParentsAndTheirKids(mom,dad);class Person { public: stirng name; shared_ptr
mother; shared_ptr
father; vector
> kids; void setParentsAndTheirKids(shared_ptr
m = nullprt shared_ptr
f = nullptr) { mother = m; father = f; if (m != nullptr){ m->kids.push_back(shared_ptr
(this)); //ERROR } if (f != nullprt){ f->kids.push_back(shared_ptr
(this)); //ERROR } }}

问题出在“得自this的那个shared pointer”的建立。我们需要一个shared pointer指向kid,而我们手上没有。如果,根据this建立起一个新的shared pointer并不能解决问题,因为这样一来就开启了一个新的拥有者团队。

解决方法之一:讲指向kid的那个shared pointer传递为第三参数。但C++标准库提供了另一个选项:class std::enable_shared_from_this<>。

可以从std::enable_shared_from_this<>派生出你自己的class,表现出“被shared pointer管理”的对象;做法是讲class名称当做template实参传入。然后你就可以使用shared_from_this建立起一个源自this的正确shared_ptr。

class Person :  std::enable_shared_from_this
{ public: stirng name; shared_ptr
mother; shared_ptr
father; vector
> kids; void setParentsAndTheirKids(shared_ptr
m = nullprt shared_ptr
f = nullptr) { mother = m; father = f; if (m != nullptr){ m->kids.push_back(shared_from_this()); } if (f != nullprt){ f->kids.push_back(shared_from_this()); } }}

注意,不能再构造函数内调用shared_from_this。因为shared_ptr本身被存放于Person的base class,所以,在初始化shared pointer的那个对象的构造期间,绝对无法建立shared pointer的循环引用。

转型

cast操作符可将一个pointer转为不同类型。其语义与其所对应的操作符相同,得到的是不同类型的另一个shared pointer。不可以使用寻常的cast操作符,因为那会导致不明确行为:

shared_ptr
sp(new int);...shred_ptr
(static_cast
(sp.get())) //ERRORstatic_pointer_cast
(sp) //OK

Class unique_ptr

这个smart pointer实现了独占式拥有概念,意味着它可确保一个对象和其相应资源同一时间只被一个pointer拥有。一旦拥有者被销毁或变成empty,或开始拥有另外一个对象,先前拥有的那个对象就会被销毁,其任何相应资源亦会被释放。

Class unique_ptr继承class auto_ptr,后者由C++98引入但已不再被认可。Class unique_ptr提供了一个简明干净的接口,比auto_pointer更不易出错。

Class unique_ptr的目的

如下程序可能会有诸多问题。一个明显的问题是,有可能忘记delete对象,特别是如果你在函数中有个return语句。另外一个较不明显的危险是它可能抛出异常。那将立刻退离函数,末尾的delete语句也就没机会被调用,会导致内存泄漏。

void f(){    ClassA* ptr = new ClassA;    ...    delete ptr;}

使用智能指针解决问题:

#include 
void f(){ std::unique
ptr(new ClassA); ...}

使用unique_ptr

unique_ptr有着与寻常pointer非常相似的接口,操作符*用来提领(dereference)指向对象,操作符->用来访问成员----如果被指向的对象来自class或struct。但是不提供point算术如++等。poniter算术运算符往往是麻烦的来源。

std::unique_ptr
up(new std::string("nico"));(*up)[0] = 'N';up->append("lai");std::cout << *up << std::endl;

class unique_ptr<>不允许你以赋值语法将一个寻常的pointer当作初值。因此必须直接初始化:

std::unique_ptr
up = new int; //ERRORstd::unique_ptr
up(new int); //OK

unique_ptr不比一定拥有对象,也可以是empty。例如当它被default构造函数创建出来:

std::unique_ptr
up;

你也可以对它赋予nullptr,调用reset()或调用release()进行释放:

up = nullptr;up.reset();up.release();

你可以调用操作符bool()用以检查是否unique pointer拥有对象,也可以拿unique pointer和nullprt比较,或查询unique_ptr内的raw pointer:

if(up) {    std::cout << *up << std::endl;}if (up != nullptr)if (up.get() != nullptr)

转移unique_ptr的拥有权

unique_ptr提供的语义是“独占式拥有”。但需要程序员来确保“没有两个unique pointer以同一个pointer作为初值”。以下程序为运行期错误:

std::string* sp = new std::string("hello");std::unique_ptr
up1(sp);std::unique_ptr
up2(sp); //ERROR

不可以对unique_ptr执行copy或assign。但是C++11起提供的move语义,可以使用copy构造函数或assignment操作符会将拥有权移交给另一个unique pointer。

std::unique_ptr
up1(new ClassA);std::unique_ptr
up2(up1); //ERROR,编译期错误std::unique_ptr
up3(std::move(up1)); //OK

Assignment操作符的行为和上面所说很类似:

std::unique_ptr
up1(new ClassA);std::unique_ptr
up2;up2 = up1;up2 = std::move(up1);

如果上述赋值动作之前up2原本拥有对象,会有一个delete动作被调用,删除该对象:

std::unique_ptr
up1(new ClassA);std::unique_ptr
up2(new ClassA);up2 = std::move(up1);

失去对象拥有权的unique_ptr并不会获得一个"指向无物"(refer to no object)的新拥有权。如果要赋新值给unique_ptr,新值必须也是个unique_ptr,不可以是寻常pointer:

std::unique_ptr
ptr;ptr = new ClassA; //ERRORptr = std::unique_ptr
(new ClassA); //OK

赋值nullptr也可以,和调用reset()效果相同:

up = nullptr;

###源头和去处

拥有权的转移暗暗支出了unique_ptr的一种用途:函数可利用它们将拥有权一处给其他函数。这会发生在两种情况下:

  1. 函数是接收端。如果使用std::move()建立起来的unique_ptr以rvalue reference身份当作函数实参,那么被调用函数的参数将会取得unique_ptr的拥有权。因此,如果该函数不再转移拥有权,对象将会在函数结束时被deleted.

代码如下:

void sink(std::unique_ptr
up){ ...}std::unique_ptr
up(new ClassA);...sink(std::move(up));...
  1. 函数是供应端。当函数返回一个unique_ptr,其拥有权会转移至调用端场景内。在这里source()的return语句不需要std::move()的原因是,C++11语言规定,编译器应该自动尝试加上move。

代码如下:

std::unique_ptr
source(){ std::unique_ptr
ptr(new ClassA); ... return ptr;}void g(){ std::unique_ptr
p; for (int i=0; i<10; ++i) { p = source(); ... }}

unique_ptr被当作成员

在class内部使用unique_ptr可避免资源泄漏,且不再需要析构函数,但还是需要copy构造函数和assignment操作符;此外unique_ptr也可协助避免“对象初始化期间因抛出异常而造成资源泄漏”。因为只有当构造函数完成的时候,析构函数才有可能被调用。所以,如果在构造函数中,第一个new成功,第二个new失败,就可能导致内存泄漏。

class ClassB{    private:    std::unique_ptr
ptr1; std::unique_ptr
ptr2; public: ClassB(int val1, int val2) : ptr1(new ClassA(val1)), ptr2(new ClassA(val2)) { } ClassB(const ClassB& x) : ptr1(new ClassA(*x.ptr1)), ptr2(new ClassA(*x.ptr2)) { } const ClassB& operator= (const ClassB& x) { *ptr1 = *x.ptr1; *ptr2 = *x.ptr2; return *this; } ...};

对付Array

默认情况下unique_ptr如果失去拥有权,会为其所拥有的对象调用delete。而对于数组需要调用delete[],所以一下语句错误:

std::unique_ptr
up(new std::string[10]);

shared_ptr需要自己定义deleter才能处理array,但是unique_ptr拥有一个偏特化版本:

std::unique_ptr
up(new std::string[10]);

但是这个偏特化版本不再提供操作符*和->,改而提供操作符[],用以访问其所指向的array中的某一个对象,并且索引的合法性需要程序员进行保证:

std::unique_ptr
up(new std::string[10]); //OK...std::cout << *up << std::endl; //ERRORstd::cout << up[0] << std::endl; //OK

同时这个class不接受"派生类型"的array作为初值。

其他相应资源的Deleter

不同于shared_ptr的deleter定义方式,unique_ptr需要具体致命deleter的类型作为第二个template实参。例如:

class ClassADeleter{    public:        void operator () (ClassA* p) {            std::cout << "call delete for ClassA object" << std::endl;            delete p;        }};...std::unique_ptr
up(new ClassA());

如果你给的是个函数或lambda,必须声明deleter的类型为void(*)(T*)std::function<void(T*)>,要不就使用decltype

Class auto_ptr

class auto_ptr是C++98标准库提供的一个smart pointer,但它已被C++11明确声明不再支持。它的目标是提供如今unique_ptr所提供的语义,不过却导致一些问题:

  • 设计它时,C++语言尚未有move语义让构造函数和assignment操作符使用。
  • 不存在deleter所表示的语义,因此你智能使用它处理“以new分配之单一对象”。
  • 由于它是C++标准库提供的最早且唯一的smart pointer,所以常被误用,特别是他僭越地提供了如今class shared_ptr提供的拥有权共享语义。

考虑以下代码:

template
{ if (p.get() == NLLL ) { std::cout << "NULL"; } else { std::cout << *p; }}std::auto_ptr
p(new int);*p = 42;bad_print(p); //控制权转移,在bad_print函数执行完后数据对象被析构*p = 18; //RUNTIME ERROR

Smart Pointer结语

关于效能

Class shared_ptr由于存在拥有控制块,所以会有一些额外的开销。unique_ptr是无状态,其效能与raw pointer无异。

关于使用

Smart pointer并不完美,还是会有很多误用的情况。并且不是线程安全。

转载于:https://www.cnblogs.com/zhangtianqi/p/5448626.html

你可能感兴趣的文章
jq check 复选变单选。
查看>>
C#父类子类对象关系
查看>>
(转)RACI模型(RACI Model)
查看>>
关于&&和||
查看>>
HTML5 学习笔记(一)——HTML5概要与新增标签
查看>>
浅谈MySQL之@1
查看>>
Jenkins使用
查看>>
三层VS控制器
查看>>
函数传递参数的本质
查看>>
Python学习第二篇
查看>>
Python学习第三篇——逻辑判定
查看>>
c++入门之函数指针和函数对象
查看>>
一个数组中同时找到最大/最小值
查看>>
python终端下打印颜色
查看>>
《从Paxos到ZooKeeper 分布式一致性原理与实践》阅读【Leader选举】
查看>>
RPC框架基础概念理解以及使用初体验
查看>>
Visual Studio 代码风格约束
查看>>
Jzoj3170 挑选玩具
查看>>
测试随想
查看>>
JavaWeb_客户端相对/绝对路径和服务器端路径
查看>>