C++智能指针

本文详细介绍了C++11标准库中智能指针的概念及其使用方法,包括shared_ptr、weak_ptr、unique_ptr等类型的特点与应用场景。并讨论了它们在资源管理、生命周期控制以及异常安全性方面的优势。

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<string> pNico(new string("nico"));  //OK
shared_ptr<string> pNico = new string("nico"); //ERROR
shared_ptr<string> pNico{new string("nico")};  //OK

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

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

shared_ptr<string> pNico = make_shared<string>("nico"); //OK

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

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

shared_ptr<string> pNico4;
pNico4 = new string("nico");        //ERROR
pNico4.reset(new string(nico));     //OK

定义一个Deleter

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

上诉伪代码会打印出:

delete nico

对付Array

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

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

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

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

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

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

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

std::unique_ptr<int[]> p(new int[10]);    //OK
std::shared_ptr<int[]> p(new int[10]);    //ERROR

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

std::unique_ptr<int, void(*)(int*)> 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<json_t> 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<A> p1 = std::make_shared<A>();
std::shared_ptr<A> p2(new A);

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

异常安全

考虑下面一段代码:
void f(std::shared_ptr &lhs, std::shared_ptr &rhs){...}

f(std::shared_ptr<Lhs>(new Lhs()),
std::shared_ptr<Rhs>(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<Lhs>(new Lhs());
auto rhs = std::shared_ptr<Rhs>(new Rhs());
f(lhs, rhs);

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

f(std::make_shared<Lhs>(),
 std::make_shared<Rhs>()
);
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<B> m_ptB;
};
class B{
public:
    int m_b;
    shared_ptr<A> m_ptA;
};

void fn()
{
    shared_ptr<A> ptA(new A);
    shared_ptr<B> 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<A> 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<int> sp1(p);
shared_ptr<int> sp2(p);        //ERROR

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

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

考虑一下代码:

shared_ptr<Person> mom(new Person(name+"'s mom"));
shared_ptr<Person> dad(new Person(name+"'s dad"));
shared_ptr<Person> kid(new Person(name));
ked->setParentsAndTheirKids(mom,dad);

class Person {
    public:
        stirng name;
        shared_ptr<Person> mother;
        shared_ptr<Person> father;
        vector<weak_ptr<Person>> kids;
    void setParentsAndTheirKids(shared_ptr<Person> m = nullprt
                                 shared_ptr<Person> f = nullptr) { 
        mother = m;
        father = f;
        if (m != nullptr){
            m->kids.push_back(shared_ptr<Person>(this));   //ERROR
        }                            
        if (f != nullprt){
            f->kids.push_back(shared_ptr<Person>(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<Person> {
    public:
        stirng name;
        shared_ptr<Person> mother;
        shared_ptr<Person> father;
        vector<weak_ptr<Person>> kids;
    void setParentsAndTheirKids(shared_ptr<Person> m = nullprt
                                 shared_ptr<Person> 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<void> sp(new int);
...
shred_ptr<int>(static_cast<int*>(sp.get()))   //ERROR
static_pointer_cast<int*>(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 <memory>

void f()
{
    std::unique<ClassA> ptr(new ClassA);
    ...
}

使用unique_ptr

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

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

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

std::unique_ptr<int> up = new int;      //ERROR
std::unique_ptr<int> up(new int);       //OK

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

std::unique_ptr<std::string> 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<std::string> up1(sp);
std::unique_ptr<std::string> up2(sp);  //ERROR

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

std::unique_ptr<ClassA> up1(new ClassA);

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

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

std::unique_ptr<ClassA> up1(new ClassA);
std::unique_ptr<ClassA> up2;

up2 = up1;

up2 = std::move(up1);

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

std::unique_ptr<ClassA> up1(new ClassA);
std::unique_ptr<ClassA> up2(new ClassA);

up2 = std::move(up1);

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

std::unique_ptr<ClassA> ptr;
ptr = new ClassA;                             //ERROR
ptr = std::unique_ptr<ClassA>(new ClassA);   //OK

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

    up = nullptr;

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

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

代码如下:

void sink(std::unique_ptr<ClassA> up)
{
    ...
}

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

代码如下:

std::unique_ptr<ClassA> source()
{
    std::unique_ptr<classA> ptr(new ClassA);
    ...
    return ptr;
}

void g()
{
    std::unique_ptr<ClassA> 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<ClassA> ptr1;
    std::unique_ptr<ClassB> 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<std::string> up(new std::string[10]);

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

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

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

std::unique_ptr<std::string[]> up(new std::string[10]);  //OK
...
std::cout << *up << std::endl;      //ERROR
std::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<ClassA, ClassADeleter> 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<typename T>
{
    if (p.get() == NLLL ) {
        std::cout << "NULL";
    }
    else {
        std::cout << *p;
    }
}

std::auto_ptr<int> 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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值