C++ -- 智能指针

611 阅读6分钟

内存中的栈区与堆区

栈区由操作系统自动分配和释放,其内存空间较小。 堆区需要手动申请和释放,常通过new关键字来分配,其内存空间较大。除了栈区和堆区外,C++还将内存分为代码区全局区:在代码区中存放函数体的二进制代码,由操作系统进行管理;在全局区中,存放全局变量静态(全局、局部)变量字符串常量

智能指针和常规指针相比,重要的区别在于:它负责自动释放所指向的对象。智能指针主要是用来管理堆上内存的分配。

智能指针的使用

在头文件<memory>中,有三个智能指针:

  • shared_ptr:共享资源所有权的指针
  • unique_ptr:独占资源所有权的指针
  • weak_ptr:共享资源的观察者,不控制所指向对象生存期,需要与shared_ptr一起使用。

shared_ptr

shared_ptr实现共享式拥有概念。shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。每使用它一次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,自动删除所指向的堆内存。shared_ptr内部的引用计数是线程安全的,但是对象的读取需要加锁。

初始化:

类似vector,智能指针也是个模板。因此,当我们创建智能指针时,必须提供额外的信息——指针可以指向的类型。与vector,我们在尖括号内给出类型,之后是定义的智能指针的名字:

shared_ptr<int> p1; // 不传入任何实参
shared_ptr<int> p2(nullptr); // 传入空指针
shared_ptr<int> p3; // 指向int
shared_ptr<vector<int>> p4; // 指向int的vector

// 和new结合使用
shared_ptr<int> p5 = new int(1024); // 错误!!!必须使用直接初始化形式
shared_ptr<int> p6(new int(1024)); // 正确,使用了直接初始化形式

// 使用make_shared函数
shared_ptr<int> p7 = make_shared<int>(42); // 指向一个值为42的int的shared_ptr
shared_ptr<string> p8 = make_shared<string>(5, '9'); // 指向一个值为"99999"的string
shared_ptr<int> p9 = make_shared<int>(); // 指向一个值初始化的int,即,值为0

// 使用拷贝构造函数
shared_ptr<int> p10(p9); // 或者shared_ptr<int> p10 = p9;

// 移动构造
std::shared_ptr<int> p11(std::move(p10)); //或者 std::shared_ptr<int> p11 = std::move(p10);

对于移动构造来说,std::move(p10)会强制将p10转换成对应的右值,和拷贝构造函数不同,调用移动构造函数时,p11拥有了p10的堆内存,而p10则变成了空智能指针

成员函数

shared_ptr成员方法.png

注意事项

多次赋值:同一普通指针不能同时为多个shared_ptr赋值,否则会导致程序出现异常,二次释放同一个内存。

int* ptr = new int; 
shared_ptr<int> p1(ptr); 
shared_ptr<int> p2(ptr); // 错误

循环引用:循环引用会导致堆内存无法释放,造成内存泄漏

weak_ptr

weak_ptr是一种不控制所指向对象生存期的智能指针,它指向一个shared_ptr管理的对象。将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。weak_ptr是为了配合shared_ptr而引入的一种智能指针,因为它不具有普通指针的行为,没有重载operator*和->,它的最大作用在于协助shared_ptr工作,像旁观者那样观测资源的使用情况。 weak_ptr可以从一个shared_ptr或者另一个weak_ptr对象构造,获得资源的观测权。但weak_ptr没有共享资源,它的构造不会引起指针引用计数的增加。

成员函数

使用weak_ptr的成员函数use_count() 可以观测资源的引用计数 另一个成员函数expired() 的功能等价于use_count()==0,但更快,表示被观测的资源(也就是shared_ptr的管理的资源)已经不复存在。
weak_ptr可以使用一个非常重要的成员函数lock()从被观测的shared_ptr获得一个可用的shared_ptr对象, 从而操作资源。但当expired()==true的时候,lock()函数将返回一个存储空指针的shared_ptr。

weak_ptr成员函数.png

解决shared_ptr循环引用问题

weak_ptr用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放

class B;
class A
{
    public:
      shared_ptr<B> pb_;
      ~A()
      {
        cout<<"A delete\n";
      }
};
class B
{
    public:
      shared_ptr<A> pa_;
      ~B()
      {
        cout<<"B delete\n";
      }
};
void fun()
{
    shared_ptr<B> pb(new B()); // B的引用计数为1
    shared_ptr<A> pa(new A()); // A的引用计数为1
    pb->pa_ = pa; // A的引用计数为2
    pa->pb_ = pb; // B的引用计数为2
    cout<<pb.use_count()<<endl;
    cout<<pa.use_count()<<endl;
}
int main()
{
    fun();
    return 0;
}

fun函数中pa ,pb之间互相引用,两个资源的引用计数为2,当要跳出函数时,智能指针pa,pb析构时两个资源引用计数会减一,但是两者引用计数还是为1,导致跳出函数时资源没有被释放(A B的析构函数没有被调用

解决方法: 把其中一个改为weak_ptr就可以了,我们把类A里面的shared_ptr<B> pb_; 改为weak_ptr<B> pb_; 这样的话,资源B的引用开始就只有1,当pb析构时,B的计数变为0,B得到释放,B释放的同时也会使A的计数减一,同时pa析构时使A的计数减一,那么A的计数为0,A得到释放。

下面这个例子和上面一样,不过换成了父子两个类,是正确的使用方法: 即在循环引用双方中某一方,把shared_ptr改成weak_ptr。这样初始化的时候计数器不会都等于2.一个1一个2就能释放。

#include <iostream>
#include <memory>

class Child;
class Parent;

class Parent {
private:
    //std::shared_ptr<Child> ChildPtr;
    std::weak_ptr<Child> ChildPtr;
public:
    void setChild(std::shared_ptr<Child> child) {
        this->ChildPtr = child;
    }

    void doSomething() {
        //new shared_ptr
        if (this->ChildPtr.lock()) {

        }
    }

    ~Parent() {
    }
};

class Child {
private:
    std::shared_ptr<Parent> ParentPtr;
public:
    void setPartent(std::shared_ptr<Parent> parent) {
        this->ParentPtr = parent;
    }
    void doSomething() {
        if (this->ParentPtr.use_count()) {

        }
    }
    ~Child() {
    }
};

int main() {
    std::weak_ptr<Parent> wpp;
    std::weak_ptr<Child> wpc;
    {
        std::shared_ptr<Parent> p(new Parent);
        std::shared_ptr<Child> c(new Child);
        p->setChild(c);
        c->setPartent(p);
        wpp = p;
        wpc = c;
        std::cout << p.use_count() << std::endl; // 2
        std::cout << c.use_count() << std::endl; // 1
    }
    std::cout << wpp.use_count() << std::endl;  // 0
    std::cout << wpc.use_count() << std::endl;  // 0
    return 0;
}

unique_ptr

一个unique_ptr“拥有”它所指向的对象,与shared_ptr不同,某个时刻只能有一个unique_ptr指向一个给定对象。当unique_ptr被销毁时,它所指向的对象也被销毁。

unique_ptr实现独占式拥有或严格拥有概念,保证同一时间内只有一个智能指针可以指向该对象(通过禁止拷贝语义、只有移动语义move来实现)。它对于避免资源泄露(例如“以new创建对象后因为发生异常而忘记调用delete”)特别有用。使得在出现异常的情况下,动态资源能得到释放。

unique_ptr指针本身的生命周期:从unique_ptr指针创建时开始,直到离开作用域。离开作用域时,若其指向对象,则将其所指对象销毁(默认使用delete操作符,用户可指定其他操作)。

初始化

与shared_ptr不同,没有类似make_shared的标准库函数返回一个unique_ptr。当我们定义一个unique_ptr时,需要将其绑定到一个new返回的指针上。类似shared_ptr,初始化unique_ptr必须采用直接初始化的形式:

unique_ptr<double> p1; // 可以指向一个double的unique_ptr
unique_ptr<int> p2(new int(42)); // p2指向一个值为42的int

注意事项

由于一个unique_ptr拥有它指向的对象,因此unique_ptr不支持普通的拷贝或赋值操作

unique_ptr<string> p1(new string("xxxxx"));
unique_ptr<string> p2(p1); // 错误! unique_ptr不支持拷贝
unique_ptr<string> p3;
p3 = p2; // 错误! unique_ptr不支持赋值

虽然不能拷贝或赋值unique_ptr,但可以通过调用release或reset将指针的所有权从一个(非const)unique_ptr转移给另一个unique:

// 将所有权从p1(指向string "xxxxx")转移给p2
unique_ptr<string> p2(p1.release()); // release将p1置为空
unique_ptr<string> p3(new string("Trex"));
// 将所有权从p3转移给p2
p2.reset(p3.release()); // reset释放了p2原来指向的内存

release成员返回unique_ptr当前保存的指针并将其置为空,因此,p2被初始化为p1原来保存的指针,而p1被置为空。

还可以通过移动语义转移所有权

unique_ptr<string> p2 = std::move(p1); // 通过move移动语义转移其所有权,转移后p1变成空的智能指针

unique_ptr虽然不允许拷贝构造和赋值,但是允许临时右值赋值:

unique_ptr<string> pu1(new string ("hello world"));
unique_ptr<string> pu2;
pu2 = pu1;             // #1 not allowed,或者unique_ptr pu2(pu1)也不允许
unique_ptr<string> pu3;
pu3 = unique_ptr<string>(new string ("You"));   // #2 allowed

不会留下悬挂的unique_ptr,因为它调用 unique_ptr 的构造函数,该构造函数创建的临时对象在其所有权让给 pu3 后就会被销毁

成员函数

unique_ptr成员函数.png