栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > C/C++/C#

智能指针详解(C++)

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

智能指针详解(C++)

指针

指针其实是内存管理的一部分,它要解决的问题就是在什么地方存放什么数据。指针用于存储值的地址,*运算被称为间接运值或解除引用运算符。

int *p;
p = &a;	  //这种方式正确
printf("p = %dn",p);

变量p存放的a的地址。

指针的定义:

指针的意义:指针相当于物品的标签,别名,为了容易查找定位。

指针的使用:说白了就跟快递柜相似的作用。用来保存变量地址的的变量。

指针的痛点:在C++中创建指针时,计算机会分配用来存储地址的内存,但是不会分配用来存储指针所指向的数据的内存。因此需要为数据提供独立的空间。

内存管理

使用new运算符来分配内存,然后可以使用指针来访问,使用结束后需要使用delete运算符释放掉申请的内存空间。对空指针应用delete是安全的。

int *pn = new int;   //为pn分配int型的内存空间
*pn = 1001; //存储一个具体的数值
  
  int *pm = new int(3);
//也可单独赋值,*p = 3;
delete pm;

int jugs = 5;
int *pi = &jugs;
delete pi; //该操作是错误的,memory not allocated by new;

//new数组
int *q = new int[3];
for(int i=0; i<3; i++)
  q[i]=i;
return q;
delete [ ] q;
进一步探究:new申请的地址空间在哪一块上?

new从被称为堆的自由存储区的内存区分配内存。经常用new来创建动态数组。

而常规变量则是存储在栈的内存区域中。

在 C 语言中,内存分配方式不外乎有如下三种形式:

  1. 从静态存储区域分配:它是由编译器自动分配和释放的,即内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在,直到整个程序运行结束时才被释放,如全局变量与 static 变量。
  2. 在栈上分配:它同样也是由编译器自动分配和释放的,即在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元将被自动释放。需要注意的是,栈内存分配运算内置于处理器的指令集中,它的运行效率一般很高,但是分配的内存容量有限。
  3. 从堆上分配:也被称为动态内存分配,它是由程序员手动完成申请和释放的。即程序在运行的时候由程序员使用内存分配函数(如 malloc 函数)来申请任意多少的内存,使用完之后再由程序员自己负责使用内存释放函数(如 free 函数)来释放内存。也就是说,动态内存的整个生存期是由程序员自己决定的,使用非常灵活。需要注意的是,如果在堆上分配了内存空间,就必须及时释放它,否则将会导致运行的程序出现内存泄漏等错误。
智能指针

提出背景:避免内存泄漏问题,主要用于管理在堆上的分配的内存。

什么是智能指针:将普通的指针封装为一个栈对象,当栈对象的生存周期结束后,会在析构函数中释放掉申请的内存,从而防止内存泄漏。

包括4种智能指针:

unique_ptr:

std::unique_ptr对其持有的堆内存具有唯一拥有权,也就是不可以拷贝或者复制给其他对象,当std::unique_ptr对象销毁时会释放掉其持有的堆内存。

对其初始化如下:

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

扩展:如何对unique_ptr对象转移非另一个对象?使用移动构造。

代码如下:

注:

令很多人对 C++11 规范不满的地方是,C++11 新增了 std::make_shared() 方法创建一个 std::shared_ptr 对象,却没有提供相应的 std::make_unique() 方法创建一个 std::unique_ptr 对象,这个方法直到 C++14 才被添加进来。当然,在 C++11 中你很容易实现出这样一个方法来:

int main()
{
    std::unique_ptr up1(std::make_unique(123));
    std::unique_ptr up2(std::move(up1));  //通过移动实现了复制操作
    std::cout << ((up1.get() == nullptr) ? "up1 is NULL" : "up1 is not NULL") << std::endl;
    
    std::unique_ptr 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 正好实现了这二者

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 引用的对象是安全的)。std::shared_ptr 提供了一个 use_count() 方法来获取当前持有资源的引用计数。除了上面描述的,std::shared_ptr 用法和 std::unique_ptr 基本相同。

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

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

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

    return 0;
}

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

#include 
#include 

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

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

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

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

        //初始化方式2
        std::shared_ptr 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 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
  //sp1 出了其作用域被析构,在其析构时递减资源 A 的引用计数至 0,并析构资源 A 对象,因此类 A 的析构函数被调用。
A destructor

std::shared_ptr 有几个常用函数如下:

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()

Weak_ptr:

背景:

std::weak_ptr 是一个不控制资源生命周期的智能指针,是对对象的一种弱引用,只是提供了对其管理的资源的一个访问手段,引入它的目的为协助 std::shared_ptr 工作。

怎么使用:

std::weak_ptr可以从一个 std::shared_ptr 或另一个std::weak_ptr 对象构造,std::shared_ptr 可以直接赋值给 std::weak_ptr ,也可以通过 std::weak_ptr 的 lock() 函数来获得 std::shared_ptr。它的构造和析构不会引起引用计数的增加或减少。std::weak_ptr 可用来解决 std::shared_ptr 相互引用时的死锁问题(即两个std::shared_ptr 相互引用,那么这两个指针的引用计数永远不可能下降为 0, 资源永远不会释放)。

示例代码:

#include 
#include 

int main()
{
    //创建一个std::shared_ptr对象
    std::shared_ptr sp1(new int(123));
    std::cout << "use count: " << sp1.use_count() << std::endl;

    //通过构造函数得到一个std::weak_ptr对象
    std::weak_ptr sp2(sp1);
    std::cout << "use count: " << sp1.use_count() << std::endl;

    //通过赋值运算符得到一个std::weak_ptr对象
    std::weak_ptr sp3 = sp1;
    std::cout << "use count: " << sp1.use_count() << std::endl;

    //通过一个std::weak_ptr对象得到另外一个std::weak_ptr对象
    std::weak_ptr sp4 = sp2;
    std::cout << "use count: " << sp1.use_count() << std::endl;

    return 0;
}

运行结果:

use count: 1
use count: 1
use count: 1
use count: 1

有什么特点:

无论通过何种方式创建 std::weak_ptr 都不会增加资源的引用计数,因此每次输出引用计数的值都是 1。

既然,std::weak_ptr 不管理对象的生命周期,那么其引用的对象可能在某个时刻被销毁了,如何得知呢?std::weak_ptr 提供了一个 expired() 方法来做这一项检测,返回 true,说明其引用的资源已经不存在了;返回 false,说明该资源仍然存在,这个时候可以使用 std::weak_ptr 的 lock() 方法得到一个 std::shared_ptr 对象然后继续操作资源,以下代码演示了该用法:

// tmpConn_ 是一个 std::weak_ptr 对象
// tmpConn_ 引用的TcpConnection已经销毁,直接返回
if (tmpConn_.expired())
    return;

std::shared_ptr conn = tmpConn_.lock();
if (conn)
{
    //对conn进行操作,省略...
}

既然使用了 std::weak_ptr 的 expired() 方法判断了对象是否存在,为什么不直接使用 std::weak_ptr 对象对引用资源进行操作呢?实际上这是行不通的,std::weak_ptr 类没有重写 operator-> 和 operator* 方法,因此不能像 std::shared_ptr 或 std::unique_ptr 一样直接操作对象,同时 std::weak_ptr 类也没有重写 operator! 操作,因此也不能通过 std::weak_ptr 对象直接判断其引用的资源是否存在。

因此,std::weak_ptr 的正确使用场景是那些资源如果可能就使用,如果不可使用则不用的场景,它不参与资源的生命周期管理。

std::weak_ptr 的应用场景,经典的例子是订阅者模式或者观察者模式中。这里以订阅者为例来说明,消息发布器只有在某个订阅者存在的情况下才会向其发布消息,而不能管理订阅者的生命周期。

class Subscriber
{
};

class SubscribeManager
{
public:
    void publish()
    {
        for (const auto &iter : m_subscribers)
        {
            if (!iter.expired())
            {
                //TODO:给订阅者发送消息
            }
        }
    }

private:
    std::vector> m_subscribers;
};

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 lock()

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

一旦智能指针对象接管了你的资源,所有对资源的操作都应该通过智能指针对象进行,不建议再通过原始指针进行操作了。

当然,除了 std::weak_ptr 之外,std::unique_ptr 和 std::shared_ptr 都提供了获取原始指针的方法——get() 函数。

我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号