【C++】智能指针

简介: 【C++】智能指针

异常中内存的释放

  string* s = new string("sss");
  delete s;

一般情况下,使用动态开辟的内存空间只要在程序结束之前将其释放即可。

double div()
{
  int a, b;
  cin >> a >> b;
  if (b == 0)
  {
    throw "Dibision by zero condition!";
  }
 
  return (double)a / (double)b;
}
 
void Fun()
{
  int* p1 = new int;
  int* p2 = new int;
  try
  {
    div();
  }
  catch(...)
  {
    delete p1;
    delete p2;
    throw;
  }
 
  delete p1;
  delete p2;
}
 
int main()
{
  try 
  {
    Fun();
  }
  catch (const char* e)
  {
    //在 C++ 的标准库中,what() 函数通常是异常类的成员函数,
    // 用于获取异常对象所携带的描述异常信息的字符串。
    cout << e << endl;
  }
  return 0;
}

可能出现的问题:

  • 如果p1这里new抛异常会怎么样?
  • 如果p2这里new抛异常会怎么样?
  • 如果div调用这里同样抛出异常会出现什么情况?

在存在异常的情况下,存在多种情况的动态开辟内存空间,可能会导致内存泄漏。

内存泄漏

内存泄漏:内存泄漏是指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存之后,由于设计错误,失去了对该段内存的控制,因而造成了内存的浪费。

内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统,后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。

int main()
{
  //1.内存申请后忘记释放
  int* p1 = (int*)malloc(sizeof(int));
  int* p2 = new int;
 
  //2.异常安全的问题
  int* p3 = new int[10];
  //Fun()函数抛异常而导致p3没有释放
  Fun();
  delete[] p3;
 
  return 0;
}

内存泄漏分类:

在C/C++程序中,一般关心俩种方面的内存泄漏

  • 堆内存泄漏(Heap leak):堆内存指的是程序执行中依据需要通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的的free或者delete删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。
  • 系统资源泄漏:指的是程序使用系统分配的资源,比如套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。

如何避免内存泄漏

  • 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间需要注意释放。(这种情况属于理想情况,如果遇上异常,就算注意释放,可能还是会出现问题,需要智能指针来管理)
  • 采用RALL思想或者智能指针来管理资源。
  • 部分公司内部规范使用内部实现的私有内存管理库,这套库自带内存泄漏检测的功能选项。
  • 出现问题可以使用内存泄漏工具检测,但是很多工具都不是很靠谱,或者价格昂贵。

【总结】:内存泄漏在工程中是非常常见的,解决方案分为俩种

  1. 事先预防型,例如使用智能指针等。
  2. 事后查错型,例如泄漏检测工具等。

智能指针的使用及原理

RAII

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

       在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。

       通过这种方式,我们实际把管理一份资源的责任托管给一个对象,这种做法的好处是:

  • 不需要显式地释放资源。
  • 采用这种方式,对象所需地资源在其生命周期内始终保持有效。
//利用RAII地思想设计SmartPtr类
template<class T>
class SmartPtr
{
public:
  //利用类来构造
  SmartPtr(T* ptr = nullptr)
    :_ptr(ptr)
  {}
  //利用类来析构
  ~SmartPtr()
  {
    if (_ptr)
    {
      delete _ptr;
    }
  }
private:
  T* _ptr;
};

通过利用RAII思想,可以巧妙地开辟与释放空间,但是该SmartPtr还不能称之为智能指针,因为其还不具有指针的行为。指针可以解引用,可以使用->来访问所指空间中的内置,也可以进行拷贝构造等操作。

在C++智能指针的历史中,一个存在着4中智能指针:

  • auto_ptr
  • unique_ptr
  • shared_ptr
  • weak_ptr

下面进行一一介绍:

auto_ptr

        在C++98版本中,提供了auto_ptr的智能指针,其实现原理是:管理权转移的思想。

简化模拟:

  template<class T>
  class auto_ptr
  {
  public:
    //构造
    auto_ptr(T* ptr = nullptr)
      :_ptr(ptr)
    {}
    //析构
    ~auto_ptr()
    {
      if (_ptr)
      {
        delete _ptr;
      }
    }
    //向指针一样使用
    T& operator*()
    {
      return *_ptr;
    }
    T* operator->()
    {
      return _ptr;
    }
    //管理权转移
    auto_ptr(auto_ptr<T>& sp)
      :_ptr(sp._ptr)
    {
      sp._ptr = nullptr;
    }
    auto_ptr<T>& operator=(auto_ptr<T>& ap)
    {
      //判断是否自己给自己赋值
      if (this == &ap)
      {
        return *this;
      }
      //释放当前对象的资源
      if (_ptr)
      {
        delete _ptr;
      }
      //管理权限转移
      _ptr = ap._ptr;
      ap._ptr = nullptr;
 
      return *this;
    }
  private:
    T* _ptr;
  };

在拷贝过程中,auto_ptr会将被拷贝对象的资源管理权将给拷贝对象,导致被拷贝对象会被悬空额,而无法访问。

auto_ptr是一个失败的设计,很多公司都明确要求不能使用auto_ptr。

unique_ptr

在C++11中,开始提供更加靠谱的unique_ptr,其实现原理是:简单粗暴防拷贝。

  template<class T>
  class unique_ptr
  {
  public:
    //构造
    unique_ptr(T* ptr = nullptr)
      :_ptr(ptr)
    {}
    //析构
    ~unique_ptr()
    {
      if (_ptr)
      {
        delete _ptr;
      }
    }
    //向指针一样使用
    T& operator*()
    {
      return *_ptr;
    }
    T* operator->()
    {
      return _ptr;
    }
    //禁止拷贝赋值
    unique_ptr(const unique_ptr<T>& up) = delete;
    unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;
  private:
    T* _ptr;
  };

shared_ptr

在C++11中,提供了一个更加靠谱并且支持拷贝的shared_ptr。

shared_ptr的实现原理是:通过计数的方式来实现多个shared_ptr对象之间共享资源。

  1. shared_ptr在其内部,每个资源都维护着一份计数,用来记录该份资源被几个对象共享。
  2. 在对象被销毁的时候(即析构函数调用的时候),就说明不需要使用该资源了,对象的引用计数减一。
  3. 如果引用计数是0,就说明已经没有对象使用该资源,就需要对该资源进行释放。
  4. 如果引用计数不是0,就说明除了自己这个对象,还有其他对象在使用这份资源,不能释放这份资源,只需要将该引用计数减一即可,否则就会变成野指针。
  5. 引用计数同样是被指针所指的。
  template<class T>
  class shared_ptr
  {
  public:
    shared_ptr(T* ptr = nullptr)
      :_ptr(ptr)
      ,_pcount(new int(1))
    {}
 
    ~shared_ptr()
    {
      if (--(*_pcount) == 0)
      {
        delete _ptr;
        delete _pcount;
      }
    }
    T& operator*()
    {
      return *_ptr;
    }
    T* operator->()
    {
      return _ptr;
    }
    shared_ptr(const shared_ptr<T>& sp)
      :_ptr(sp._ptr)
      ,_pcount(sp._pcount)
    {
      ++(*_pcount);
    }
    shared_ptr<T>& operator=(const shared_ptr<T>& sp)
    {
      //自己给自己赋值
      if (_ptr == sp._ptr)
      {
        return *this;
      }
      if (--(*_pcount) == 0)
      {
        delete _ptr;
        delete _pcount;
      }
      _ptr = sp._ptr;
      _pcount = sp._pcount;
      ++(*_pcount);
 
      return *this;
    }
    int use_count()
    {
      return *_pcount;
    }
    T* get()
    {
      return _ptr;
    }
 
  private:
    T* _ptr;
    int* _pcount;
  };

使用shared_ptr基本可以解决大多数问题,但是有一种情况shared_ptr难以解决:

template<class T>
struct Node
{
  shared_ptr<Node<T>> _next;
  shared_ptr<Node<T>> _prev;
  T* _val;
};
 
 
int main()
{
  // 循环引用
  shared_ptr<Node<int>> sp1(new Node<int>);
  shared_ptr<Node<int>> sp2(new Node<int>);
 
  cout << sp1.use_count() << endl;
  cout << sp2.use_count() << endl;
 
  sp1->_next = sp2;
  sp2->_prev = sp1;
}

该问题就是循环引用:

  1. node1和node2俩个智能指针对象指向俩个结点,引用计数变成1,此时不需要手动delete
  2. node1的_next指向node2,node2的_prev指向node1,引用计数变成2。
  3. node1和node2析构,引用计数减到1,但是_next还指向下一个结点,但是_prev还指向上一个结点。
  4. 此时,如果_next析构了,node2就会析构;如果_prev析构了,node1就会析构
  5. 但是_next属于node的成员,node1释放了,_next才会析构,而node1又由_prev管理,_prev属于node2成员。

这种情况被称为循环引用。

weak_ptr

在上述情况之下,只需要在引用计数的场景之下,把结点中的_prev和_next改成weak_ptr即可。

其原理就是:node1->_next=node2;和node2->_prev=node1;时weak_ptr的_next和_prev不会增加node1和node2的引用计数。

  template<class T>
  class weak_ptr
  {
  public:
    weak_ptr()
      :_ptr(nullptr)
    {}
 
    weak_ptr(const shared_ptr<T>& sp)
      :_ptr(sp.get())
    {}
 
    weak_ptr<T>& operator=(const shared_ptr<T>& sp)
    {
      _ptr = sp.get();
      return *this;
    }
 
    T& operator*()
    {
      return *_ptr;
    }
 
    T* operator->()
    {
      return _ptr;
    }
  private:
    T* _ptr;
  };

weak_ptr不是RAII智能指针,专门用来解决shared_ptr循环引用问题,weak_ptr不增加引用计数,可以访问资源,不参与资源释放的管理。

定制删除器

如果不是new出来的对象,shared_ptr设计了一个删除器来解决此类问题。

// 仿函数的删除器
template<class T>
struct FreeFunc {
  void operator()(T* ptr)
  {
    cout << "free:" << ptr << endl;
    free(ptr);
  }
};
template<class T>
struct DeleteArrayFunc {
  void operator()(T* ptr)
  {
    cout << "delete[]" << ptr << endl;
    delete[] ptr;
  }
};
int main()
{
  FreeFunc<int> freeFunc;
  std::shared_ptr<int> sp1((int*)malloc(4), freeFunc);
  DeleteArrayFunc<int> deleteArrayFunc;
  std::shared_ptr<int> sp2((int*)malloc(4), deleteArrayFunc);
 
 
  std::shared_ptr<A> sp4(new A[10], [](A* p) {delete[] p; });
  std::shared_ptr<FILE> sp5(fopen("test.txt", "w"), [](FILE* p)
    {fclose(p); });
 
  return 0;
}

智能指针的选择

  • auto_ptr:管理权转移,会导致被拷贝对象悬空,建议不要使用。
  • unique_ptr:禁止拷贝,简单粗暴,一般在不需要拷贝的场景中使用,可以使用
  • shared_ptr:引用计数支持拷贝,需要拷贝的场景,就可以使用它,但是需要小心构成循环引用,循环引用会导致内存泄漏
  • weak_ptr:专门用来解决shared_ptr的循环引用问题。
相关文章
|
7天前
|
C++
C++(十八)Smart Pointer 智能指针简介
智能指针是C++中用于管理动态分配内存的一种机制,通过自动释放不再使用的内存来防止内存泄漏。`auto_ptr`是早期的一种实现,但已被`shared_ptr`和`weak_ptr`取代。这些智能指针基于RAII(Resource Acquisition Is Initialization)原则,即资源获取即初始化。RAII确保对象在其生命周期结束时自动释放资源。通过重载`*`和`-&gt;`运算符,可以方便地访问和操作智能指针所指向的对象。
|
7天前
|
C++
C++(九)this指针
`this`指针是系统在创建对象时默认生成的,用于指向当前对象,便于使用。其特性包括:指向当前对象,适用于所有成员函数但不适用于初始化列表;作为隐含参数传递,不影响对象大小;类型为`ClassName* const`,指向不可变。`this`的作用在于避免参数与成员变量重名,并支持多重串联调用。例如,在`Stu`类中,通过`this-&gt;name`和`this-&gt;age`明确区分局部变量与成员变量,同时支持链式调用如`s.growUp().growUp()`。
|
20天前
|
存储 安全 C++
C++:指针引用普通变量适用场景
指针和引用都是C++提供的强大工具,它们在不同的场景下发挥着不可或缺的作用。了解两者的特点及适用场景,可以帮助开发者编写出更加高效、可读性更强的代码。在实际开发中,合理选择使用指针或引用是提高编程技巧的关键。
22 1
|
24天前
|
安全 NoSQL Redis
C++新特性-智能指针
C++新特性-智能指针
|
27天前
|
编译器 C++
virtual类的使用方法问题之在C++中获取对象的vptr(虚拟表指针)如何解决
virtual类的使用方法问题之在C++中获取对象的vptr(虚拟表指针)如何解决
|
1月前
|
存储 安全 编译器
C++入门 | auto关键字、范围for、指针空值nullptr
C++入门 | auto关键字、范围for、指针空值nullptr
47 4
|
2月前
|
存储 安全 C++
浅析C++的指针与引用
虽然指针和引用在C++中都用于间接数据访问,但它们各自拥有独特的特性和应用场景。选择使用指针还是引用,主要取决于程序的具体需求,如是否需要动态内存管理,是否希望变量可以重新指向其他对象等。理解这二者的区别,将有助于开发高效、安全的C++程序。
25 2
|
2月前
|
存储 安全 C++
浅析C++的指针与引用
虽然指针和引用在C++中都用于间接数据访问,但它们各自拥有独特的特性和应用场景。选择使用指针还是引用,主要取决于程序的具体需求,如是否需要动态内存管理,是否希望变量可以重新指向其他对象等。理解这二者的区别,将有助于开发高效、安全的C++程序。
22 3
|
22天前
|
存储 C++
c++学习笔记06 指针
C++指针的详细学习笔记06,涵盖了指针的定义、使用、内存占用、空指针和野指针的概念,以及指针与数组、函数的关系和使用技巧。
28 0
|
27天前
|
安全 编译器 容器
C++STL容器和智能指针
C++STL容器和智能指针