[C++11新特性] weak_ptr和unique_ptr

一、weak_ptr弱引用的智能指针

1.1 shared_ptr相互引用会有什么后果?

shared_ptr的一个最大的陷阱是循环引用,循环引用会导致堆内存无法正确释放,导致内存泄漏。看下面的例子:

#include <iostream>
#include <memory>

class Parent;  // Parent类的前置声明

class Child {
public:
    Child() { std::cout << "hello child" << std::endl; }
    ~Child() { std::cout << "bye child" << std::endl; }

    std::shared_ptr<Parent> father;
};

class Parent {
public:
    Parent() { std::cout << "hello Parent" << std::endl; }
    ~Parent() { std::cout << "bye parent" << std::endl; }

    std::shared_ptr<Child> son;
};

void testParentAndChild() {

}

int main() {
    std::shared_ptr<Parent> parent(new Parent());  // 1  资源A
    std::shared_ptr<Child> child(new Child());  // 2   资源B
    parent->son = child;     // 3   child.use_count() == 2 and parent.use_count() == 1 
    child->father = parent;  // 4   child.use_count() == 2 and parent.use_count() == 2

    return 0;
}

/*
输出:
hello Parent
hello child
*/

很惊讶的发现,用了shared_ptr管理资源,没有调用 Parent 和 Child 的析构函数,表示资源最后还是没有释放!内存泄漏还是发生了。

分析:

  • 执行编号1的语句时,构造了一个共享智能指针p,称呼它管理的资源叫做资源Anew Parent()产生的对象)吧, 语句2构造了一个共享智能指针c,管理资源B(new Child()产生的对象),此时资源AB的引用计数都是1,因为只有1个智能指针管理它们,执行到了语句3的时候,是一个智能指针的赋值操作,资源B的引用计数变为了2,同理,执行完语句4,资源A的引用计数也变成了2
  • 出了函数作用域时,由于析构和构造的顺序是相反的,会先析构共享智能指针c,资源B的引用计数就变成了1;接下来继续析构共享智能指针p,资源A的引用计数也变成了1。由于资源AB的引用计数都不为1,说明还有共享智能指针在使用着它们,所以不会调用资源的析构函数!
  • 这种情况就是个死循环,如果资源A的引用计数想变成0,则必须资源B先析构掉(从而析构掉内部管理资源A的共享智能指针),资源B的引用计数想变为0,又得依赖资源A的析构,这样就陷入了一个死循环。

1.2 weak_ptr如何解决相互引用的问题

要想解决上面循环引用的问题,只能引入新的智能指针std::weak_ptrstd::weak_ptr有什么特点呢?与std::shared_ptr最大的差别是在赋值的时候,不会引起智能指针计数增加。

  • weak_ptr被设计为与shared_ptr共同工作,可以从一个shared_ptr或者另一个weak_ptr对象构造,获得资源的观测权。但weak_ptr没有共享资源,它的构造不会引起指针引用计数的增加。
  • 同样,在weak_ptr析构时也不会导致引用计数的减少,它只是一个静静地观察者。weak_ptr没有重载operator*->,这是特意的,因为它不共享指针,不能操作资源,这是它弱的原因。
  • 如要操作资源,则必须使用一个非常重要的成员函数lock()从被观测的shared_ptr获得一个可用的shared_ptr对象,从而操作资源。

当我们创建一个weak_ptr时,要用一个shared_ptr来初始化它:

auto p = make_shared<int>(42);
weak_ptr<int> wp(p); // wp弱共享p; p的引用计数未改变

我们在上面的代码基础上使用std::weak_ptr进行修改,如下:

#include <iostream>
#include <memory>

class Parent;  // Parent类的前置声明

class Child {
public:
    Child() { std::cout << "hello child" << std::endl; }
    ~Child() { std::cout << "bye child" << std::endl; }

    // 测试函数
    void testWork()
    {
        std::cout << "testWork()" << std::endl;
    }

    std::weak_ptr<Parent> father;
};

class Parent {
public:
    Parent() { std::cout << "hello Parent" << std::endl; }
    ~Parent() { std::cout << "bye parent" << std::endl; }

    std::weak_ptr<Child> son;
};

void testParentAndChild() {

}

int main() {
    std::shared_ptr<Parent> parent(new Parent());
    std::shared_ptr<Child> child(new Child());
    parent->son = child;
    child->father = parent;
    std::cout << "parent_ref:" << parent.use_count() << std::endl;
    std::cout << "child_ref:" << child.use_count() << std::endl;

    // 把std::weak_ptr类型转换成std::shared_ptr类型,以调用内部成员函数
    std::shared_ptr<Child> tmp = parent.get()->son.lock();
    tmp->testWork();
    std::cout << "tmp_ref:" << tmp.use_count() << std::endl;

    return 0;
}

/*
输出:
hello Parent
hello child
parent_ref:1
child_ref:1
testWork()
tmp_ref:2
bye child
bye parent
*/

由以上代码运行结果我们可以看到:

  • 所有的对象最后都能正常释放,不会存在上一个例子中的内存没有释放的问题;
  • parent 和 child 在 main 函数中退出前,引用计数均为 1,也就是说,对std::weak_ptr的相互引用,不会导致计数的增加。

1.3 weak_ptr常用操作

weak_ptr<T> w;	// 空weak_ptr可以指向类型为T的对象
weak_ptr<T> w(shared_ptr p);	// 与p指向相同对象的weak_ptr, T必须能转换为sp指向的类型
w = p;	// p可以是shared_ptr或者weak_ptr,赋值后w和p共享对象
w.reset();	// weak_ptr置为空
w.use_count();	// 与w共享对象的shared_ptr的计数
w.expired();	// w.use_count()为0则返回true,否则返回false
w.lock();	// w.expired()为true,返回空的shared_ptr;否则返回指向w的shared_ptr

二、unique_ptr独占的智能指针

2.1 unique_ptr的基本使用

unique_ptr相对于其他两个智能指针更加简单,它和shared_ptr使用差不多,但是功能更为单一,它是一个独占型的智能指针,不允许其他的智能指针共享其内部的指针,更像原生的指针(但更为安全,能够自己释放内存)。不允许赋值和拷贝操作,只能够移动

std::unique_ptr<int> ptr1(new int(0));
std::unique_ptr<int> ptr2 = ptr1; // 错误,不能复制
std::unique_ptr<int> ptr3 = std::move(ptr1); // 可以移动

在 C++11 中,没有类似std::make_shared的初始化方法,但是在 C++14 中,对于std::unique_ptr引入了std::make_unique方法进行初始化。

#include <iostream>
#include <memory>

int main()
{
    std::unique_ptr<std::string> ptr1(new std::string("unique_ptr"));
    std::cout << "ptr1 is " << *ptr1 << std::endl;

    std::unique_ptr<std::string> ptr2 = std::make_unique<std::string>("make_unique init!");
    std::cout << "ptr2 is " << *ptr2 << std::endl;

    return 0;
}
/*
输出:
ptr1 is unique_ptr
ptr2 is make_unique init!
*/

2.2 unique_ptr常用操作

下面列出了unique_ptr特有的操作。

unique_ptr<T> u1 // 空unique_ptr,可以指向类型为T的对象。u1会使用delete来释放它的指针
unique_ptr<T, D> u2 // u2会使用一个类型为D的可调用对象来释放它的指针
unique_ptr<T, D> u(d) // 空unique_ptr,指向类型为T的对象,用类型为D的对象d替代delete
u = nullptr // 释放u指向的对象,将u置为空
u.release() // u放弃对指针的控制权,返回指针,并将u置为空
u.reset() // 释放u指向的对象
u.reset(q) // 如果提供了内置指针q,另u指向这个对象;否则将u置为空
u.reset(nullptr)   

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

unique_ptr<string> p1(new string("Stegosaurus"));
// 将所有权从pl (指向string Stegosaurus)转移给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和它原来管理的对象间的联系,如果我们不用另一个智能指针来保存 release 返回的指针,我们的程序就要负责资源的释放:

p2.release(); // 错误:p2不会释放内存,而且我们丢失了指针
auto p = p2.release(); // 正确,但我们必须记得 delete(p)
delete(p);

2.3 传递unique_ptr参数和返回unique_ptr

不能拷贝 unique_ptr 的规则有一个例外:我们可以拷贝或赋值一个将要被销毁的 unique_ptr。最常见的例子是从函数返回一个unique_ptr

unique_ptr<int> clone (int p) 
{
	unique_ptr<int> ret(new int (p));
	// ...
    return ret;
}

对于上面这段代码,编译器都知道要返回的对象将要被销毁。在此情况下,编译器执行一种特殊的“拷贝”,在《C++ Primer》13.6.2节(第473页)中有介绍。

三、性能与安全的权衡

使用智能指针虽然能够解决内存泄漏问题,但是也付出了一定的代价。以shared_ptr举例:

  • shared_ptr的大小是原始指针的两倍,因为它的内部有一个原始指针指向资源,同时有个指针指向引用计数。
  • 引用计数的内存必须动态分配。虽然一点可以使用make_shared()来避免,但也存在一些情况下不能够使用make_shared()
  • 增加和减小引用计数必须是原子操作,因为可能会有读写操作在不同的线程中同时发生。比如在一个线程里有一个指向一块资源的shared_ptr可能调用了析构(因此所指向的资源的引用计数减一),同时,在另一线程里,指向相同对象的一个shared_ptr可能执行了拷贝操作(因此,引用计数加一)。原子操作一般会比非原子操作慢。但是为了线程安全,又不得不这么做,这就给单线程使用环境带来了不必要的困扰。

我觉得还是分场合吧,看应用场景来进行权衡,我也没啥经验,但我感觉安全更重要,现在硬件已经足够快了,其他例如java这种支持垃圾回收的语言不还是用的很好吗。


参考:

《C++ Primer 第5版》

c++11&14-智能指针专题

c++11]智能指针学习笔记


原文地址:https://www.cnblogs.com/linuxAndMcu/p/14576078.html