【C++】动态内存管理(四)智能指针(std)

智能指针总结

对于编译器来说,智能指针实际上是一个栈对象,并非指针类型,在栈对象生命期即将结束时,智能指针通过析构函数释放由它管理的堆内存。所有智能指针都重载了“operator->”操作符,直接返回对象的引用,用以操作对象。访问智能指针原来的方法则使用“.”操作符。
访问智能指针包含的裸指针则可以用 get() 函数。由于智能指针是一个对象,所以if (my_smart_object)永远为真,要判断智能指针的裸指针是否为空,需要这样判断:if(my_smart_object.get())。 智能指针包含了 reset() 方法,如果不传递参数(或者传递 NULL),则智能指针会释放当前管理的内存。如果传递一个对象,则智能指针会释放当前对象,来管理新传入的对象。
上一个博客我们已经自己做了几个资源管理对象,其实智能指针的构造也大多就那几种类型,但是却更加复杂了许多,而且C++自带的智能指针已经很不错了,我们也不用都用自己写的资源管理对象。所以我们就来测试一下C++的各种资源管理对象——智能指针。
本文主要讲解参见的智能指针的用法。包括:std::auto_ptr、std::unique_ptr、std::shared_ptr、std::weak_ptr

首先我们先构建一个对象,用来测试各个智能指针的特性:

        class Simple
    {
    private:
        size_t value;
    public:
        Simple(const Simple& rhs) :value(rhs.value) 
        {
            cout << "creat simple" << endl;
        };
        Simple() :value()
        { 
            cout << "creat simple" << endl; 
        };
        explicit Simple(size_t v) :value(value)
        {
            cout << "creat simple" << endl; 
        };
        void DoSomething()
        {
            cout << "hello" << endl;
        }
        ~Simple()
        { 
            cout << "delete Simple" << endl;
        }

    };

智能指针


1、std::auto_ptr

std::auto_ptr 属于 STL,自然就包含在 namespace std 中,包含头文件 #include<memory> 便可以使用。std::auto_ptr 能够方便的管理单个堆内存对象。

以下是一段测试代码:

void test()
{
    auto_ptr<Simple> auto_p(new Simple(1));
    if (auto_p.get())   //判断源指针是否为空
    {
        auto_p->DoSomething();//用->调用智能指针管理的对象中的函数
        auto_p->name = "auto_ptr";// 用->对智能指针管理的对象中的成员变量进行赋值
        auto_p.get()->DoSomething();//输出已被修改的name成员变量   
        (*auto_p).name = "change";  //使用*操作符直接获取资源对象,然后对资源对象进行操作
    }
}

执行结果如下:

creat simple
hellonone
helloauto_ptr
delete Simple

一切正常,不需要delete就可以自动将管理的资源进行析构,是不是很完美?
但是看一下下面的代码:

void test()
{
    auto_ptr<Simple> auto_p(new Simple(1));
    if (auto_p.get())   
    {
        auto_ptr<Simple> auto_p_1;
        auto_p->DoSomething();//正常
        auto_p_1 = auto_p;
        auto_p_1->DoSomething();//正常
        auto_p->DoSomething();//错误
    }
}

我们调试之后可以发现,在auto_p_1 = auto_p; 之后auto_p内部管理的内存就消失了,完全转移到了auto_p_1身上。
这个其实和我们上一篇博客实现的控制权转移的资源管理对象非常相似,在std::auto_ptr 的赋值和拷贝构造过程中,它会将源智能指针的资源夺取然后交给新智能指针,控制权发生了转移,然后源智能指针就是指向了一个空的内存空间。所以报错。
所以,尽量不要使用operator =操作,如果用了,就不要再使用先前对象。

再观察如下代码:

void test()
{
    auto_ptr<Simple> auto_p(new Simple(1));
    if (auto_p.get())   
    {
        auto_p.release();
    }
}

输出如下:

creat simple

我们创建出来的对象没有被析构,当我们不想让 auto_p继续生存下去,我们调用 release() 函数释放内存,结果却导致内存泄露(在内存受限系统中,如果auto_p占用太多内存,我们会考虑在使用完成后,立刻归还,而不是等到 auto_p 结束生命期后才归还)。
正确的代码应该这样:

    auto_ptr<Simple> auto_p(new Simple(1));
    if (auto_p.get())   
    {
        auto_p.reset();//释放auto_t管理的内存
    }

或者这样:

void test()
{
    auto_ptr<Simple> auto_p(new Simple(1));
    if (auto_p.get())   
    {
        Simple* p = auto_p.release();
        delete p;
    }
}

原来 std::auto_ptrrelease() 函数只是让出内存所有权,这显然不符合 C++ 编程思想。

总结:

std::auto_ptr 可用来管理单个对象的内存,但是,请注意如下几点:

(1) 尽量不要使用operator=和拷贝构造。如果使用了,请不要再使用先前对象。
(2) 记住 release()函数不会释放对象,仅仅归还所有权。
(3) std::auto_ptr 最好不要当成参数传递(读者可以自行写代码确定为什么不能)。
(4) 由于 std::auto_ptroperator=问题,有其管理的对象不能放入 std::vector 等容器中。
(5) ……


2、std::unique_ptr

std::unique_ptr 属于 STL 库,定义在 namespace std 中,包含头文件#include<memory> 便可以使用。
std::unique_ptrstd::auto_ptr 一样,可以方便的管理单个堆内存对象,特别的是,std::unique_ptr 独享所有权,避免了 std::auto_ptr恼人的几个问题。

std::unique_ptr具备如下特性:

  • std::unique_ptr 可以像 auto_ptr 一样使用

  • 由于std::unique_ptr 是独享所有权的,所以明确拒绝用户写auto_p_1 = auto_p之类的语句

以下是一个unique_ptr的使用示例:

#include <iostream>
#include <memory>

struct Foo {
    Foo() { std::cout << "Foo::Foo\n"; }
    ~Foo() { std::cout << "Foo::~Foo\n"; }
    void bar() { std::cout << "Foo::bar\n"; }
};

void f(const Foo &foo)
{
    std::cout << "f(const Foo&)\n";
}

int main()
{
    std::unique_ptr<Foo> p1(new Foo);  // p1 拥有 Foo
    if (p1) p1->bar();

    {
        std::unique_ptr<Foo> p2(std::move(p1));  // 现在 p2 拥有 Foo
        f(*p2);

        p1 = std::move(p2);  // 所有权还给了 p1
        std::cout << "destroying p2...\n";
    }

    if (p1) p1->bar();

    // p1 离开作用域时, Foo 实例会自动销毁
}


输出:

Foo::Foo
Foo::bar
f(const Foo&)
destroying p2...
Foo::bar
Foo::~Foo

常用函数

函数介绍
unique_ptr<T>up(T* res)将资源res交给up来管理,资源不使用时会自动释放。
pp可以作为条件判断,若p指向一个对象,则为true,否则为false
*pshared_ptr<T>对象解引用,得到的是由p管理的T类型对象的引用。
p->mem等价于(*p).mem。(比如所指对象的成员数据)
p.get()返回p中保存的T*指针。
swap(p,q)交换p,q各自内部的T*指针,注意p,q类型要相同。
p.swap(q)同上。
p = q这是无效操作,unique_ptr不能进行赋值。
unique_ptr<T>sp(q)这是无效操作,unique_ptr不能进行拷贝。
p = nullptr释放p指向的对象,并将p置为控。
p.release()p放弃对指针的控制权,返回资源指针,并将p置为空。
p.reset(T* q)释放p指向的对象,并将p置为空,若传入参数q,则让p指向q该指针。
p.reset(q.release())释放掉p指向的对象,q将自身指向对象的控制权转移给p,然后q自身置为空。

3、std::shared_ptr

同样的,这个资源管理对象我们也在前面实现过,通过引用计数的方式共享资源的所有权(多个std::shared_ptr可以管理同一个堆对象),并且保证对象被析构。当指向资源的所有的std::shared_ptr都被释放之后,他们共同管理的资源也会被析构。

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

  • 指向被管理对象的指针
  • 指向控制块(control block)的指针

控制块是一个动态分配的对象,其中包含:

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

通过 std::make_sharedstd::allocate_shared 创建 shared_ptr 时,控制块将被管理对象本身作为其数据成员;而通过构造函数创建 shared_ptr 时则保存指针。
shared_ptr 持有的指针是通过 get() 返回的;而控制块所持有的指针/对象则是最终引用计数归零时会被删除的那个。两者并不一定相等。
shared_ptr 的析构函数会将控制块中的 shared_ptr 计数器减一,如果减至零,控制块就会调用被管理对象的析构函数。但控制块本身直到 std::weak_ptr 计数器同样归零时才会释放。

下面是对std::shared_ptr的使用和测试(官方案例):

#include <iostream>
#include <memory>
#include <thread>
#include <chrono>
#include <mutex>

struct Base
{
    Base() { std::cout << "  Base::Base()\n"; }
    // 注意:这里可以使用非虚的析构器
    ~Base() { std::cout << "  Base::~Base()\n"; }
};

struct Derived: public Base
{
    Derived() { std::cout << "  Derived::Derived()\n"; }
    ~Derived() { std::cout << "  Derived::~Derived()\n"; }
};

void thr(std::shared_ptr<Base> p)
{
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::shared_ptr<Base> lp = p; // 类型安全,即使
                                  // 共享的 use_count 增加
    {
        static std::mutex io_mutex;
        std::lock_guard<std::mutex> lk(io_mutex);
        std::cout << "local pointer in a thread:\n"
                  << "  lp.get() = " << lp.get()
                  << ", lp.use_count() = " << lp.use_count() << '\n';
    }
}

int main()
{
    std::shared_ptr<Base> p = std::make_shared<Derived>();

    std::cout << "Created a shared Derived (as a pointer to Base)\n"
              << "  p.get() = " << p.get()
              << ", p.use_count() = " << p.use_count() << '\n';
    std::thread t1(thr, p), t2(thr, p), t3(thr, p);
    p.reset(); // 从 main 中释放所有权
    std::cout << "Shared ownership between 3 threads and released\n"
              << "ownership from main:\n"
              << "  p.get() = " << p.get()
              << ", p.use_count() = " << p.use_count() << '\n';
    t1.join(); t2.join(); t3.join();
    std::cout << "All threads completed, the last one deleted Derived\n";
}

可能的输出如下(内存地址不一定相同):

Base::Base()
  Derived::Derived()
Created a shared Derived (as a pointer to Base)
  p.get() = 0xc99028, p.use_count() = 1
Shared ownership between 3 threads and released
ownership from main:
  p.get() = (nil), p.use_count() = 0
local pointer in a thread:
  lp.get() = 0xc99028, lp.use_count() = 3
local pointer in a thread:
  lp.get() = 0xc99028, lp.use_count() = 4
local pointer in a thread:
  lp.get() = 0xc99028, lp.use_count() = 2
  Derived::~Derived()
  Base::~Base()
All threads completed, the last one deleted Derived

可以看到,我们对shared_ptr的赋值操作都会引起资源的共享而不是资源的拷贝或者其支配权的转移,所以,由上面的测试我们可以总结出几个shared_ptr的特点:

  • shared_ptr之间的拷贝构造和赋值都只会引起元对象将资源的支配权分享给新对象,而不是资源的拷贝或者其支配权的转移
  • shared_ptr可以知道当前使用这个资源的对象的个数
  • shared_ptr更改一个对象管理的资源,相当于更改所有使用此资源的shared_ptr对象管理的资源
  • 当最后一个管理此资源的对象也被删除了,那么这个资源也将被delete
  • 我们可以使用release()放弃对资源的控制权,并且将自身指针指向空

常用函数

函数介绍
shared_ptr<T>sp(T* res)将资源res交给sp来管理,资源不使用时会自动释放。
shared_ptr<T>sp = make_spared(params)params参数构造T类型的对象并且交给sp
pp可以作为条件判断,若p指向一个对象,则为true,否则为false。
*pshared_ptr<T>对象解引用,得到的是由p管理的T类型对象的引用。
p->mem等价于(*p).mem
p.get()返回p中保存的T*指针
swap(p,q)交换p,q各自内部的T*指针,注意p,q类型要相同
p.swap(q)同上
p = qp,q必须都是shared_ptr指针,并且各自管理的指针类型能相互转换。此操作会递减p的引用次数,递增q的引用次数;若p的引用次数变为0,则其管理的原内存会自动释放。
shared_ptr<T>sp(q)使得sp引用q所占资源。
p.reset() , p.reset(q)放弃p对资源的引用,若为唯一引用,还会释放内存。若传有参数q,则让p引用q所占资源。
p.use_count()返回与p共享的智能指针数量;可能很慢,主要用于调试。
p.unique()p.use_count()为1,返回true,否则返回false

4、std::weak_ptr

weak_ptr 是一种不控制所指向对象生命期的智能指针,它指向一个由 shared_ptr 管理的对象。将一个 weak_ptr 绑定到一个 shared_ptr 不会改变 shared_ptr 的引用次数。
一旦最后一个指向对象的shared_ptr 被销毁,对象就会被释放
即使这个时候存在 weak_ptr 指向了该对象,对象还是会被释放。所以我们可以把 weak_ptr 看作是一种”弱引用”


函数介绍
weak_ptr<T>wpweak_ptr 可以指向类型为T的对象
weak_ptr<T>wp(sp)与shared_ptr sp指向相同的对象,T必须可以转化为sp指向对象的类型。
w = pp 可以是weak_ptr或者shared_ptr。赋值后w,p共享对象。但是w不干涉对象声明周期。
w.reset()m置为空。
w.use_count()m共享资源的shared_ptr个数。
w.expired()w.use_count()为0,返回true,否则返回false。
w.lock()如果expiredtrue,返回一个空的shared_ptr;否则返回指向w对象的shared_ptr

以下是一个示例(如何通过锁来保证指针的有效性):

#include <iostream>
#include <memory>

std::weak_ptr<int> gw;

void f()
{
    if (auto spt = gw.lock()) 
    {    // 使用之前必须复制到 shared_ptr
        std::cout << *spt << "\n";
    }
    else 
    {
        std::cout << "gw is expired\n";
    }
}

int main()
{
    {
        auto sp = std::make_shared<int>(42);
        gw = sp;

        f();
    }

    f();
}

输出:

42
gw is expired

以下是std::weak_ptr的一些特性:

std::weak_ptr在访问所引用的对象前必须先转换为 std::shared_ptr
std::weak_ptr 用来表达临时所有权的概念:当某个对象只有存在时才需要被访问,而且随时可能被他人删除时,可以使用 std::weak_ptr 来跟踪该对象
需要获得临时所有权时,则将其转换为 std::shared_ptr,此时如果原来的 std::shared_ptr 被销毁,则该对象的生命期将被延长至这个临时的 std::shared_ptr 同样被销毁为止。
此外,std::weak_ptr 还可以用来避免 std::shared_ptr 的循环引用。

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值