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时,除了需要将该资源释放,还需要将该资源对应的引用计数的内存空间进行释放


目录
相关文章
|
6天前
|
存储 安全 编译器
在 C++中,引用和指针的区别
在C++中,引用和指针都是用于间接访问对象的工具,但它们有显著区别。引用是对象的别名,必须在定义时初始化且不可重新绑定;指针是一个变量,可以指向不同对象,也可为空。引用更安全,指针更灵活。
|
24天前
|
存储 C++
c++的指针完整教程
本文提供了一个全面的C++指针教程,包括指针的声明与初始化、访问指针指向的值、指针运算、指针与函数的关系、动态内存分配,以及不同类型指针(如一级指针、二级指针、整型指针、字符指针、数组指针、函数指针、成员指针、void指针)的介绍,还提到了不同位数机器上指针大小的差异。
24 1
|
25天前
|
存储 编译器 C语言
C++入门2——类与对象1(类的定义和this指针)
C++入门2——类与对象1(类的定义和this指针)
21 2
|
27天前
|
存储 安全 编译器
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值(一)
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值
|
1月前
|
存储 C++ 索引
C++函数指针详解
【10月更文挑战第3天】本文介绍了C++中的函数指针概念、定义与应用。函数指针是一种指向函数的特殊指针,其类型取决于函数的返回值与参数类型。定义函数指针需指定返回类型和参数列表,如 `int (*funcPtr)(int, int);`。通过赋值函数名给指针,即可调用该函数,支持两种调用格式:`(*funcPtr)(参数)` 和 `funcPtr(参数)`。函数指针还可作为参数传递给其他函数,增强程序灵活性。此外,也可创建函数指针数组,存储多个函数指针。
|
2月前
|
编译器 C++
【C++核心】指针和引用案例详解
这篇文章详细讲解了C++中指针和引用的概念、使用场景和操作技巧,包括指针的定义、指针与数组、指针与函数的关系,以及引用的基本使用、注意事项和作为函数参数和返回值的用法。
33 3
|
27天前
|
存储 编译器 程序员
【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++提供的强大工具,它们在不同的场景下发挥着不可或缺的作用。了解两者的特点及适用场景,可以帮助开发者编写出更加高效、可读性更强的代码。在实际开发中,合理选择使用指针或引用是提高编程技巧的关键。
30 1