C++ 智能指针

智能指针概念
C/C++ 语言最为人所诟病的特性之一就是存在内存泄露问题,因此后来的大多数语言都提供了内置内存分配与释放功能,有的甚至干脆对语言的使用者屏蔽了内存指针这一概念。这里不置贬褒,手动分配内存与手动释放内存有利也有弊,自动分配内存和自动释放内存亦如此,这是两种不同的设计哲学。有人认为,内存如此重要的东西怎么能放心交给用户去管理呢?而另外一些人则认为,内存如此重要的东西怎么能放心交给系统去管理呢?在 C/C++ 语言中,内存泄露的问题一直困扰着广大的开发者,因此各类库和工具的一直在努力尝试各种方法去检测和避免内存泄露,如 boost,智能指针技术应运而生。

智能指针主要用于管理在堆上分配的内存,它将普通的指针封装为一个栈对象。当栈对象的生存周期结束后,会在析构函数中释放掉申请的内存,从而防止内存泄漏。简要的说,智能指针利用了 C++ 的 RAII 机制,在智能指针对象作用域结束后,会自动做内存释放的相关操作,不需要我们再手动去操作内存。

RAII是C++的发明者Bjarne Stroustrup提出的概念,RAII全称是“Resource Acquisition is Initialization”,直译过来是“资源获取即初始化”,也就是说在构造函数中申请分配资源,在析构函数中释放资源。因为C++的语言机制保证了,当一个对象创建的时候,自动调用构造函数,当对象超出作用域的时候会自动调用析构函数。所以,在RAII的指导下,我们应该使用类来管理资源,将资源和对象的生命周期绑定。

C++ 中有四种智能指针:auto_pt、unique_ptr、shared_ptr、weak_ptr 其中后三个是 C++11 支持,第一个已经被 C++11 弃用且被 unique_prt 代替,不推荐使用。下文将对其逐个说明。

std::auto_ptr

在这个年代讨论 std::auto_ptr 不免有点让人怀疑是不是有点过时了,确实如此,随着 C++11 标准的出现(最新标准是 C++20),std::auto_ptr 已经被彻底放弃,取而代之是 std::unique_ptr。然而,之所以还向介绍一下 std::auto_ptr 的用法以及它的设计不足之处是想更多了解 C++ 语言中智能指针的发展过程,一项技术如果我们了解它过去的样子和发展的轨迹,我们就能更好地掌握它。

std::auto_ptr 的基本用法如下代码所示:

#include <iostream>
#include <memory>
using namespace std;
int main()
{
    //初始化方式1
    std::auto_ptr<int> ap1(new int(8));
    //初始化方式2
    std::auto_ptr<int> ap2;
    ap2.reset(new int(8));
    cout << *ap1 << ", " << *ap2 << endl;
    return 0;
}

输出:

8, 8

智能指针对象 ap1 和 ap2 均持有一个在堆上分配 int 对象,其值均是 8,这两块堆内存均可以在 ap1 和 ap2 释放时得到释放。这是 std::auto_ptr 的基本用法。

std::auto_ptr 真正让人容易误用的地方是其不常用的复制语义,即当复制一个 std::auto_ptr 对象时(拷贝复制或 operator= 复制),原对象所持有的堆内存对象也会转移给复制出来的对象。示例代码如下:

#include <memory>

int main()
{
    //测试拷贝构造
    std::auto_ptr<int> ap1(new int(8));
    std::auto_ptr<int> ap2(ap1);
    if (ap1.get() != NULL)
    {
        std::cout << "ap1 is not empty." << std::endl;
    }
    else
    {
        std::cout << "ap1 is empty." << std::endl;
    }
    if (ap2.get() != NULL)
    {
        std::cout << "ap2 is not empty." << std::endl;
    }
    else
    {
        std::cout << "ap2 is empty." << std::endl;
    }
    
    //测试赋值构造
    std::auto_ptr<int> ap3(new int(8));
    std::auto_ptr<int> ap4;
    ap4 = ap3;
    if (ap3.get() != NULL)
    {
        std::cout << "ap3 is not empty." << std::endl;
    }
    else
    {
        std::cout << "ap3 is empty." << std::endl;
    }

    if (ap4.get() != NULL)
    {
        std::cout << "ap4 is not empty." << std::endl;
    }
    else
    {
        std::cout << "ap4 is empty." << std::endl;
    }
    return 0;
}

运行结果如下:

ap1 is empty.
ap2 is not empty.
ap3 is empty.
ap4 is not empty.

分析:上述代码中分别利用拷贝构造(ap1 => ap2)和 赋值构造(ap3 => ap4)来创建新的 std::auto_ptr 对象,因此 ap1 持有的堆对象被转移给 ap2,ap3 持有的堆对象被转移给 ap4。而 ap1 和 ap2 已经指向 NULL,若现在再对 ap1 和 ap2 进行访问并操作,将会出现内存错误问题。

由于 std::auto_ptr 这种不常用的复制语义,我们应该避免在 stl 容器中使用 std::auto_ptr,例如我们绝不应该写出如下代码:

std::vector<std::auto_ptr<int>> myvectors;

当用算法对容器操作的时候(如最常见的容器元素遍历),很难避免不对容器中的元素实现赋值传递,这样便会使容器中多个元素被置为空指针,这不是我们想看到的,会造成很多意想不到的错误。

std::unique_ptr

作为对 std::auto_ptr 的改进,std::unique_ptr 对其持有的堆内存具有唯一拥有权,也就是 std::unique_ptr 不可以拷贝或赋值给其他对象,其拥有的堆内存仅自己独占,std::unique_ptr 对象销毁时会释放其持有的堆内存。

可以使用以下方式初始化一个 std::unique_ptr 对象:

int main()
{
    //初始化方式1
    std::unique_ptr<int> up1(new int(123));
    //初始化方式2
    std::unique_ptr<int> up2;
    up2.reset(new int(123));
    //初始化方式3 (-std=c++14)
    std::unique_ptr<int> up3 = std::make_unique<int>(123);
}

应该尽量使用初始化方式 3 的方式去创建一个 std::unique_ptr 而不是方式 1 和 2,因为形式 3 更安全

鉴于 std::auto_ptr 的前车之鉴,std::unique_ptr 禁止复制语义,为了达到这个效果,std::unique_ptr 类的拷贝构造函数和赋值运算符(operator =)被标记为 delete。

template <class T>
class unique_ptr
{
    //省略其他代码...

    //拷贝构造函数和赋值运算符被标记为delete
    unique_ptr(const unique_ptr &) = delete;
    unique_ptr &operator=(const unique_ptr &) = delete;
};

因此,下列代码是无法通过编译的:

    std::unique_ptr<int> up1(std::make_unique<int>(123));; // C++ 14
    std::unique_ptr<int> up2(up1); // 错误
    std::unique_ptr<int> up3;
    up3 = up1; // 错误

禁止复制语义也存在特例,即可以通过一个函数返回一个 std::unique_ptr:

#include <memory>

std::unique_ptr<int> func(int val)
{
    std::unique_ptr<int> up(new int(val));
    return up;
}

int main()
{
    std::unique_ptr<int> up1 = func(123);

    return 0;
}

上述代码从 func 函数中得到一个 std::unique_ptr 对象,然后返回给 up1。

既然 std::unique_ptr 不能复制,那么如何将一个 std::unique_ptr 对象持有的堆内存转移给另外一个呢?答案是使用移动构造,示例代码如下:

int main()
{
    std::unique_ptr<int> up1(std::make_unique<int>(123));
    std::unique_ptr<int> up2(std::move(up1));
    std::cout << ((up1.get() == nullptr) ? "up1 is NULL" : "up1 is not NULL") << std::endl;
    
    std::unique_ptr<int> up3;
    up3 = std::move(up2);
    std::cout << ((up2.get() == nullptr) ? "up2 is NULL" : "up2 is not NULL") << std::endl;
    
    return 0;
}

运行结果:

up1 is NULL
up2 is NULL

以上代码利用 std::move 将 up1 持有的堆内存(值为 123)转移给 up2,再把 up2 转移给 up3。最后,up1 和 up2 不再持有堆内存的引用,变成一个空的智能指针对象。并不是所有的对象的 std::move 操作都有意义,只有实现了移动构造函数或移动赋值运算符的类才行,而 std::unique_ptr 正好实现了这二者,以下是实现伪码:

template <typename T, typename Deletor>
class unique_ptr
{
    //其他函数省略...
public:
    unique_ptr(unique_ptr &&rhs)
    {
        this->m_pT = rhs.m_pT;
        //源对象释放
        rhs.m_pT = nullptr;
    }

    unique_ptr &operator=(unique_ptr &&rhs)
    {
        this->m_pT = rhs.m_pT;
        //源对象释放
        rhs.m_pT = nullptr;
        return *this;
    }

private:
    T *m_pT;
};

常用函数

void reset(pointer p = pointer())

释放当前由 unique_ptr(如果有)管理的指针并获得参数 p(参数 p 默认为 NULL)的所有权。如果 p 是空指针(例如默认初始化的指针),则 unique_ptr 变为空,调用后不管理任何对象。

pointer release()

返回管理的指针并将其替换为空指针, 释放其管理指针的所有权。这个调用并不会销毁托管对象,但是将 unique_ptr 对象管理的指针解脱出来。如果要强制销毁所指向的对象,请调用 reset 函数或对其执行赋值操作。

element_type* get()

返回存储的指针,不会使 unique_ptr 释放指针的所有权。因此,该函数返回的值不能于构造新的托管指针,如果为了获得存储的指针并释放其所有权,请调用 release。

void swap (unique_ptr& x)

将 unique_ptr 对象的内容与对象 x 进行交换,在它们两者之间转移管理指针的所有权而不破坏二者。

#include <iostream>
#include <memory>

int main()
{

    std::unique_ptr<int> up1(std::make_unique<int>(123)); // C++ 14
    std::unique_ptr<int> up2;
    up2.reset(new int(111));
    up2.swap(up1);
    std::cout << "*up1: " << *up1 << ", *up2: " << *up2 << std::endl;
    up2.release();
    if (up2.get() == nullptr) {
        std::cout << "up2 is null" << std::endl;
    }
    up1.reset();
    if (up1.get() == nullptr) {
        std::cout << "up1 is null" << std::endl;
    }
 
    return 0;
}

输出:

*up1: 111, *up2: 123
up2 is null
up1 is null

std::shared_ptr

std::unique_ptr 对其持有的资源具有独占性,而 std::shared_ptr 持有的资源可以在多个 std::shared_ptr 之间共享,每多一个 std::shared_ptr 对资源的引用,资源引用计数将增加 1,每一个指向该资源的 std::shared_ptr 对象析构时,资源引用计数减 1,最后一个 std::shared_ptr 对象析构时,发现资源计数为 0,将释放其持有的资源。

下面是一个初始化 std::shared_ptr 的示例:
 

int main()
{
    //初始化方式1
    std::shared_ptr<int> sp1(new int(123));

    //初始化方式2
    std::shared_ptr<int> sp2;
    sp2.reset(new int(123));

    //初始化方式3
    std::shared_ptr<int> sp3;
    sp3 = std::make_shared<int>(123);

    return 0;
}

和 std::unique_ptr 一样,你应该优先使用 std::make_shared 去初始化一个 std::shared_ptr 对象。

再来看另外一段代码:

#include <iostream>
#include <memory>

class A
{
public:
    A()
    {
        std::cout << "A constructor" << std::endl;
    }

    ~A()
    {
        std::cout << "A destructor" << std::endl;
    }
};

int main()
{
    {
        //初始化方式1
        std::shared_ptr<A> sp1(new A());

        std::cout << "use count: " << sp1.use_count() << std::endl;

        //初始化方式2
        std::shared_ptr<A> sp2(sp1);
        std::cout << "use count: " << sp1.use_count() << std::endl;

        sp2.reset();
        std::cout << "use count: " << sp1.use_count() << std::endl;

        {
            std::shared_ptr<A> sp3 = sp1;
            std::cout << "use count: " << sp1.use_count() << std::endl;
        }

        std::cout << "use count: " << sp1.use_count() << std::endl;
    }

    return 0;
}

所以整个程序的执行结果如下:

A constructor
use count: 1
use count: 2
use count: 1
use count: 2
use count: 1
A destructor

常用函数

void swap (unique_ptr& x)

将 shared_ptr 对象的内容与对象 x 进行交换,在它们两者之间转移管理指针的所有权而不破坏或改变二者的引用计数。

void reset()

void reset (ponit p)

没有参数时,先将管理的计数器引用计数减一并将管理的指针和计数器置清零。有参数 p 时,先做面前没有参数的操作,再管理 p 的所有权和设置计数器。

element_type* get()

得到其管理的指针。

long int use_count()

返回与当前智能指针对象在同一指针上共享所有权的 shared_ptr 对象的数量,如果这是一个空的 shared_ptr,则该函数返回 0。如果要用来检查 use_count 是否为 1,可以改用成员函数 unique 会更快。

bool unique()

返回当前 shared_ptr 对象是否不和其他智能指针对象共享指针的所有权,如果这是一个空的 shared_ptr,则该函数返回 false。

element_type& operator*()

重载指针的 * 运算符,返回管理的指针指向的地址的引用。

element_type* operator->()

重载指针的 -> 运算符,返回管理的指针,可以访问其成员。

explicit operator bool()

返回存储的指针是否已经是空指针,返回的结果与 get() != 0 相同。

#include <iostream>
#include <memory>

int main()
{

    std::shared_ptr<int> up1(std::make_shared<int>(123)); // C++ 14
    std::shared_ptr<int> up2;
    up2.reset(new int(111));
    std::cout << "*up1: " << *up1 << ", *up2: " << *up2 << std::endl;
    up2.swap(up1);
    std::cout << "*up1: " << *up1 << ", *up2: " << *up2 << std::endl;
    int* p = new int(222);
    up1.reset(p);
    if (p == nullptr) {
        std::cout << "p is nullptr" << std::endl;
    }
    std::cout << "*up1: " << *up1 << std::endl;
 
    return 0;
}

 输出:

*up1: 123, *up2: 111
*up1: 111, *up2: 123
*up1: 222

std::weak_ptr

weak_ptr是弱智能指针对象,它不控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的智能指针。将一个weak_ptr绑定到一个shared_ptr对象,不会改变shared_ptr的引用计数。一旦最后一个所指向对象的shared_ptr被销毁,所指向的对象就会被释放,即使此时有weak_ptr指向该对象,所指向的对象依然被释放。
std::weak_ptr 可用来解决 std::shared_ptr 相互引用时的死锁问题(即两个std::shared_ptr 相互引用,那么这两个指针的引用计数永远不可能下降为 0, 资源永远不会释放)。

#include <iostream>
#include <memory>

using namespace std;

class B;
class A
{
public:
    shared_ptr<class B> m_spB;
};


class B
{
public:
    shared_ptr<class A> m_spA;
};

//weak reference
class WeakB;
class WeakA
{
public:
    weak_ptr<class WeakB> m_wpB;
};


class WeakB
{
public:
    weak_ptr<class WeakA> m_wpA;
};


void test_loop_ref()
{
    weak_ptr<class A> wp1;

    {
        auto pA = make_shared<class A>();
        auto pB = make_shared<class B>();

        pA->m_spB = pB;
        pB->m_spA = pA;

        cout << "pA count: " << pA.use_count() << ", " << "pB count: " << pB.use_count() << endl;

        wp1 = pA;    
    }//内存泄漏

    cout << "wp1 count: " << wp1.use_count() << "
";

    weak_ptr<class WeakA> wp2;
    {
        auto pA = make_shared<class WeakA>();
        auto pB = make_shared<class WeakB>();

        pA->m_wpB = pB;
        pB->m_wpA = pA;

        cout << "pA count: " << pA.use_count() << ", " << "pB count: " << pB.use_count() << endl;

        wp2 = pA;
    }//无内存泄漏

    cout << "wp2 count: " << wp2.use_count() << "
";
}

int main()
{    
    //std::weak_ptr 用来避免 std::shared_ptr 的循环引用
    test_loop_ref();

    return 0;
}

运行结果:

pA count: 2, pB count: 2
wp1 count: 1
pA count: 1, pB count: 1
wp2 count: 0

另外,std::weak_ptr 有几个常用函数如下:

void swap (weak_ptr& x)

将当前 weak_ptr 对象的内容与 x 的内容交换。

void reset()

将当前 weak_ptr 对象管理的指针和计数器变成空的,就像默认构造的一样。

long int use_count()

返回与当前 weak_ptr 对象在同一指针上共享所有权的 shared_ptr 对象的数量。

bool expired()

检查是否过期,返回 weak_ptr 对象管理的指针为空,或者和他所属共享的没有更多 shared_ptr。lock 函数一般需要先调用 expired 判断,如果已经过期,就不能通过 weak_ptr 恢复拥有的 shared_ptr。此函数应返回与(use_count() == 0)相同的值,但是它可能以更有效的方式执行此操作。

shared_ptr<element_type> lock()

如果它没有过期,则返回一个 shared_ptr,其中包含由 weak_ptr 对象保留的信息。如果 weak_ptr 对象已经过期,则该函数返回一个空的 shared_ptr(默认构造一样)。因为返回的 shared_ptr 对象也算作一个所有者,所以这个函数锁定了拥有的指针,防止它被释放(至少在返回的对象没有释放它的情况下)。 此操作以原子方式执行。

线程安全性讨论

std::shared_ptr

智能指针包括一个实际数据指针和一个引用计数指针,这两个操作不是一个指令可以完成的,因此多线程环境下,势必有问题。

 

参考资料

https://blog.csdn.net/code_peak/article/details/119722167

https://blog.csdn.net/c_base_jin/article/details/79440999

https://www.jianshu.com/p/cb3e574eee5f

http://www.cppblog.com/Solstice/archive/2013/01/28/197597.html

原文地址:https://www.cnblogs.com/xumaomao/p/15175448.html