C++进阶 智能指针(上)

简介: C++进阶 智能指针

为什么会存在智能指针

我们首先来看下面的这段代码

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

在上面这段代码中有着一个很明显的内存泄露风险

当我们的程序运行在Func函数内的div函数时 很可能因为除0错误而跳转到另外一个执行流从而导致Func函数内两个new出来的内存没法被回收

为了解决这个问题我们发明了内存指针

内存泄露

内存泄漏定义

通常是由于我们的疏忽或者是程序错误导致未使用的内存没有被及时释放

这里有个经典的面试题 内存泄漏是内存丢了还是指针丢了

答案是指针丢了 因为我们能够找到指针就能够释放内存

内存泄漏的危害

内存泄漏会导致运行环境越来越慢 最终导致服务器崩溃

如何检测内存泄漏

Linux检测 : Linux内存泄漏检测工具

windows检测: Windows下内存泄漏检测工具

如何避免内存泄漏

  • 良好的编程习惯 主动申请的资源记得要主动释放
  • 利用RAII思想或智能指针来管理资源
  • 有些公司内部规范使用内部实现的私有内存管理库 这套库自带内存泄漏检测的功能选项
  • 出问题了使用内存泄漏工具检测

智能指针的使用及其原理

RAII

RAII的英文全称是 Resource Acquisition Is Initialization 直译过来即为 资源请求后初始化

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

在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:

  • 不需要显式地释放资源。
  • 采用这种方式,对象所需的资源在其生命期内始终保持有效。

设计一个智能指针

我们将上面的代码放在Linux平台下编译运行 能够得到这的结果

我们发现 没有除0错误的时候能正常delete掉new出来的空间

可是一旦发生了除0错误就会造成内存泄漏

为了防止这种情况 我们结合上面的RAII技术自己写出一个智能指针出来

template<class T>      
class SmartPtr      
{      
  private:      
    T* _ptr;      
  public:      
    SmartPtr(T* ptr)      
      :_ptr(ptr)      
    {}      
    ~SmartPtr()      
    {      
      delete _ptr;
      cout << "delete success!" << endl;                                                 
    }           
}; 

之后将源代码中的指针使用智能指针管理起来后重新编译运行

此时我们就会发现 不管有没有发生除0错误 new出来的内存都会被delete

为了让定义出来的智能指针对象更加符合原生指针的操作 我们使用operator操作符重载下 *->

T& operator*()    
    {                 
      return *_ptr;                         
    }               
    T* operator->()    
    {    
      return _ptr;                                              
    }    

C++官方的智能指针

这里介绍一个C++98版本中就有的指针指针 auto_ptr

它的头文件是memory

演示代码如下

#include <iostream>    
  using namespace std;    
  #include <memory>    
  class A    
  {    
    public:    
      ~A()    
      {    
        cout << "delete A" << endl;    
      }    
  };    
  int main()    
  {    
W>  auto_ptr<A> ap1(new A);                                     
    return 0;    
  }   

编译运行之后我们可以发现 即使我们没有主动析构 它也自动帮我们调用了析构函数

(这里报警告的原因是auto_otr并不安全 实际上std::auto_ptr 已经在 C++11 中被弃用 并且在C++11中被删除 )

实际上auto_ptr能够做到的事情我们自己写的SmartPtr一样可以做到

而智能指针的难点也并不在这里 而在拷贝

如果我们写出这样子的代码

SmartPtr<A> sp1(new A);    
  SmartPtr<A> sp2(sp1);   

那么编译运行之后就会出现双重释放问题

为什么会出现这样子的现象呢?

如下图

本来是只有一个sp1对象管理着一份资源

然后我们使用拷贝构造构造出了第二个对象sp2 由于我们没有写构造函数 所以说类使用默认构造函数浅拷贝同样指向了sp1的资源

那么此时两个对象同时管理同一份资源 当析构的时候自然会析构两次 自然就会出现上面的双重释放的错误了

那么我们应该如何解决这个错误呢?

方案一: 写一个深拷贝

这个方案虽然理论上可行 但是实际上它严重违背了我们使用智能指针的初衷 我们当初使用智能指针的目的就是为了管理资源 而如果使用了这个方案则进行拷贝构造的时候还会额外的占用资源 未免太得不偿失了

方案二: 管理权转移

auto_ptr使用的就是该方案

它的具体思路就是 将被拷贝对象管理的指针置空 将原来的指针拷贝到拷贝后的对象中

这是一种很不负责任的做法 因为如果使用了该方法 我们就极有可能遇到空指针的问题 实际上也就是因为这点auto_ptr在C++11以后被弃用

auto_ptr的赋值运算符重载思路

假设现在智能指针ap1管理着一个资源 指针指针ap2管理一个资源

进行了 ap1 = ap2 操作之后

ap1改为管理ap2的资源 ap1之前的资源会被释放掉 ap2的指针置空

当然 这是一个很差的设计思路 我们学习这个东西的意义仅仅在于了解 大家做项目的时候不要去使用这种思路

方案三:禁用拷贝

在C++11中的 unique_ptr就是使用的这种方案

实现方式也很简单

在C++11之后的版本 在构造函数后面加上 =delete 就可以

在C++11之前的版本 我们需要将拷贝构造函数和赋值函数只声明不实现并且私有化

方案四:引用计数

shared_ptr就是使用的这个方案

设计方案如图

我们每次创建一个对象就在计数器中加上一个数字 每次删除一个对象就在计数器中减去一个数字

直到计数器中的数字为0时 我们才真正的删除资源

那么我们如何定义这个计数器呢? 使用静态变量嘛?

使用静态变量肯定是不可以的 因为静态变量是一个全局变量 它虽然能解决多个对象管理一个资源的问题 但是却解决不了多个对象管理多个资源的问题

我们这里的解决方案应该是使用一个int类型的指针

当我们创建对象的时候给这个指针new出来一块空间作为计数器

每次拷贝的时候将这个int类型的指针也同样赋值 之后让计数器++即可

shared_ptr如何实现赋值运算符重载

shared_ptr的赋值运算符重载跟其他智能指针不同的一点是 它是多个对象共同管理者一个资源的

所以说我们赋值后不能简单的置空 还要考虑–计数器 如果–之后计数器为0 则还要考虑释放资源的问题

并且还要注意下一份资源不能给相同资源赋值的问题 (判断指向资源的指针是否相等即可)

循环引用问题

假如说我们现在用智能指针管理两个节点

现在自动释放还没有问题

可是如果我们做出下面两步操作 就会造成一个循环引用从而无法释放的问题

  1. 我们让n1的_next节点指向n2
  2. 我们让n2的_prev节点指向n1

到函数最后会按照定义的先后顺序反向析构 假设我们先定义的n1 后定义的n2 就会先析构n2 再析构n1

可以析构之后我们会发现这样子的场景

析构一次n2之后 由于计数器不为0 所以说n2资源依旧存在

析构一次n1之后 由于计数器不为0 所以说n1资源依旧存在

而由于n1的资源由n2的_prev指针管理

n2的资源由n1的_next指针管理

所以说

要想析构n1 首先要析构掉n2

而要想析构n2 首先要析构掉n1

这样子就形成了一个死循环 这个就是shared_ptr的循环引用问题 这个问题内部没有解决方式

为了解决这个问题 C++11发明了weak_ptr用来解决 shared_ptr的循环引用问题

我们可以把weak_ptr理解为shared_ptr的小跟班 它不单独出现

在节点里面的智能指针我们可以使用weak_ptr来进行定义

weak_ptr不会增加引用计数 但是可以正常的访问修改资源 从而也就不会存在循环引用问题了

代码表示如下

template<class T>
  class weak_ptr
  {
  public:
    weak_ptr()
      :_ptr(nullptr)
    {}
    weak_ptr(const shared_ptr<T>& sp)
      :_ptr(sp.get())
    {}
    weak_ptr& operator=(const shared_ptr<T>& sp)
    {
      _ptr = sp.get();
      return *this;
    }
    T& operator*()
    {
      return *_ptr;
    }
    T* operator->()
    {
      return _ptr;
    }
    T* get()
    {
        return _ptr;
    }
  private:
    T* _ptr; //管理的资源
  };
相关文章
|
11天前
|
存储 安全 编译器
在 C++中,引用和指针的区别
在C++中,引用和指针都是用于间接访问对象的工具,但它们有显著区别。引用是对象的别名,必须在定义时初始化且不可重新绑定;指针是一个变量,可以指向不同对象,也可为空。引用更安全,指针更灵活。
|
30天前
|
存储 C++
c++的指针完整教程
本文提供了一个全面的C++指针教程,包括指针的声明与初始化、访问指针指向的值、指针运算、指针与函数的关系、动态内存分配,以及不同类型指针(如一级指针、二级指针、整型指针、字符指针、数组指针、函数指针、成员指针、void指针)的介绍,还提到了不同位数机器上指针大小的差异。
28 1
|
30天前
|
存储 编译器 C语言
C++入门2——类与对象1(类的定义和this指针)
C++入门2——类与对象1(类的定义和this指针)
22 2
|
1月前
|
存储 安全 编译器
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值(一)
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值
|
1月前
|
存储 C++ 索引
C++函数指针详解
【10月更文挑战第3天】本文介绍了C++中的函数指针概念、定义与应用。函数指针是一种指向函数的特殊指针,其类型取决于函数的返回值与参数类型。定义函数指针需指定返回类型和参数列表,如 `int (*funcPtr)(int, int);`。通过赋值函数名给指针,即可调用该函数,支持两种调用格式:`(*funcPtr)(参数)` 和 `funcPtr(参数)`。函数指针还可作为参数传递给其他函数,增强程序灵活性。此外,也可创建函数指针数组,存储多个函数指针。
|
2月前
|
编译器 C++
【C++核心】指针和引用案例详解
这篇文章详细讲解了C++中指针和引用的概念、使用场景和操作技巧,包括指针的定义、指针与数组、指针与函数的关系,以及引用的基本使用、注意事项和作为函数参数和返回值的用法。
35 3
|
1月前
|
存储 编译器 程序员
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值(二)
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值
|
2月前
|
C++
C++(十八)Smart Pointer 智能指针简介
智能指针是C++中用于管理动态分配内存的一种机制,通过自动释放不再使用的内存来防止内存泄漏。`auto_ptr`是早期的一种实现,但已被`shared_ptr`和`weak_ptr`取代。这些智能指针基于RAII(Resource Acquisition Is Initialization)原则,即资源获取即初始化。RAII确保对象在其生命周期结束时自动释放资源。通过重载`*`和`-&gt;`运算符,可以方便地访问和操作智能指针所指向的对象。
|
2月前
|
C++
C++(九)this指针
`this`指针是系统在创建对象时默认生成的,用于指向当前对象,便于使用。其特性包括:指向当前对象,适用于所有成员函数但不适用于初始化列表;作为隐含参数传递,不影响对象大小;类型为`ClassName* const`,指向不可变。`this`的作用在于避免参数与成员变量重名,并支持多重串联调用。例如,在`Stu`类中,通过`this-&gt;name`和`this-&gt;age`明确区分局部变量与成员变量,同时支持链式调用如`s.growUp().growUp()`。
|
3月前
|
存储 安全 C++
C++:指针引用普通变量适用场景
指针和引用都是C++提供的强大工具,它们在不同的场景下发挥着不可或缺的作用。了解两者的特点及适用场景,可以帮助开发者编写出更加高效、可读性更强的代码。在实际开发中,合理选择使用指针或引用是提高编程技巧的关键。
31 1