C++ 11新特性之shared_ptr

简介: C++ 11新特性之shared_ptr

概述

在C++ 11标准中,智能指针作为一种自动资源管理工具被引入,极大地提升了代码的健壮性和安全性。其中,std::shared_ptr作为多所有权智能指针,凭借其独特的引用计数机制和内存自动释放功能,成为现代C++开发中的重要组件。

std::shared_ptr是C++标准库提供的智能指针类型之一,它通过引用计数技术来追踪指向动态分配内存对象的所有者数量。当最后一个所有者(即最后一个std::shared_ptr实例)销毁时,其所指向的动态分配内存也会随之释放,有效地避免了内存泄漏问题。

shared_ptr的核心特性

1、共享所有权。

一个std::shared_ptr实例可以被复制或移动到另一个std::shared_ptr实例,复制后两者会共享同一份资源,并且都参与到引用计数的维护中。

2、弱引用。

C++标准库还提供了std::weak_ptr,它是一种弱引用,不会增加引用计数,用于解决循环引用导致的对象无法释放的问题。

3、自定义删除器。

std::shared_ptr允许指定自定义的删除器,在资源不再需要时执行特定的清理操作。

4、原子性保证。

在多线程环境下,引用计数的增减操作是原子性的,确保了线程安全。

shared_ptr的接口

shared_ptr是一个引用计数智能指针,用于共享对象的所有权。它可以从一个裸指针、另一个shared_ptr、一个auto_ptr、或者一个weak_ptr构造。还可以传递第二个参数给shared_ptr的构造函数,它被称为删除器。删除器用于处理共享资源的释放,这对于管理那些不是用new分配也不是用delete释放的资源时非常有用。shared_ptr被创建后,就可以像普通指针一样使用了,除了一点,它不能被显式地删除。

shared_ptr一些重要的接口如下。

template <class T>
explicit shared_ptr(T* p);


这个构造函数获得给定指针p的所有权,参数p必须是指向T的有效指针。构造后引用计数设为1,唯一从这个构造函数抛出的异常是std::bad_alloc(仅在一种很罕见的情况下发生,即不能获得引用计数器所需的空间)。

template <class T,class D>
shared_ptr(T* p,D d);


这个构造函数带有两个参数。第一个是shared_ptr将要获得所有权的那个资源,第二个是shared_ptr被销毁时负责释放资源的一个对象,被保存的资源将以d(p)的形式传给那个对象。如果引用计数器不能分配成功,shared_ptr抛出一个类型为std::bad_alloc的异常。

shared_ptr(const shared_ptr& r);
1.

r中保存的资源被新构造的shared_ptr所共享,引用计数加一。这个构造函数不会抛出异常。

template <class T>
explicit shared_ptr(const weak_ptr<T>& r);


从一个weak_ptr构造shared_ptr。这使得weak_ptr的使用具有线程安全性,因为指向weak_ptr参数的共享资源的引用计数将会自增(weak_ptr不影响共享资源的引用计数)。如果weak_ptr为空(r.use_count()==0), shared_ptr抛出一个类型为bad_weak_ptr的异常。

template <typename T>
shared_ptr(auto_ptr<T>& r);

这个构造函数从一个auto_ptr获取r中保存的指针的所有权,方法是保存指针的一份拷贝并对auto_ptr调用release。构造后的引用计数为1,而r则变为空的。如果引用计数器不能分配成功,则抛出std::bad_alloc。

~shared_ptr();
1.

shared_ptr析构函数,对引用计数减一。如果计数为零,则保存的指针被删除。删除指针的方法是调用operator delete,或者,如果给定了一个执行删除操作的删除器对象,就把保存的指针作为唯一参数调用这个对象。析构函数不会抛出异常。

shared_ptr& operator=(const shared_ptr& r);
1.

赋值操作共享r中的资源,并停止对原有资源的共享。赋值操作不会抛出异常。

void reset();
1.

reset函数用于停止对保存指针的所有权的共享。共享资源的引用计数减一。

T& operator*() const;
1.

这个操作符返回对已存指针所指向的对象的一个引用。如果指针为空,调用operator*会导致未定义行为。这个操作符不会抛出异常。

T* operator->() const;
1.

这个操作符返回保存的指针。这个操作符与operator*一起使得智能指针看起来象普通指针。这个操作符不会抛出异常。

T* get() const;
1.

get函数是当保存的指针有可能为空时(这时 operator* 和 operator-> 都会导致未定义行为)获取它的最好办法。注意,你也可以使用隐式布尔类型转换来测试shared_ptr是否包含有效指针。这个函数不会抛出异常。

bool unique() const;
1.

这个函数在shared_ptr是它所保存指针的唯一拥有者时返回true;否则返回false。 unique不会抛出异常。

long use_count() const;
1.

use_count函数返回指针的引用计数。它在调试的时候特别有用,因为它可以在程序执行的关键点获得引用计数的快照。小心地使用它,因为在某些可能的shared_ptr实现中,计算引用计数可能是昂贵的,甚至是不行的。这个函数不会抛出异常。

void swap(shared_ptr<T>& b);
1.

这可以很方便地交换两个shared_ptr。swap函数交换保存的指针(以及它们的引用计数)。这个函数不会抛出异常。

template <typename T,typename U>  shared_ptr<T> static_pointer_cast(const shared_ptr<U>& r);
1.

要对保存在shared_ptr里的指针执行static_cast,我们可以取出指针然后强制转换它,但我们不能把它存到另一个shared_ptr里;新的shared_ptr会认为它是第一个管理这些资源的。解决的方法是用static_pointer_cast,使用这个函数可以确保被指对象的引用计数保持正确。static_pointer_cast不会抛出异常。

shared_ptr的使用

使用shared_ptr的示例代码可参考如下。

#include <iostream>
#include <assert.h>
using namespace std;
int main()
{
    shared_ptr<int> pInt1;
    // 还没有引用指针
    assert(pInt1.use_count() == 0);
    {
        shared_ptr<int> pInt2(new int(66));
        // new int(66)这个指针被引用1次
        assert(pInt2.use_count() == 1);
        pInt1 = pInt2;
        // new int(66)这个指针被引用2次
        assert(pInt2.use_count() == 2);
        assert(pInt1.use_count() == 2);
    }
    //pInt2离开作用域, 所以new int(66)被引用次数-1
    assert(pInt1.use_count() == 1);
    return 0;
}

如果资源的创建销毁不是以new和delete的方式进行的,该怎么办呢?通过前面的接口可以看到,shared_ptr的构造函数中可以指定删除器。具体如何使用,可参考下面的示例代码。

#include <iostream>
using namespace std;
class CFileCloser
{
public:
    void operator()(FILE *pFile)
    {
        if (pFile != NULL)
        {
            fclose(pFile);
            pFile = NULL;
        }
    }
};
int main()
{
    shared_ptr<FILE> fp(fopen("C:\\1.txt", "r"), CFileCloser());
    return 0;
}


在使用shared_ptr时,需要避免同一个对象指针被两次当成shard_ptr构造函数里的参数的情况。考虑下面的示例代码。

{
        int *pInt = new int(66);
        shared_ptr<int> pTemp1(pInt);
        assert(pTemp1.use_count() == 1);
        shared_ptr<int> pTemp2(pInt);
        assert(pTemp2.use_count() == 1);
    }
    // pTemp1和pTemp2离开作用域,都销毁pInt,会导致两次释放同一块内存


正确的做法是:将原始指针赋给智能指针后,以后的操作都要针对智能指针了。

{
        shared_ptr<int> pTemp1(new int(66));
        assert(pTemp1.use_count() == 1);
        shared_ptr<int> pTemp2(pTemp1);
        assert(pTemp2.use_count() == 2);
    }
    // pTemp1和pTemp2离开作用域,引用次数变为0,指针被销毁。


另外,在多线程中使用shared_ptr时,如果存在拷贝或赋值操作,可能会由于同时访问引用计数而导致计数无效。解决方法是:向每个线程中传递公共的week_ptr,线程中需要使用shared_ptr时,将week_ptr转换成shared_ptr即可。

注意事项

1、初始化与赋值。

可以通过构造函数直接初始化,也可以通过成员函数reset()或赋值运算符改变所指向的对象。

2、循环引用。

需要注意的是,两个std::shared_ptr互相引用对方可能会形成循环引用,从而导致引用计数永远不为0,使得资源无法释放。此时,应合理使用std::weak_ptr打破循环引用。

3、性能开销。

虽然std::shared_ptr带来了便利,但每次增删引用都会更新引用计数,带来一定的性能开销。因此,对于频繁创建销毁的小对象或者单个所有者的场景,可能更适合使用std::unique_ptr。

4、异常安全。

std::shared_ptr具有异常安全保证,即使在构造或赋值过程中抛出异常,也不会造成内存泄漏。

总结

std::shared_ptr在C++程序设计中扮演着至关重要的角色,它的出现极大地简化了内存管理任务,增强了代码的安全性和可靠性。然而,正如任何工具一样,理解和正确使用std::shared_ptr才能发挥其最大价值。开发者需时刻关注潜在的循环引用问题以及性能开销,从而在实际项目中做到恰如其分地运用这一强大工具。


相关文章
|
12天前
|
弹性计算 人工智能 架构师
阿里云携手Altair共拓云上工业仿真新机遇
2024年9月12日,「2024 Altair 技术大会杭州站」成功召开,阿里云弹性计算产品运营与生态负责人何川,与Altair中国技术总监赵阳在会上联合发布了最新的“云上CAE一体机”。
阿里云携手Altair共拓云上工业仿真新机遇
|
8天前
|
机器学习/深度学习 算法 大数据
【BetterBench博士】2024 “华为杯”第二十一届中国研究生数学建模竞赛 选题分析
2024“华为杯”数学建模竞赛,对ABCDEF每个题进行详细的分析,涵盖风电场功率优化、WLAN网络吞吐量、磁性元件损耗建模、地理环境问题、高速公路应急车道启用和X射线脉冲星建模等多领域问题,解析了问题类型、专业和技能的需要。
2522 18
【BetterBench博士】2024 “华为杯”第二十一届中国研究生数学建模竞赛 选题分析
|
8天前
|
机器学习/深度学习 算法 数据可视化
【BetterBench博士】2024年中国研究生数学建模竞赛 C题:数据驱动下磁性元件的磁芯损耗建模 问题分析、数学模型、python 代码
2024年中国研究生数学建模竞赛C题聚焦磁性元件磁芯损耗建模。题目背景介绍了电能变换技术的发展与应用,强调磁性元件在功率变换器中的重要性。磁芯损耗受多种因素影响,现有模型难以精确预测。题目要求通过数据分析建立高精度磁芯损耗模型。具体任务包括励磁波形分类、修正斯坦麦茨方程、分析影响因素、构建预测模型及优化设计条件。涉及数据预处理、特征提取、机器学习及优化算法等技术。适合电气、材料、计算机等多个专业学生参与。
1525 15
【BetterBench博士】2024年中国研究生数学建模竞赛 C题:数据驱动下磁性元件的磁芯损耗建模 问题分析、数学模型、python 代码
|
4天前
|
存储 关系型数据库 分布式数据库
GraphRAG:基于PolarDB+通义千问+LangChain的知识图谱+大模型最佳实践
本文介绍了如何使用PolarDB、通义千问和LangChain搭建GraphRAG系统,结合知识图谱和向量检索提升问答质量。通过实例展示了单独使用向量检索和图检索的局限性,并通过图+向量联合搜索增强了问答准确性。PolarDB支持AGE图引擎和pgvector插件,实现图数据和向量数据的统一存储与检索,提升了RAG系统的性能和效果。
|
10天前
|
编解码 JSON 自然语言处理
通义千问重磅开源Qwen2.5,性能超越Llama
击败Meta,阿里Qwen2.5再登全球开源大模型王座
595 14
|
1月前
|
运维 Cloud Native Devops
一线实战:运维人少,我们从 0 到 1 实践 DevOps 和云原生
上海经证科技有限公司为有效推进软件项目管理和开发工作,选择了阿里云云效作为 DevOps 解决方案。通过云效,实现了从 0 开始,到现在近百个微服务、数百条流水线与应用交付的全面覆盖,有效支撑了敏捷开发流程。
19283 30
|
10天前
|
人工智能 自动驾驶 机器人
吴泳铭:AI最大的想象力不在手机屏幕,而是改变物理世界
过去22个月,AI发展速度超过任何历史时期,但我们依然还处于AGI变革的早期。生成式AI最大的想象力,绝不是在手机屏幕上做一两个新的超级app,而是接管数字世界,改变物理世界。
497 49
吴泳铭:AI最大的想象力不在手机屏幕,而是改变物理世界
|
1月前
|
人工智能 自然语言处理 搜索推荐
阿里云Elasticsearch AI搜索实践
本文介绍了阿里云 Elasticsearch 在AI 搜索方面的技术实践与探索。
18842 20
|
1月前
|
Rust Apache 对象存储
Apache Paimon V0.9最新进展
Apache Paimon V0.9 版本即将发布,此版本带来了多项新特性并解决了关键挑战。Paimon自2022年从Flink社区诞生以来迅速成长,已成为Apache顶级项目,并广泛应用于阿里集团内外的多家企业。
17530 13
Apache Paimon V0.9最新进展
|
3天前
|
云安全 存储 运维
叮咚!您有一份六大必做安全操作清单,请查收
云安全态势管理(CSPM)开启免费试用
367 4
叮咚!您有一份六大必做安全操作清单,请查收