【C++入门到精通】智能指针 shared_ptr 简介及C++模拟实现 [ C++入门 ]

简介: 【C++入门到精通】智能指针 shared_ptr 简介及C++模拟实现 [ C++入门 ]

引言

在 C++ 动态内存管理中,除了 auto_ptr 和 unique_ptr 之外,还有一种智能指针 shared_ptr,它可以让多个指针共享同一个动态资源,并且能够自动释放资源。shared_ptr 通过引用计数的方式来管理内存,能够避免程序中出现悬空指针和内存泄漏等问题。本文将介绍 shared_ptr 的简介和使用方法,并提供一个 C++ 模拟实现,以帮助读者更好地理解其原理和实现。

一、简介

std::shared_ptr 是 C++11 标准库中的一个智能指针,它可以让多个指针共享同一个动态资源,并且能够自动释放资源。shared_ptr 通过引用计数的方式来管理内存,能够避免程序中出现悬空指针和内存泄漏等问题

与 std::auto_ptr 和 std::unique_ptr 不同,std::shared_ptr 可以被多个指针所共享。当一个 shared_ptr 被赋值给另一个 shared_ptr 或者被拷贝构造时,它所管理的资源的引用计数会增加。只有在最后一个 shared_ptr 被销毁时,才会释放所管理的资源。这种语义被称为“共享所有权”。

🔴std::shared_ptr官方文档

二、成员函数

为了方便用户使用 shared_ptr 智能指针,shared_ptr<T> 模板类还提供有一些实用的成员方法,它们各自的功能如下表所示

成员方法名 功能
operator=() 重载赋值号,使得同一类型的 shared_ptr 智能指针可以相互赋值。
operator * () 重载 * 号,获取当前 shared_ptr 智能指针对象指向的数据。
operator->() 重载 -> 号,当智能指针指向的数据类型为自定义的结构体时,通过 -> 运算符可以获取其内部的指定成员。
swap() 交换 2 个相同类型 shared_ptr 智能指针的内容。
reset() 当函数没有实参时,该函数会使当前 shared_ptr 所指堆内存的引用计数减 1,同时将当前对象重置为一个空指针;当为函数传递一个新申请的堆内存时,则调用该函数的 shared_ptr 对象会获得该存储空间的所有权,并且引用计数的初始值为 1。
get() 获得 shared_ptr 对象内部包含的普通指针。
use_count() 返回同当前 shared_ptr 对象(包括它)指向相同的所有 shared_ptr 对象的数量。
unique() 判断当前 shared_ptr 对象指向的堆内存,是否不再有其它 shared_ptr 对象再指向它。
operator bool() 判断当前 shared_ptr 对象是否为空智能指针,如果是空指针,返回 false;反之,返回 true

⭕当然除此之外,C++11 标准还支持同一类型的 shared_ptr 对象,或者 shared_ptrnullptr 之间,进行 ==!=<<=>>= 运算。

三、使用示例

下面是一个使用 std::shared_ptr 的示例:

#include <iostream>
#include <memory>

int main() {
    std::shared_ptr<int> sp1(new int(42)); // 创建一个指向整数 42 的 shared_ptr
    std::shared_ptr<int> sp2 = sp1; // sp2 和 sp1 现在都指向同一个对象
    std::cout << *sp1 << " " << *sp2 << std::endl; // 输出结果为 42 42
    *sp1 = 10;
    std::cout << *sp1 << " " << *sp2 << std::endl; // 输出结果为 10 10
    sp1.reset(); // 释放 sp1 的所有权
    std::cout << *sp2 << std::endl; // 输出结果为 10
    sp2.reset(); // 释放 sp2 的所有权
    return 0;
}

在这个示例中,我们首先创建了一个指向整数 42 的 shared_ptr,然后将其赋值给另一个 shared_ptr。由于共享所有权的语义,它们都指向同一个对象。接着,我们修改了 sp1 指向的对象的值,然后释放了 sp1 的所有权,此时 sp2 仍然可以访问该对象。最后,我们释放了 sp2 的所有权,整个示例结束。

四、C++模拟实现

#include <iostream>
#include <mutex>

using namespace std;

template<class T>
class shared_ptr
{
public:
    // 构造函数
    explicit shared_ptr(T* ptr = nullptr)
        : _ptr(ptr)
        , _pcount(new int(1))
        , _pmtx(new mutex)
    {}

    // 析构函数
    ~shared_ptr()
    {
        Release();
    }

    /* 释放资源
    Release() 方法减少引用计数,并根据引用计数的值来判断是否需要删除指向的堆内存对象和引用计数对象。
    在操作之前,我们使用互斥量 _pmtx 进行加锁,以保证线程安全。*/
    void Release()
  {
    _pmtx->lock();
    bool deleteFlag = false;
    if (--(*_pcount) == 0)
    {
      if (_ptr)
      {
        // 删除器进行删除
        _del(_ptr);
      }

      delete _pcount;
      deleteFlag = true;
    }
    _pmtx->unlock();
    if (deleteFlag)
    {
      delete _pmtx;
    }
  }

    // 增加引用计数
    void AddCount()
    {
        _pmtx->lock();
        ++(*_pcount);
        _pmtx->unlock();
    }

    // 拷贝构造函数
    shared_ptr(const shared_ptr<T>& sp)
        : _ptr(sp._ptr)
        , _pcount(sp._pcount)
        , _pmtx(sp._pmtx)
    {
        AddCount();
    }

    // 赋值运算符重载
    shared_ptr<T>& operator=(const shared_ptr<T>& sp)
    {
        if (_ptr != sp._ptr)
        {
            Release();

            _ptr = sp._ptr;
            _pcount = sp._pcount;
            _pmtx = sp._pmtx;

            AddCount();
        }

        return *this;
    }

    // operator*() 重载
    T& operator*()
    {
        return *_ptr;
    }

    // operator->() 重载
    T* operator->()
    {
        return _ptr;
    }

    // get() 方法
    T* get()
    {
        return _ptr;
    }

    // use_count() 方法
    int use_count()
    {
        return *_pcount;
    }

    // swap() 方法,交换 2 个 shared_ptr 智能指针的内容
    void swap(shared_ptr<T>& sp) noexcept
    {
        std::swap(_ptr, sp._ptr);
        std::swap(_pcount, sp._pcount);
        std::swap(_pmtx, sp._pmtx);
    }

    // reset() 方法,重置 shared_ptr 智能指针对象
    void reset(T* ptr = nullptr)
    {
        // 释放原有资源
        Release();

        // 重新赋值
        _ptr = ptr;
        _pcount = new int(1);
        _pmtx = new mutex;
    }

private:
    T* _ptr;           // 指向堆内存对象的指针
    int* _pcount;      // 引用计数的指针
    mutex* _pmtx;      // 保护引用计数的互斥量
    
    // 包装器
  function<void(T*)> _del = [](T* ptr)
  {
    cout << "lambda delete:" << ptr << endl;
    delete ptr; 
  };
};

以上代码是一个简化版的 shared_ptr 智能指针模板类的实现。智能指针是 C++ 中的一个重要工具,可以帮助开发者更方便地管理动态内存。在手动管理内存时,很容易出现内存泄漏和悬垂指针等问题,而使用智能指针则可以自动管理对象的生命周期,避免这些问题。

该 shared_ptr 类实现了一个引用计数机制,它通过维护一个引用计数,来判断指向的堆内存对象是否应该被释放。当有多个 shared_ptr 指向同一个堆内存对象时,引用计数会增加;当 shared_ptr 对象销毁时,引用计数会减少。当引用计数为 0 时,就可以释放堆内存对象了。

🚨🚨注意:该 shared_ptr 类是一个简化版实现,可能存在一些问题,不适用于生产环境中。在实际开发中,我们可以使用标准库中的 shared_ptr 类或其他第三方库中的智能指针实现。

五、std::shared_ptr的线程安全问题

标准库中的 std::shared_ptr 是线程安全的,可以在多线程环境下使用。它通过使用原子操作和引用计数来实现线程安全

在 std::shared_ptr 内部,引用计数是一个原子操作,确保多个线程可以安全地对其进行增加和减少操作。当有一个新的 std::shared_ptr 指向同一块堆内存时,引用计数会增加;当某个 std::shared_ptr 对象销毁时,引用计数会减少。只有当引用计数为 0 时,才会释放堆内存。

此外,std::shared_ptr 还使用了原子操作来保证多个线程之间对智能指针对象的访问是互斥的。这意味着,在多线程环境下,不同的线程可以同时拷贝和赋值 std::shared_ptr 对象,而不会出现数据竞争的问题。

🚨🚨注意:虽然 std::shared_ptr 提供了线程安全的引用计数和访问控制,但它本身并不保证所指向的对象是线程安全的。如果多个线程同时访问和修改同一块内存,则需要额外的同步机制来确保线程安全性。

六、总结

shared_ptr 是C++中的智能指针类,通过引用计数机制管理堆内存对象的生命周期,并使用原子操作确保引用计数的线程安全性。它支持拷贝构造和赋值运算符重载,可以安全地共享指向同一块堆内存的对象。此外,shared_ptr提供了方便的访问和操作接口,是一种方便而安全的资源管理工具。

当然,从上述方面来看,shared_ptr 确实是一种非常强大的工具。然而,它也存在一个缺点,即“循环引用问题”。在下一篇文章中,我将介绍与之相关的知识点——weak_ptr。weak_ptr 是一种特殊的智能指针,用于解决 shared_ptr 循环引用可能导致的内存泄漏问题。通过引入weak_ptr,我们可以打破循环引用,避免内存泄漏,并在需要时安全地访问对象。敬请期待下一篇文章的发布!

温馨提示

感谢您对博主文章的关注与支持!另外,我计划在未来的更新中持续探讨与本文相关的内容,会为您带来更多关于C++以及编程技术问题的深入解析、应用案例和趣味玩法等。请继续关注博主的更新,不要错过任何精彩内容!

再次感谢您的支持和关注。期待与您建立更紧密的互动,共同探索C++、算法和编程的奥秘。祝您生活愉快,排便顺畅!

 

目录
相关文章
|
3月前
|
存储 安全 编译器
c++入门
c++作为面向对象的语言与c的简单区别:c语言作为面向过程的语言还是跟c++有很大的区别的,比如说一个简单的五子棋的实现对于c语言面向过程的设计思路是首先分析解决这个问题的步骤:(1)开始游戏(2)黑子先走(3)绘制画面(4)判断输赢(5)轮到白子(6)绘制画面(7)判断输赢(8)返回步骤(2) (9)输出最后结果。但对于c++就不一样了,在下五子棋的例子中,用面向对象的方法来解决的话,首先将整个五子棋游戏分为三个对象:(1)黑白双方,这两方的行为是一样的。(2)棋盘系统,负责绘制画面。
41 0
|
7月前
|
存储 缓存 C++
C++ 容器全面剖析:掌握 STL 的奥秘,从入门到高效编程
C++ 标准模板库(STL)提供了一组功能强大的容器类,用于存储和操作数据集合。不同的容器具有独特的特性和应用场景,因此选择合适的容器对于程序的性能和代码的可读性至关重要。对于刚接触 C++ 的开发者来说,了解这些容器的基础知识以及它们的特点是迈向高效编程的重要一步。本文将详细介绍 C++ 常用的容器,包括序列容器(`std::vector`、`std::array`、`std::list`、`std::deque`)、关联容器(`std::set`、`std::map`)和无序容器(`std::unordered_set`、`std::unordered_map`),全面解析它们的特点、用法
C++ 容器全面剖析:掌握 STL 的奥秘,从入门到高效编程
|
6月前
|
存储 分布式计算 编译器
C++入门基础2
本内容主要讲解C++中的引用、inline函数和nullptr。引用是变量的别名,与原变量共享内存,定义时需初始化且不可更改指向对象,适用于传参和返回值以提高效率;const引用可增强代码灵活性。Inline函数通过展开提高效率,但是否展开由编译器决定,不建议分离声明与定义。Nullptr用于指针赋空,取代C语言中的NULL。最后鼓励持续学习,精进技能,提升竞争力。
|
7月前
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
3月前
|
人工智能 机器人 编译器
c++模板初阶----函数模板与类模板
class 类模板名private://类内成员声明class Apublic:A(T val):a(val){}private:T a;return 0;运行结果:注意:类模板中的成员函数若是放在类外定义时,需要加模板参数列表。return 0;
78 0
|
3月前
|
存储 编译器 程序员
c++的类(附含explicit关键字,友元,内部类)
本文介绍了C++中类的核心概念与用法,涵盖封装、继承、多态三大特性。重点讲解了类的定义(`class`与`struct`)、访问限定符(`private`、`public`、`protected`)、类的作用域及成员函数的声明与定义分离。同时深入探讨了类的大小计算、`this`指针、默认成员函数(构造函数、析构函数、拷贝构造、赋值重载)以及运算符重载等内容。 文章还详细分析了`explicit`关键字的作用、静态成员(变量与函数)、友元(友元函数与友元类)的概念及其使用场景,并简要介绍了内部类的特性。
158 0
|
5月前
|
编译器 C++ 容器
【c++11】c++11新特性(上)(列表初始化、右值引用和移动语义、类的新默认成员函数、lambda表达式)
C++11为C++带来了革命性变化,引入了列表初始化、右值引用、移动语义、类的新默认成员函数和lambda表达式等特性。列表初始化统一了对象初始化方式,initializer_list简化了容器多元素初始化;右值引用和移动语义优化了资源管理,减少拷贝开销;类新增移动构造和移动赋值函数提升性能;lambda表达式提供匿名函数对象,增强代码简洁性和灵活性。这些特性共同推动了现代C++编程的发展,提升了开发效率与程序性能。
156 12
|
6月前
|
设计模式 安全 C++
【C++进阶】特殊类设计 && 单例模式
通过对特殊类设计和单例模式的深入探讨,我们可以更好地设计和实现复杂的C++程序。特殊类设计提高了代码的安全性和可维护性,而单例模式则确保类的唯一实例性和全局访问性。理解并掌握这些高级设计技巧,对于提升C++编程水平至关重要。
123 16
|
7月前
|
编译器 C语言 C++
类和对象的简述(c++篇)
类和对象的简述(c++篇)