C++11智能指针(一)

简介: C++11智能指针

一、智能指针的初步认识

1.1 使用场景

使用智能指针是解决内存泄露问题的良好手段

int Div()
{
  int a, b;
  cin >> a >> b;
  if (b == 0)
    throw invalid_argument("除0错误");
  return a / b;
}
void func()
{
  int* ptr = new int;
  //...
  cout << Div() << endl;
  //...
  delete ptr;
}
int main()
{
  try
  {
    func();
  }
  catch (exception& e)
  {
    cout << e.what() << endl;
  }
  return 0;
}


执行上述代码时,若用户输入的除数为0,那么Div()函数中就会抛出异常,这时程序的执行流会直接跳转到主函数中的catch块中执行,最终导致func()函数中申请的内存资源没有得到释放


利用异常的重新捕获解决


对于这种情况,可以在func()函数中先对Div()函数中抛出的异常进行捕获,捕获后先将之前申请的内存资源释放,然后再将异常重新抛出

int Div()
{
  int a, b;
  cin >> a >> b;
  if (b == 0)
    throw invalid_argument("除0错误");
  return a / b;
}
void func()
{
  int* ptr = new int;
  try
  {
    cout << Div() << endl;
  }
  catch (...)
  {
    delete ptr;
    throw;
  }
  delete ptr;
}
int main()
{
  try
  {
    func();
  }
  catch (exception& e)
  {
    cout << e.what() << endl;
  }
  return 0;
}

但这种方式并完全不可靠,有时可能会疏忽一些异常情况


利用智能指针解决

#include <iostream>
using namespace std;
template<class T>
class SmartPtr
{
public:
  SmartPtr(T* ptr) :_ptr(ptr) {}
  ~SmartPtr(){
    cout << "delete:" << _ptr << endl;
    delete _ptr;
  }
  T& operator*() { return *_ptr; }
  T* operator->() { return _ptr; }
private:
  T* _ptr;
};
int Div(){
  int a, b;
  cin >> a >> b;
  if (b == 0) throw invalid_argument("除0错误");
  return a / b;
}
void Func()
{
  SmartPtr<int> sp1(new int);//是否抛异常都会释放
  SmartPtr<int> sp2(new int);
  *sp1 = 0;
  *sp2 = 2;
  cout << Div() << endl;
}
int main()
{
  try {
    Func();
  }
  catch (exception& e) {
    cout << e.what() << endl;
  }
  return 0;
}


代码中将申请到的内存空间交给了一个SmartPtr对象进行管理


在构造SmartPtr对象时,SmartPtr将传入的需要被管理的内存空间的地址保存起来

在SmartPtr对象析构时,SmartPtr的析构函数中会自动将管理的内存空间进行释放

为了让SmartPtr对象能够像原生指针一样使用,还需要对*和->运算符进行重载

无论程序是正常执行完毕返回了,还是因为某些原因中途返回了,或是因为抛异常返回了,只要SmartPtr对象的生命周期结束就会调用其对应的析构函数,进而完成内存资源的释放


1.2 原理

实现智能指针时需要考虑以下三个方面的问题:


在对象构造时获取资源,在对象析构的时候释放资源,利用对象的生命周期来控制程序资源,即RAII特性

对*和->运算符进行重载,使得该对象具有像指针一样的行为

智能指针对象的拷贝问题

概念说明: RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、互斥量等等)的简单技术


智能指针对象的拷贝问题


对于当前实现的SmartPtr类,若用一个SmartPtr对象来拷贝构造另一个SmartPtr对象,或是将一个SmartPtr对象赋值给另一个SmartPtr对象,都会导致程序崩溃


int main()
{
  SmartPtr<int> sp1(new int);
  SmartPtr<int> sp2(sp1); //拷贝构造
  SmartPtr<int> sp3(new int);
  SmartPtr<int> sp4(new int);
  sp3 = sp4; //拷贝赋值
  return 0;
}

原因如下:


编译器默认生成的拷贝构造函数对内置类型完成值拷贝(浅拷贝),因此用sp1拷贝构造sp2后,相当于这sp1和sp2管理了同一块内存空间,当sp1和sp2析构时就会导致这块空间被释放两次

编译器默认生成的拷贝赋值函数对内置类型也是完成值拷贝(浅拷贝),因此将sp4赋值给sp3后,相当于sp3和sp4管理的都是原来sp3管理的空间,当sp3和sp4析构时就会导致这块空间被释放两次,并且还会导致sp4原来管理的空间没有得到释放

智能指针就是要模拟原生指针的行为,当将一个指针赋值给另一个指针时,目的就是让这两个指针指向同一块内存空间,所以这里就应该进行浅拷贝。但单纯的浅拷贝又会导致空间被多次释放,因此根据解决智能指针拷贝问题方式的不同,从而衍生出了不同版本的智能指针


二、std::auto_ptr

2.1 管理权转移

auto_ptr是C++98中引入的智能指针,auto_ptr通过管理权转移的方式解决智能指针的拷贝问题,保证一个资源只有一个智能指针对象在对其进行管理,同一个资源就不会被多次释放了

#include <iostream>
using namespace std;
int main()
{
  std::auto_ptr<int> ap1(new int(1));
  std::auto_ptr<int> ap2(ap1);
  *ap2 = 10;
  //*ap1 = 20; //error
  std::auto_ptr<int> ap3(new int(1));
  std::auto_ptr<int> ap4(new int(2));
  ap3 = ap4;
  cout << *ap3 << endl;//2
  //cout << *ap4 << endl;//error
  return 0;
}

但使用管理权转移的方式来解决问题并不优秀。对象的管理权转移后也就意味着,不能再用该对象对原来管理的资源进行访问了,否则程序就会崩溃


使用auto_ptr之前必须先了解其机制,否则程序极易出问题,很多公司也规定禁止使用auto_ptr


2.2 auto_ptr的模拟实现

在构造函数中获取资源,在析构函数中释放资源,利用对象的生命周期来控制资源

对*和->运算符进行重载,使auto_ptr对象具有指针一样的行为

在拷贝构造函数中,用传入对象管理的资源来构造当前对象,将传入对象管理资源的指针置空

在拷贝赋值函数中,将当前对象管理的资源释放,然后再接管传入对象管理的资源,最后将传入对象管理资源的指针置空

namespace bjy
{
  template<class T>
  class auto_ptr
  {
  public:
    auto_ptr(T* ptr = nullptr):_ptr(ptr) {}
    ~auto_ptr() {
      if (_ptr != nullptr) {
        delete _ptr;
        _ptr = nullptr;
      }
    }
    auto_ptr(auto_ptr<T>& ap) {
      _ptr = ap._ptr;
      ap._ptr = nullptr;
    }
    auto_ptr& operator=(auto_ptr<T>& ap) {
      if (this != &ap) {
        delete _ptr;
        _ptr = ap._ptr;
        ap._ptr = nullptr;
      }
      return *this;
    }
    T& operator*() { return *_ptr; }
    T* operator->() { return _ptr; }
  private:
    T* _ptr;//指向所管理的资源
  };
}

三、std::unique_ptr

unique_ptr是C++11中引入的智能指针,unique_ptr通过防拷贝的方式解决智能指针的拷贝问题,ji即简单粗暴的防止对智能指针对象进行拷贝,保证资源不会被多次释放


int main()
{
  std::unique_ptr<int> up1(new int(10));
  //std::unique_ptr<int> up2(up1); //error
  return 0;
}

但防拷贝其实也不是一个很好的办法,总有一些场景需要进行拷贝


unique_ptr的模拟实现


在构造函数中获取资源,在析构函数中释放资源,利用对象的生命周期来控制资源

对 * 和 -> 运算符进行重载,使unique_ptr对象具有指针一样的行为

用C++98的方式将拷贝构造函数和拷贝赋值函数声明为私有,或者用C++11的方式=delete,防止外部调用

namespace bjy
{
  template<class T>
  class unique_ptr
  {
  public:
    unique_ptr(T* ptr = nullptr) :_ptr(ptr) {}
    ~unique_ptr() {
      if (_ptr != nullptr) {
        delete _ptr;
        _ptr = nullptr;
      }
    }
    T& operator*() { return *_ptr; }
    T* operator->() { return _ptr; }
    //防拷贝
    unique_ptr(unique_ptr<T>& ap) = delete;
    unique_ptr& operator=(unique_ptr<T>& ap) = delete;
  private:
    T* _ptr;//指向所管理的资源
  };
}

四、std::shared_ptr

4.1 基础设计

shared_ptr是C++11中引入的智能指针,shared_ptr通过引用计数的方式解决智能指针的拷贝问题


每一个被管理的资源都有一个对应的引用计数,通过这个引用计数记录着当前有多少个对象在管理着这块资源

当新增一个对象管理这块资源时则将该资源对应的引用计数进行++,当一个对象不再管理这块资源或该对象被析构时则将该资源对应的引用计数进行--

当一个资源的引用计数减为0时说明已经没有对象在管理这块资源了,这时就可以将该资源进行释放

通过引用计数的方式就能支持多个对象一起管理某一个资源,即支持了智能指针的拷贝,并且只有当资源对应的引用计数减为0时才会释放资源,保证了同一个资源不会被释放多次

#include <iostream>
int main()
{
  std::shared_ptr<int> sp1(new int(1));
  std::shared_ptr<int> sp2(sp1);
  *sp1 = 10;
  *sp2 = 20;
  std::cout << sp1.use_count() << std::endl; //2
  std::shared_ptr<int> sp3(new int(1));
  std::shared_ptr<int> sp4(new int(2));
  sp3 = sp4;
  std::cout << *sp3 << std::endl;//2
  std::cout << sp3.use_count() << std::endl; //2
  return 0;
}

注意: use_count()成员函数,用于获取当前对象管理的资源对应的引用计数


shared_ptr的模拟实现


在shared_ptr类中增加一个成员变量count,表示智能指针对象管理的资源对应的引用计数

在构造函数中获取资源,并将该资源对应的引用计数设置为1,表示当前有一个对象在管理该资源

在拷贝构造函数中,与传入对象一起管理它管理的资源,同时将该资源对应的引用计数++

在拷贝赋值函数中,先将当前对象管理的资源对应的引用计数--(若减为0则需要释放),然后再与传入对象一起管理它管理的资源,同时需要将该资源对应的引用计数++

在析构函数中,将管理资源对应的引用计数--,若减为0则需要将该资源释放

对 * 和 -> 运算符进行重载,使shared_ptr对象具有指针一样的行为

namespace bjy
{
  template<class T>
  class shared_ptr
  {
  public:
    shared_ptr(T* ptr = nullptr):_ptr(ptr),_pCount(new size_t(1)) {}
    ~shared_ptr() 
    {
      if (--(*_pCount) == 0) 
      {
        if (_ptr != nullptr) {//shared_ptr可能管理的是0地址处的空间
          delete _ptr;
          _ptr = nullptr;
        }
        delete _pCount;
        _pCount = nullptr;
      }
    }
    shared_ptr(shared_ptr<T>& sp):_ptr(sp._ptr),_pCount(sp._pCount) {
      ++(*_pCount);
    }
    shared_ptr<T>& operator=(shared_ptr<T>& sp) {
      if (_ptr != sp._ptr)
      {
        if (--(*_pCount) == 0) {//若引用计数为0,则释放该对象
          delete _ptr;
          delete _pCount;
        }
        _ptr = sp._ptr;
        _pCount = sp._pCount;
        ++(*_pCount);
      }
      return *this;
    }
    size_t GetCount() { return *_pCount; }
    T& operator*() { return *_ptr; }
    T* operator->() { return _ptr; };
  private:
    T* _ptr;
    size_t* _pCount;
  };
}


为什么引用计数需要存放在堆区?


shared_ptr中的引用计数不能单纯的定义成一个整型类型的成员变量,否则每个shared_ptr对象都有各自的引用计数,而当多个对象要管理同一个资源时,这些对象应该用的是同一个引用计数

31ce60d6270e4d2e85f4a3f98a2bc4d6.png



shared_ptr中的引用计数也不能定义成静态成员变量,因为静态成员变量是所有类型对象共享的,会导致管理相同资源的对象和管理不同资源的对象用到的都是同一个引用计数

fcd8dcb139ac4da5b70c33a312adee09.png



若将shared_ptr中的引用计数定义成一个指针,当资源第一次被管理时就在堆区开辟一块空间用于存储其对应的引用计数,若有其他对象也想要管理这个资源,那么除了需要这个资源的地址之外,还需要引用计数的地址 。此时管理同一个资源的多个对象访问到的就是同一个引用计数,而管理不同资源的对象访问到的就是不同的引用计数,相当于将各个资源与其对应的引用计数进行了绑定


1f1559ef4c7044d6ad51249febdf32a0.png


注意:由于引用计数的内存空间也是在堆上开辟的,因此当资源对应的引用计数减为0时,除了需要将该资源释放,还需要将该资源对应的引用计数的内存空间进行释放


目录
相关文章
|
18天前
|
存储 安全 C++
C++中的引用和指针:区别与应用
引用和指针在C++中都有其独特的优势和应用场景。引用更适合简洁、安全的代码,而指针提供了更大的灵活性和动态内存管理的能力。在实际编程中,根据需求选择适当的类型,能够编写出高效、可维护的代码。理解并正确使用这两种类型,是掌握C++编程的关键一步。
21 1
|
18天前
|
数据采集 存储 编译器
this指针如何使C++成员指针可调用
本文介绍了C++中的this指针,它是一个隐藏的指针,用于在成员函数中访问对象实例的成员。文章通过代码示例阐述了this指针的工作原理,以及如何使用指向成员变量和成员函数的指针。此外,还提供了一个多线程爬虫示例,展示this指针如何使成员指针在对象实例上调用,同时利用代理IP和多线程提升爬取效率。
this指针如何使C++成员指针可调用
|
5天前
|
存储 安全 编译器
【C++航海王:追寻罗杰的编程之路】引用、内联、auto关键字、基于范围的for、指针空值nullptr
【C++航海王:追寻罗杰的编程之路】引用、内联、auto关键字、基于范围的for、指针空值nullptr
32 5
|
4天前
|
C++ 容器
【编程技巧】 C++11智能指针
C++11引入了智能指针以自动管理内存,防止内存泄漏和悬挂指针: - `shared_ptr`:引用计数,多所有权,适用于多个对象共享资源。 - `unique_ptr`:独占所有权,更轻量级,适用于单一对象所有者。 - `weak_ptr`:弱引用,不增加引用计数,解决`shared_ptr`循环引用问题。 ## shared_ptr - 支持引用计数,所有者共同负责资源释放。 - 创建方式:空指针、new操作、拷贝构造/移动构造,以及自定义删除器。 - 提供`operator*`和`operator-&gt;`,以及`reset`、`swap`等方法。 ## unique_ptr
|
6天前
|
存储 Java C#
C++语言模板类对原生指针的封装与模拟
C++|智能指针的智能性和指针性:模板类对原生指针的封装与模拟
|
4天前
|
C++
C++职工管理系统(类继承、文件、指针操作、中文乱码解决)
C++职工管理系统(类继承、文件、指针操作、中文乱码解决)
7 0
C++职工管理系统(类继承、文件、指针操作、中文乱码解决)
|
6天前
|
设计模式 C++ 开发者
C++一分钟之-智能指针:unique_ptr与shared_ptr
【6月更文挑战第24天】C++智能指针`unique_ptr`和`shared_ptr`管理内存,防止泄漏。`unique_ptr`独占资源,离开作用域自动释放;`shared_ptr`通过引用计数共享所有权,最后一个副本销毁时释放资源。常见问题包括`unique_ptr`复制、`shared_ptr`循环引用和裸指针转换。避免这些问题需使用移动语义、`weak_ptr`和明智转换裸指针。示例展示了如何使用它们管理资源。正确使用能提升代码安全性和效率。
14 2
|
11天前
|
存储 算法 安全
C++一分钟之-数组与指针基础
【6月更文挑战第19天】在C++中,数组和指针是核心概念,数组是连续内存存储相同类型的数据,而指针是存储内存地址的变量。数组名等同于指向其首元素的常量指针。常见问题包括数组越界、尝试改变固定大小数组、不正确的指针算术以及忘记释放动态内存。使用动态分配和智能指针可避免这些问题。示例代码展示了安全访问和管理内存的方法,强调了实践的重要性。
26 3
|
16天前
|
编译器 Linux C++
C++智能指针
**C++智能指针是RAII技术的体现,用于自动管理动态内存,防止内存泄漏。主要有三种类型:已废弃的std::auto_ptr、不可复制的std::unique_ptr和可共享的std::shared_ptr。std::unique_ptr通过禁止拷贝和赋值确保唯一所有权,而std::shared_ptr使用引用计数来协调多个指针对同一资源的共享。在C++17中,std::auto_ptr因设计缺陷被移除。**
|
23天前
|
存储 编译器 C++
C++中的指针
C++中的指针
12 1