『 C++ 』智能指针 ( 万字梳理 )

news2024/9/22 23:15:45

文章目录

    • 智能指针概念
    • 内存泄漏的危害
    • RAII与智能指针
    • 智能指针的赋值
    • auto_ptr 管理权转移
      • auto_ptr 的对象悬空问题
    • unique_ptr 防拷贝
      • unique_ptr 简单实现
    • shared_ptr 引用计数
      • shared_ptr 简单实现
      • shared_ptr 的循环引用问题与 weak_ptr
    • 智能指针的自定义删除器


智能指针概念

请添加图片描述

智能指针是C++标准库中用于管理动态分配内存的一种对象;

通过封装原生指针为用户自动管理内存的生命周期从而减少内存泄漏和悬空指针的问题;

在C++中动态分配内存是通过new运算符在堆空间中进行开辟的,使用后需要手动调用delete来释放内存;

如果程序因为错误或是用户忽略而导致释放内存就会造成内存泄漏,这种未delete的原因可能是异常导致的,也可能是用户忽略造成的;

class Data {
 public:
  Data() { cout << "Data()" << endl; }
  ~Data() { cout << "~Data()" << endl; }
};

double Div(double num1, double num2) {
  if (num2 == 0) throw "Division by zero error";
  return num1 / num2;
}

void func() {
  Data *d1 = new Data();
  Data *d2 = new Data();
  cout << Div(10, 0) << endl;
  delete d1;
  delete d2;
}

int main() {
  try {
    func();
  } catch (const char *abnor) {
    cout << "main get a abnormal : " << abnor << endl;
  }
  return 0;
}

在这个例子中定义了一个Data类,其中该类存在构造和析构函数用来判断构造和析构是否被调用;

定义了一个除法函数Div,当num20时则抛出一个异常并在main函数中处理;

func函数调用Div且在调用该函数前用new在堆上开了两个Data类型的对象,main函数调用func函数,运行结果如下:

$ ./test 
Data()
Data()
main get a abnormal : Division by zero error

在这个例子中Data类的析构函数并未被调用,意味着出现了内存泄漏的问题;

同时如果用户多次释放同一块内存则可能导致未定义的问题(悬空指针,当一块空间被delete后所指向的空间将是一个无效的空间,如果对一个悬空指针进行释放则导致未定义问题);

int main() {
  int *a = new int;
  int *b = a;
  delete a;
  delete b;
  return 0;
}
/*
	运行结果为:
	$ ./test 
	*** Error in `./test': double free or corruption (fasttop): 0x00000000012dec20 ***
	======= Backtrace: =========
	/lib64/libc.so.6(+0x81329)[0x7fe3332b2329]
	...
	...
	Aborted
*/

在这个例子中new了一个int类型的对象并用指针a进行接收,并声明了一个指针将a所指向的空间赋值给b;

当对a进行deleteab所指向的空间就已经释放,这时指针ab都为悬空指针,当再次对已经释放的空间进行delete后则出现未定义问题;

而智能指针可解决这一系列问题以防止内存泄漏;

class Data {
 public:
  Data() { cout << "Data()" << endl; }
  ~Data() { cout << "~Data()" << endl; }
};

double Div(double num1, double num2) {
  if (num2 == 0) throw "Division by zero error";
  return num1 / num2;
}

void func() {
  auto_ptr<Data> d1(new Data); // 智能指针
  auto_ptr<Data> d2(new Data);
  cout << Div(10, 0) << endl;
}

int main() {
  try {
    func();
  } catch (const char *abnor) {
    cout << "main get a abnormal : " << abnor << endl;
  }
  return 0;
}
/*
	运行结果为:
	$ ./test 
	Data()
	Data()
	~Data()
	~Data()
	main get a abnormal : Division by zero error
*/

在这个例子中使用了auto_ptr智能指针来封装原生指针,当智能指针的生命周期结束时将自动调用析构函数来释放资源以解决上述的内存泄漏问题,Data类的析构函数被调用;


内存泄漏的危害

请添加图片描述

内存泄漏是指程序在动态分配内存后 没有释放或无法释放已分配的内存块 这会导致内存永远无法被再次使用 内存泄漏的危害主要包括以下几点;

  • 资源耗尽

    内存泄漏会逐渐减少系统中的可用内存,最终可能导致系统无法再分配足够的内存来满足新的请求,这会导致程序或系统崩溃;

  • 性能下降

    内存泄漏会使内存分配效率降低;

    系统在分配内存时需要花费更多时间来搜索可用的内存块,从而增加分配和释放内存的时间开销从而降低整体性能;

  • 程序不稳定

    随着内存泄漏的积累,程序会消耗越来越多的内存;

    这会导致程序运行变得不稳定,甚至最终崩溃;

  • 数据丢失或损坏

    如果内存泄漏涉及到重要的数据结构或对象;

    这些数据可能因为内存泄漏而被破坏或丢失;

    这会导致程序运行时出现意外的行为或结果;

  • 难以调试

    内存泄漏通常不会立即显现问题而是逐渐积累;

    因此当问题出现时,定位和修复内存泄漏可能非常困难,需要使用专门的内存分析工具来检测和排查;

  • 安全风险

    在某些情况下,内存泄漏可能导致安全漏洞;

    如果未释放的内存中包含敏感信息,如密码或加密密钥,这些信息可能会泄露给未经授权的第三方;


RAII与智能指针

请添加图片描述

RAII(Resource Acquisition Is Initialization)旨在通过对象的生命周期来管理资源,确保在对象被创建时获取资源并在对象被销毁时释放资源;

RAII使用构造函数和析构函数的特性来自动管理资源以避免资源泄露问题;

  • 资源获取即初始化

    对象在构造时获得资源(文件,文件句柄,网络连接等),并在析构时释放资源,这表示对象的构造和析构会自动管理他所拥有的资源;

  • 对象的生命周期管理

    大部分大于旋风在离开作用域时会自动清理(调用析构函数),使通过RAII管理的资源也会在对象消亡时会被自动释放;

  • 异常安全

    RAII能够保障异常情况下资源的正确释放;

    即使在构造或操作过程中发生异常,析构函数也会被调用从而正确释放资源;

智能指针是RAII的一种实现,主要用于动态内存管理;

template <class T>
class smartPoint {
 public:
    // 完成RAII
  smartPoint(T *ptr) : ptr_(ptr) {}

  ~smartPoint() {
    delete ptr_;
    ptr_ = nullptr;
    std::cout << " ~smartPoint()" << std::endl;
  }

  T &operator*() { return *ptr_; }

  T *operator->() { return ptr_; }

 private:
  T *ptr_;
};

这是一个简单的智能指针的实现,其中构造函数和析构函数完成了RAII,即对象创建时被初始化,对象销毁时清理资源;

其中operator*()operator->()*->的重载使其能够像指针一样进行解引用;

class Data {
 public:
  Data(int data = 0) : data_(data) { cout << "Data()" << endl; }
  ~Data() { cout << "~Data()" << endl; }

 public:
  int data_;
};

void func() {
  {  // 可使用 {} 来控制对象的生命周期
    smartPoint<Data> d1(new Data(10));
    cout << "对象创建即初始化" << endl;
    cout << "d1 -> data_ :" << (d1->data_ += 10)
         << endl;  // 像指针一样进行解引用
  }
  cout << "对象清理结束" << endl;
}

int main() {
  func();
  return 0;
}

/*
	运行结果为:
	$ ./test 
	Data()
	对象创建即初始化
	d1 -> data_ :20
	~Data()
 	~smartPoint()
	对象清理结束
*/

智能指针的赋值

请添加图片描述

智能指针必须像指针一样进行赋值;

上文中的智能指针的实现的赋值采用的是浅拷贝,即将一个智能指针所指向的空间地址赋值给另一个智能指针对象,即两个智能指针都指向同一块空间;

当同一块空间被连续delete则会出现未定义行为;

template <class T>
class smartPoint {
 public:
  smartPoint(T *ptr) : ptr_(ptr) {}

  ~smartPoint() {
    delete ptr_;
    ptr_ = nullptr;
    std::cout << " ~smartPoint()" << std::endl;
  }

  T &operator*() { return *ptr_; }

  T *operator->() { return ptr_; }

 private:
  T *ptr_;
};

class Data {
 public:
  Data(int data = 0) : data_(data) { cout << "Data()" << endl; }
  ~Data() { cout << "~Data()" << endl; }

 public:
  int data_;
};

void func() { smartPoint<Data> d1(new Data(10));
  smartPoint<Data> d2 = d1;
}

int main() {
  func();
  return 0;
}

/*
	运行结果为:
	$ ./test 
    Data()
    ~Data()
     ~smartPoint()
    ~Data()
    *** Error in `./test': double free or corruption (fasttop): 0x0000000000abdc20 ***
    ======= Backtrace: =========
    ...
    ...
    Aborted
*/

在这个例子中智能指针进行了赋值,导致对同一块空间连续delete导致崩溃;

解决智能指针赋值问题有几种方法:

  • 管理权转移

    管理权转移是指指针的所有权在指针之间转移,以确保某一时刻只有一个智能指针拥有资源;

  • 防拷贝

    防拷贝机制禁止智能指针的拷贝构造和拷贝赋值以确保资源的唯一性和不可复制性;

    可直接使用deleted关键字禁用拷贝构造和赋值重载;

  • 引用计数

    引用计数是一种共享所有权的机制,允许多个指针共享同一个资源(对象);

    当每创建一个新的智能指针指向统一资源时引用计数增加;

    当指针被销毁时引用计数减少,当引用计数减到0时资源才会实际被释放;


auto_ptr 管理权转移

请添加图片描述

auto_ptrC++98标准库引入的一种智能指针,主要目的用于自动管理动态分配的内存,以避免内存泄漏;

auto_ptr所采用的智能指针赋值的解决方案及采用管理权转移的方式;

template <class T>
class myauto_ptr {
 public:
  myauto_ptr(T *ptr) : ptr_(ptr) {}

  ~myauto_ptr() {
    // 确保在指针为非空状态时进行资源释放
    if (ptr_) {
      delete ptr_;
      ptr_ = nullptr;
      std::cout << " ~myauto_ptr()" << std::endl;
    }
  }

  myauto_ptr(myauto_ptr<T> &ptr) : ptr_(ptr.ptr_) {
    // 构造函数进行管理权转移
    ptr.ptr_ = nullptr;
  }

  myauto_ptr<T> &operator=(myauto_ptr<T> &ptr) {
    // 赋值重载进行管理权转移
    ptr_ = ptr.ptr_;
    ptr.ptr_ = nullptr;
    return *this;
  }

  T &operator*() { return *ptr_; }

  T *operator->() { return ptr_; }

 private:
  T *ptr_;
};

在这个例子中模拟了一个auto_ptr指针的实现;

其中当进行拷贝或者赋值时进行管理权的转移,即将原本的智能指针所指向的空间的管理权交给新的智能指针对象,而原本的智能指针对象的指针设为空;

析构函数为当智能指针所指向的空间不为空nullptr时则对资源进行清理;

class Data {
 public:
  Data(int data = 0) : data_(data) { cout << "Data()" << endl; }
  ~Data() { cout << "~Data()" << endl; }

 public:
  int data_;
};

void func() {
  myauto_ptr<Data> d1(new Data(10));
  myauto_ptr<Data> d2 = d1;  // 进行赋值
}

int main() {
  func();
  return 0;
}
/*
	运行结果:
	$ ./test 
	Data()
	~Data()
 	~myauto_ptr()
*/

这个例子使用了智能指针d1封装原生指针并且再创建了一个智能指针对象d2d1赋值给d2,其中空间的管理权会转移给d2;

当智能指针的生命周期结束时将调用析构函数,析构函数为当指向空间不为空时对空间进行delete并将其指针置为nullptr;


auto_ptr 的对象悬空问题

请添加图片描述

auto_ptr智能指针的对象悬空问题指当一个auto_ptr智能指针对象赋值给另一个auto_ptr智能指针时进行指针的管理权转移后当前的auto_ptr智能指针对象将处于一个对象悬空的状态,此时再次对该智能指针对象进行修改等操作将会导致对空指针的非法解引用导致出现未定义行为;

template <class T>
class myauto_ptr {
 public:
  myauto_ptr(T *ptr) : ptr_(ptr) {}

  ~myauto_ptr() {
    // 确保在指针为非空状态时进行资源释放
    if (ptr_) {
      delete ptr_;
      ptr_ = nullptr;
      std::cout << " ~myauto_ptr()" << std::endl;
    }
  }

  myauto_ptr(myauto_ptr<T> &ptr) : ptr_(ptr.ptr_) {
    // 构造函数进行管理权转移
    ptr.ptr_ = nullptr;
  }

  myauto_ptr<T> &operator=(myauto_ptr<T> &ptr) {
    // 赋值重载进行管理权转移
    ptr_ = ptr.ptr_;
    ptr.ptr_ = nullptr;
    return *this;
  }

  T &operator*() { return *ptr_; }

  T *operator->() { return ptr_; }

 private:
  T *ptr_;
};

class Data {
 public:
  Data(int data = 0) : data_(data) { cout << "Data()" << endl; }
  ~Data() { cout << "~Data()" << endl; }

 public:
  int data_;
};

void func() {
  myauto_ptr<Data> d1(new Data(10));
  myauto_ptr<Data> d2 = d1;  // 进行赋值

  ++d2->data_;
  ++d1->data_; // 对悬空对象进行修改操作
}

int main() {
  func();
  return 0;
}
/*
	运行结果为:
	$ ./test 
	Data()
	Segmentation fault
*/

在这个例子中使用了自定义的myauto_ptr封装了自定义的Data类指针为a1a2,将a1赋值给a2并对a1进行修改操作,最终运行崩溃;

在使用库中的auto_ptr也存在相同的问题;

class Data {
 public:
  Data(int data = 0) : data_(data) { cout << "Data()" << endl; }
  ~Data() { cout << "~Data()" << endl; }

 public:
  int data_;
};

void func() {
  auto_ptr<Data> d1(new Data(10));
  auto_ptr<Data> d2 = d1;  // 进行赋值

  ++d2->data_;
  ++d1->data_;  // 对悬空对象进行修改操作
}

int main() {
  func();
  return 0;
}

/*
	运行结果为:
	$ ./test 
	Data()
	Segmentation fault
*/

auto_ptrC++98引入的一种智能指针,旨在简化动态内存管理;

而由于其设计上的一些缺陷特别是拷贝语义的不直观和不适用于STL容器等问题在C++11后被弃用;


unique_ptr 防拷贝

请添加图片描述

unique_ptrC++11标准引入的一种智能指针,旨在提供独占资源所有权,无法被复制只能通过移动;

这一特性确保了在任何时刻某个动态分配的资源只由一个unique_ptr管理从而避免了重复删除和潜在的资源泄露问题;

  • 防拷贝

    class Data {
     public:
      Data(int data = 0) : data_(data) { cout << "Data()" << endl; }
      ~Data() { cout << "~Data()" << endl; }
    
     public:
      int data_;
    };
    
    void func() {
      unique_ptr<Data> d1(new Data(10));
      unique_ptr<Data> d2 = d1;  // 进行赋值
    }
    
    int main() {
      func();
      return 0;
    }
    /*
    	编译结果为:
    	$ make
        g++ -o test Main.cpp -g -Wall -std=c++11
        Main.cpp: In function ‘void func()’:
        Main.cpp:19:25: error: use of deleted function ‘std::unique_ptr<_Tp, _Dp>::unique_ptr(const std::unique_ptr<_Tp, _Dp>&) [with _Tp = Data; _Dp = std::default_delete<Data>]’
           unique_ptr<Data> d2 = d1;  // 进行赋值
                                 ^
        In file included from /usr/include/c++/4.8.2/memory:81:0,
                         from Main.cpp:2:
        /usr/include/c++/4.8.2/bits/unique_ptr.h:273:7: error: declared here
               unique_ptr(const unique_ptr&) = delete;
               ^
        make: *** [test] Error 1
    */
    

    在这个例子中使用标准库中的unique_ptr对象d1封装自定义类型Data的指针,并试图使用拷贝构造来将其赋给d2,最终编译结果为编译报错;

  • 移动拷贝

    class Data {
     public:
      Data(int data = 0) : data_(data) { cout << "Data()" << endl; }
      ~Data() { cout << "~Data()" << endl; }
    
     public:
      int data_;
    };
    
    void func() {
      unique_ptr<Data> d1(new Data(10));
      unique_ptr<Data> d2 = move(d1);  // 移动构造
      cout << ++d2->data_ << endl;
    }
    
    int main() {
      func();
      return 0;
    }
    
    /*
    	运行结果为:
    	$ ./test 
        Data()
        11
        ~Data()
    */
    

    在这个例子中使用了标准库中的unique_ptr对象d1封装了自定义类型Data对象指针,并使用了move()移动语句将其移动拷贝至d2中;

    由于移动语义语义明确即使用移动的方式进行管理权转移,本身使用move()进行移动任意自定义类型时对应的资源也会被转移,即对转移后的资源进行修改本质上就是一种错误的行为;


unique_ptr 简单实现

请添加图片描述

// 类模板 myunique_ptr 的定义
template <class T>
class myunique_ptr {
 public:
  // 默认构造函数,初始化 ptr_ 为 nullptr
  myunique_ptr() : ptr_(nullptr) {}

  // 带指针的构造函数,用给定指针初始化 ptr_
  myunique_ptr(T *ptr) : ptr_(ptr) {}

  // 析构函数,负责释放 ptr_ 指向的资源
  ~myunique_ptr() {
    if (ptr_) {
      delete ptr_;
      ptr_ = nullptr;
      std::cout << "~myunique_ptr()" << std::endl;
    }
  }

  // 删除拷贝构造函数,防止拷贝 myunique_ptr 对象
  myunique_ptr(const myunique_ptr<T> &ap) = delete;

  // 删除拷贝赋值运算符,防止拷贝赋值 myunique_ptr 对象
  myunique_ptr<T> &operator=(const myunique_ptr<T> &) = delete;

  // 移动构造函数,将资源从另一个 myunique_ptr 对象转移过来
  myunique_ptr(myunique_ptr &&ap) noexcept : ptr_(ap.ptr_) {
    ap.ptr_ = nullptr;
  }

  // 移动赋值运算符,将资源从另一个 myunique_ptr 对象转移过来
  myunique_ptr<T> &operator=(myunique_ptr &&ap) noexcept {
    if (this != &ap) {
      if (ptr_) {
        delete ptr_;
      }
      ptr_ = ap.ptr_;
      ap.ptr_ = nullptr;
    }
    return *this;
  }

  // 重置指针,释放当前持有的资源并指向新的资源
  void reset(T *ptr = nullptr) {
    if (ptr_) {
      delete ptr_;
    }
    ptr_ = ptr;
  }

  // 释放控制权,返回原始指针,并将内部指针置空
  T *release() {
    T *tmp = ptr_;
    ptr_ = nullptr;
    return tmp;
  }

  // 解引用运算符,返回指向的对象
  T &operator*() { return *ptr_; }

  // 成员访问运算符,返回原始指针
  T *operator->() { return ptr_; }

 private:
  T *ptr_;  // 内部指针,指向管理的资源
};

这是一个unique_ptr的简单实现;

  • 构造函数

    默认构造函数,初始化指针为nullptr,支持构造一个指向空的unique_ptr智能指针对象;

    带参构造函数为使用传入的指针初始化智能指针内部的指针;

  • 析构函数

    在对象销毁时检查内部指针是否为空,如果不为空则释放资源并输出析构信息;

  • 防拷贝机制

    通过delete关键字修饰拷贝构造和拷贝赋值运算符防止对象的拷贝从而保证该智能指针具有唯一权;

  • 移动语义

    • 移动构造函数

      移动构造函数将源对象的指针转移到新的对象,并将源智能指针对象的指针置空;

    • 移动赋值

      通过检查自赋值后释放当前对象的资源并转移源智能指针对象的指针,将源智能指针对象指针置空;

  • 成员函数

    • reset()

      重置内部指针,释放当前持有的资源并指向新的资源(默认为nullptr);

    • release()

      释放控制权,返回原始指针并将内部指针置空;

    • 解引用和成员访问运算符重载

      提供与原始指针相同的访问接口;

class Data {
 public:
  Data(int data = 0) : data_(data) { cout << "Data()" << endl; }
  ~Data() { cout << "~Data()" << endl; }

 public:
  int data_;
};

void func() {
  // 创建一个 myunique_ptr<Data> 对象 d1,管理一个新的 Data 对象,初始值为 10
  myunique_ptr<Data> d1(new Data(10));

  // 使用移动构造函数将 d1 的所有权转移到 d2,d1 现在为空
  myunique_ptr<Data> d2 = move(d1);  // 移动构造

  // 创建一个新的 Data 对象,初始值为 50,并将其指针赋给 d3
  Data* d3 = new Data(50);

  // 释放 d2 管理的资源,并返回原始指针,赋值给 d4
  Data* d4 = d2.release();

  // 输出 d4 指向的 Data 对象的值
  cout << "d4: " << d4->data_ << endl;

  // 手动删除 d4 指向的 Data 对象,释放资源
  delete d4;

  // 使用 reset() 方法将 d1 重置为管理新的 Data 对象 d3
  d1.reset(d3);

  // 输出 d1 指向的 Data 对象的值
  cout << "d1: " << d1->data_ << endl;
}

int main() {
  func();
  return 0;
}

/*
	运行结果为:
	$ ./test 
	Data()
	Data()
	d4: 10
	~Data()
	d1: 50
	~Data()
	~myunique_ptr()
*/

shared_ptr 引用计数

请添加图片描述

shared_ptrC++11提供的一种智能指针类型,用于管理共享资源的生命周期;

通过引用计数机制确保资源在最后一个引用它的shared_ptr对象被销毁时才会释放;

  • 引用计数

    shared_ptr内部维护一个引用计数用于记录有多少个shared_ptr实例共享同一个资源;

    这个引用计数是单独开的空间(new)确保对应的指向同一个资源的shared_ptr实例能够访问这个引用计数;

    每次拷贝构造或是赋值时引用计数将会增加,销毁一个shared_ptr智能指针实例或将其reset时引用计数减少,当引用计数减为零时所管理的资源被释放;

  • 线程安全

    引用计数的增加和减少是线程安全的,但对同一个shared_ptr对象的读写操作则需要额外增加同步机制;

  • 类型安全

    shared_ptr是类型安全的,能够确保指针类型正确以避免常见的指针类型转换错误;

class Data {
 public:
  Data(int data = 0) : data_(data) { cout << "Data()" << endl; }
  ~Data() { cout << "~Data()" << endl; }

 public:
  int data_;
};

void func() {
  shared_ptr<Data> d1(new Data(10));

  shared_ptr<Data> d2 = d1;  // 拷贝构造 此时d1 与 d2都指向同一个资源
  cout << "d1: " << d1->data_ << endl;
  cout << "d2: " << d2->data_ << endl;
}

int main() {
  func();
  return 0;
}
/*
	运行结果:
	$ ./test 
    Data()
    d1: 10
    d2: 10
    ~Data()
*/

在这个例子中使用Data类型指针实例化一个标准库中的shared_ptr智能指针对象,而后利用拷贝构造使得两个智能指针对象d1d2指向同一个空间,并对其进行打印;

最终结果为d1d2中的Datadata_成员相同,且两个智能指针对象只有最后的智能指针对象在调用析构函数后对资源进行释放;


shared_ptr 简单实现

请添加图片描述

template <class T>
class myshared_ptr {
 public:
  // 构造函数,用于初始化智能指针。接受一个原始指针 ptr。
  myshared_ptr(T *ptr) : ptr_(ptr) {}

  // 析构函数。在对象被销毁时调用。
  ~myshared_ptr() {
    // 检查 pcount_ 是否非空,减少引用计数。
    // 如果引用计数为 0,则释放资源,包括底层指针和引用计数器。
    if (pcount_ && --(*pcount_) == 0) {
      delete pcount_;
      delete ptr_;
      pcount_ = nullptr; // 防止悬挂指针
      ptr_ = nullptr;
    }
  }

  // 拷贝构造函数。创建一个新的 myshared_ptr 对象,并共享传入对象的资源。
  myshared_ptr(myshared_ptr<T> &sp) : ptr_(sp.ptr_), pcount_(sp.pcount_) {
    ++(*pcount_); // 增加引用计数,因为增加了一个新的共享对象。
  }

  // 拷贝赋值运算符。用于用另一个智能指针赋值给当前智能指针。
  myshared_ptr<T> operator=(const myshared_ptr<T> &sp) {
    if (&sp != this) { // 检查自我赋值
      // 减少当前对象的引用计数,如果为零则释放资源。
      if (--(*pcount_) == 0) {
        delete ptr_;
        delete pcount_;
      }
      // 将右侧对象的数据复制给当前对象,并增加引用计数。
      ptr_ = sp.ptr_;
      pcount_ = sp.pcount_;
      ++(*pcount_);
    }
    return *this; // 返回当前对象以支持链式赋值
  }

  // 移动构造函数。转移资源拥有权而不是拷贝,实现高效的资源管理。
  myshared_ptr(myshared_ptr<T> &&ap) : ptr_(ap.ptr_), pcount_(ap.pcount_) {
    ap.pcount_ = nullptr; // 清除原对象的指针和引用计数,避免悬挂指针
    ap.ptr_ = nullptr;
  }

  // 解引用运算符,返回所指向的对象。
  T &operator*() { return *ptr_; }

  // 成员访问运算符,返回内部存储的原始指针以便访问成员。
  T *operator->() { return ptr_; }

 private:
  T *ptr_;                // 存储管理的对象的原始指针
  int *pcount_ = new int(1); // 用于跟踪有多少 myshared_ptr 指向相同的资源

这是一个简单的shared_ptr智能指针实现;

  • 智能指针成员

    private:
      T *ptr_;                // 管理的对象的原始指针。
      int *pcount_ = new int(1); // 引用计数器,跟踪多少个 myshared_ptr 共享相同的对象。
    

    其中ptr_是类型为T*的指针,指向由智能指针管理的对象;

    pcount_是一个动态分配的整数指针,用于共享此对象的智能指针实例数的计数器,初始化为1,因为为带参构造,创建时存在一个所有者;

  • 构造函数

    public:
      myshared_ptr(T *ptr) : ptr_(ptr) {}
    

    构造函数接受一个原始指针将其存储在ptr_中,引用计数pcount_在声明时初始化为1;

    这意味着新创建的myshared_ptr独占管理权;

  • 析构函数

    ~myshared_ptr() {
      if (pcount_ && --(*pcount_) == 0) {
        delete pcount_;
        delete ptr_;
        pcount_ = nullptr; // 防止悬挂指针
        ptr_ = nullptr;
      }
    }
    

    每当shared_ptr类型对象被销毁时调用;

    首先检查引用计数是否存在并递减引用计数,如果递减后为0则表示没有其他shared_ptr实例共享这个资源从而安全地删除资源和计数器本身;

  • 拷贝构造函数

    myshared_ptr(myshared_ptr<T> &sp) : ptr_(sp.ptr_), pcount_(sp.pcount_) {
      ++(*pcount_);
    }
    

    当创建新的myshared_ptr实例并将其初始化为现有实例时调用;

    拷贝构造函数将会复制成员变量并增加引用计数表明当前有一个新的智能指针共享对象;

  • 拷贝赋值运算符重载

    myshared_ptr<T> operator=(const myshared_ptr<T> &sp) {
      if (&sp != this) {
        if (--(*pcount_) == 0) {
          delete ptr_;
          delete pcount_;
        }
        ptr_ = sp.ptr_;
        pcount_ = sp.pcount_;
        ++(*pcount_);
      }
      return *this;
    }
    

    用于将现有的myshared_ptr内容赋给另外一个现有的myshared_ptr对象并防止自我赋值,

    如果当前对象是唯一拥有者会减少引用计数并可能删除起拥有的资源和计数器;

    随后从右侧操作数复制数据并增加新的引用计数,将当前对象成为新的资源共享者;

  • 移动构造函数

    myshared_ptr(myshared_ptr<T> &&ap) : ptr_(ap.ptr_), pcount_(ap.pcount_) {
      ap.pcount_ = nullptr;
      ap.ptr_ = nullptr;
    }
    

    将右值引用(临时对象或将亡值)的所有权转移到新的智能指针对象中而不增加引用计数,在进行该操作后重置移动源(右值)以确保它在生命周期内不会无意中释放资源;

  • 操作符重载

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

    提供了解引用运算符和成员访问操作费提供了普通指针访问对应资源的接口;

class Data {
 public:
  Data(int data = 0) : data_(data) { cout << "Data()" << endl; }
  ~Data() { cout << "~Data()" << endl; }

 public:
  int data_;
};

void func() {
  myshared_ptr<Data> d1(new Data(10));

  myshared_ptr<Data> d2 = d1;  // 拷贝构造 此时d1 与 d2都指向同一个资源
  cout << "d1: " << d1->data_ << endl;
  cout << "d2: " << d2->data_ << endl;
}

int main() {
  func();
  return 0;
}
/*
	运行结果为:
	$ ./test 
    Data()
    d1: 10
    d2: 10
    ~Data()
*/

在该例子中使用了自定义的Data类对象指针实例化一个myshared_ptr智能指针d1而后利用拷贝构造实例化了一个d2;

其中d1d2共享同一块资源;

使用引用计数,两个智能指针的析构函数只有一个智能指针将会释放资源故~Data()只调用了一次;


shared_ptr 的循环引用问题与 weak_ptr

请添加图片描述

循环引用发生在两个或多个对象互相持有对方的shared_ptr的情况从而形成一个环;

当这种情况出现时即使这些对象不再被其他任何对象引用他们仍然不能被析构,因为各自的引用计数永远不会降到0;

struct ListNode {
  shared_ptr<ListNode> next;
  shared_ptr<ListNode> prev;

  ListNode() { cout << "ListNode()" << endl; }
  ~ListNode() { cout << "~ListNode()" << endl; }
};

int main() {
  shared_ptr<ListNode> n1(new ListNode);
  shared_ptr<ListNode> n2(new ListNode);
  n1->next = n2;
  n2->prev = n1;
  return 0;
}

/*
	运行结果为:
	$ ./test 
    ListNode()
    ListNode()
*/

在这个例子中创建了一个链表节点的ListNode类,其中该链表节点为双向链表的节点,当调用构造函数与析构函数时将会打印对应的构造与析构信息;

运行结果表明在这个例子中并没有调用析构函数,意味着节点没有成功被释放发生了内存泄漏的问题;

这张图片对应上面的例子;

其中n1n2中的prev智能指针实例管理同一块空间,n2n1中的next智能指针实例管理同一块空间;

其对应的引用计数也都为2;

n1n2在生命周期结束时将会调用析构函数,空间的释放需要根据两个智能指针所指向的引用计数决定,当两个智能指针实例调用析构函数时对应的引用计数都将减到1;

n1n2都调用析构函数后左边节点的管理权交由右边节点的prev智能指针实例进行管理,右边节点的管理权交由左边节点的next智能指针实例进行管理;

而只有左边节点被释放了对应的next智能指针实例才能被释放,右边节点被释放了对应的prev智能指针实例才能被释放从而形成一个环;

  • weak_ptr

    weak_ptrC++11所引入的一种智能指针,主要是用于解决shared_ptr的循环引用问题;

    其提供了一种弱引用机制,可以指向一个由shared_ptr管理的对象,但是不会参与引用计数的管理,因此使用weak_ptr可以有效打破循环引用问题而不会影响对象的生命周期管理;

    struct ListNode {
      weak_ptr<ListNode> next; // 使用weak_ptr代替shared_ptr
      weak_ptr<ListNode> prev;
    
      ListNode() { cout << "ListNode()" << endl; }
      ~ListNode() { cout << "~ListNode()" << endl; }
    };
    
    int main() {
      shared_ptr<ListNode> n1(new ListNode);
      shared_ptr<ListNode> n2(new ListNode);
      n1->next = n2;
      n2->prev = n1;
      return 0;
    }
    /*
    	运行结果为:
    	$ ./test 
    	ListNode()
    	ListNode()
    	~ListNode()
    	~ListNode()
    */
    

    在这个例子中将原先的shared_ptr换成了weak_ptr,运行结果表明析构函数被调用;

    也可用shared_ptr中的use_count成员函数观察对应的引用计数数值;

weak_ptr不是传统的智能指针,其不支持RAII;

template <class T>
class myshared_ptr {
 public:
    
	// ...
    // ...
  
   const T *get() const { return ptr_; } // 增加get函数用于myweak_ptr使用myshared_ptr进行构造

 private:
  T *ptr_;
  int *pcount_ = new int(1);
};

// ----------------- 

template <class T>
class myweak_ptr {
 public:
  myweak_ptr() : ptr_(nullptr) {}
  myweak_ptr(const myshared_ptr<T> &sp) : ptr_(sp.get()) {}

  myweak_ptr<T> &operator=(const myshared_ptr<T> &sp) {
    ptr_ = sp.get();
    return *this;
  }

  // 解引用运算符,返回指向的对象
  T &operator*() { return *ptr_; }

  // 成员访问运算符,返回原始指针
  T *operator->() { return ptr_; }

 private:
  const T *ptr_;
};

这段代码是weak_ptr智能指针的一个简单实现;

其主要功能为通过weak_ptr指向shared_ptr所管理的空间但不参与其引用计数;

struct ListNode {
  myweak_ptr<ListNode> next;
  myweak_ptr<ListNode> prev;

  ListNode() { cout << "ListNode()" << endl; }
  ~ListNode() { cout << "~ListNode()" << endl; }
};

int main() {
  myshared_ptr<ListNode> n1(new ListNode);
  myshared_ptr<ListNode> n2(new ListNode);

  n1->next = n2;
  n2->prev = n1;

  return 0;
}

/*
	运行结果为:
	$ ./test 
    ListNode()
    ListNode()
    ~ListNode()
    ~ListNode()
*/

在这个例子中使用了myshared_ptrmyweak_ptr来替代标准库里的智能指针,最终结果为节点的析构函数被调用,不存在内存泄漏问题;


智能指针的自定义删除器

请添加图片描述

现代智能指针提供了一个自定义的删除器使智能指针通过包含不同的对象来对其进行特殊的删除;

template <class U, class D> shared_ptr (U* p, D del);

shared_ptr为例,存在一个包含特定的类内模板函数的构造函数,支持传入一个自定义删除器对特殊的资源完成特殊的资源清理工作;

其传入的自定义删除器可以为任何可调用对象,lambda表达式,function包装器,bind绑定对象,仿函数等可调用对象;

class Data {
 public:
  Data(int data = 0) : data_(data) { cout << "Data()" << endl; }
  ~Data() { cout << "~Data()" << endl; }

 public:
  int data_;
};

int main() {
  shared_ptr<FILE> file(fopen("log.txt", "w"), [](FILE* fp) { fclose(fp); });
  // 使用定制删除器(lambda表达式)关闭文件

  shared_ptr<Data> dt(new Data[3], [](Data* d) { delete[] d; });
  // 使用定制删除器(lambda表达式)清理自定义类型数组
  return 0;
}

/*
	运行结果为:
	$ ./test 
    Data()
    Data()
    Data()
    ~Data()
    ~Data()
    ~Data()
*/

其自定义删除器可使用function包装器实现;

#include <functional>  // 需要包含这个头文件以使用 std::function

template <class T>
class myshared_ptr {
 public:
  myshared_ptr(T *ptr) : ptr_(ptr) {}

  myshared_ptr() : ptr_(nullptr), pcount_(nullptr) {}

  // 带自定义删除器的构造函数:初始化指针和删除器
  template <class D>
  myshared_ptr(T *ptr, D del) : ptr_(ptr), del_(del) {}

  ~myshared_ptr() {
    if (pcount_ && --(*pcount_) == 0) {
      delete pcount_;
      del_(ptr_); // 调用删除器
      pcount_ = nullptr;
      ptr_ = nullptr;
    }
  }

  myshared_ptr(const myshared_ptr<T> &sp) : ptr_(sp.ptr_), pcount_(sp.pcount_) {
    ++(*pcount_);
  }

  myshared_ptr<T> operator=(const myshared_ptr<T> &sp) {
    if (&sp != this) {
      if (pcount_ && --(*pcount_) == 0) {
        del_(ptr_); // 调用删除器
        delete pcount_;
      }
      ptr_ = sp.ptr_;
      pcount_ = sp.pcount_;
      ++(*pcount_);
    }
    return *this;
  }

  myshared_ptr(myshared_ptr<T> &&ap) : ptr_(ap.ptr_), pcount_(ap.pcount_) {
    ap.pcount_ = nullptr;
    ap.ptr_ = nullptr;
  }

  T &operator*() { return *ptr_; }

  T *operator->() { return ptr_; }

  const T *get() const { return ptr_; }

 private:
  T *ptr_;  
  int *pcount_ = new int(1);  
  std::function<void(T *)> del_ = [](T *ptr) { delete ptr; };  // 默认删除器为 delete
};

这个实现中使用了function包装器与lambda合作实现;

当用户传入一个删除器时默认删除器将会被替换;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1993983.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【独家原创】基于NRBO-Transformer多特征分类预测【24年新算法】 (多输入单输出)Matlab代码

【独家原创】NRBO-Transformer分类 Matlab代码 基于牛顿拉夫逊优化算法优化Transformer的数据分类预测&#xff0c;Matlab代码&#xff0c;可直接运行&#xff0c;适合小白新手 NRBO优化的超参数为&#xff1a;自注意力机制中的头数、正则化系数、初始化学习率 1.程序已经调试…

《知识点扫盲 · Redis 分布式锁》

&#x1f4e2; 大家好&#xff0c;我是 【战神刘玉栋】&#xff0c;有10多年的研发经验&#xff0c;致力于前后端技术栈的知识沉淀和传播。 &#x1f497; &#x1f33b; CSDN入驻不久&#xff0c;希望大家多多支持&#xff0c;后续会继续提升文章质量&#xff0c;绝不滥竽充数…

数据库索引设计原则

1. 概述 索引是优化数据库性能最重要的工具之一。但是&#xff0c;创建过多的索引或索引错误的列也会对性能产生负面影响。因此&#xff0c;在设计索引时遵循一定的原则很重要。 2. 原则A - 根据工作负载创建索引 创建高效索引最重要的原则是根据您的工作负载而不是表结构创…

数据库连接池的深入学习

为什么需要数据库连接池&#xff1f; 正常操作数据库需要对其进行连接&#xff0c;访问数据库&#xff0c;执行sql语句&#xff0c;断开连接。 创建数据库连接是一个昂贵的过程&#xff0c;在高并发的情况下&#xff0c;频繁的创建数据库的连接可能会导致数据库宕机。 有了连…

Leetcode JAVA刷刷站(8)字符串转换整数

一、题目概述 二、思路方向 要实现这个功能&#xff0c;我们可以遵循以下步骤来编写 myAtoi 函数&#xff1a; 去除前导空格&#xff1a;使用循环或字符串的 trim() 方法&#xff08;虽然直接操作字符串更高效的方式是使用循环&#xff09;。检查符号&#xff1a;记录第一个非…

TGANet部分复现

Kvasir-SEG复现结果 M e t h o d m I o U m D S C R e c a l l P r e c i s i o n F 2 P r a N e t − − − − − − 0.9663704860609511 − − T G A N e t 0.8331 0.8982 0.9132 − − 0.9029 \begin{array}{lccccr} Method&mIoU&mDSC&Recall&Precision&a…

5、Linux : 网络相关

OSI七层网络模型 TCP/IP四层 概念模型 对应网络协议 应用层&#xff08;Application&#xff09; HTTP、TFTP, FTP, NFS, WAIS、 表示层&#xff08;Presentation&#xff09; 应用层 Telnet, Rlogin, SNMP, Gopher 会话层&#xff08;Session&#xff09; SMTP…

ICETEK-DM6437-AICOM——CPU定时器及直流电机控制中断控制

一、设计目的&#xff1a; 1.1 CPU定时器程序设计&#xff1b; 1.2 2直流电机程序设计&#xff1b; 1.3 外中断。 二、设计原理&#xff1a; 2.1 定时器的控制&#xff1a; 在DM6437&#xff08;是一种数字信号处理器&#xff0c;DSP&#xff09;上使用其内部定时器和中断来…

设计模式-动态代理模式

目录 什么是代理模式&#xff1f; 为什么要用代理模式&#xff1f; 有哪几种代理模式&#xff1f; 动态代理&#xff08;jdk自带&#xff09;&#xff1a; 动态代理&#xff08;第三方库-cglib&#xff09;&#xff1a; 什么是代理模式&#xff1f; 代理模式给某一个对象提供…

Windows10不能直接拖拽文件到微信或者钉钉的解决办法【玖毅网】

不知道从何时起,微信、QQ和钉钉等相关软件,无法拖拽文件到对话窗口,拖拽的时候显示一个红色图标,可能是上次更新win之后导致的,所以嘛,系统真的不能设置自动更新,说不准哪些更新就把原设置覆盖或者关闭了,哎,吃一堑长一智吧,赶紧关闭自动更新,emmmm我在说我自己啊。…

日撸Java三百行(day17:链队列)

目录 一、队列基础知识 1.队列的概念 2.队列的实现 二、代码实现 1.链队列创建 2.链队列遍历 3.入队 4.出队 5.数据测试 6.完整的程序代码 总结 一、队列基础知识 1.队列的概念 今天我们继续学习另一个常见的数据结构——队列。和栈一样&#xff0c;队列也是一种操…

零基础5分钟上手谷歌云GCP核心云开发技能 - 利用语音AI服务搭建应用

简介&#xff1a; 欢迎来到小李哥全新谷歌云GCP云计算知识学习系列&#xff0c;适用于任何无云计算或者谷歌云技术背景的开发者&#xff0c;让大家零基础5分钟通过这篇文章就能完全学会谷歌云一个经典的服务开发架构方案。 我将每天介绍一个基于全球三大云计算平台&#xff0…

arcgis(shp)注记转CAD(dwg)文字

arcgis&#xff08;shp&#xff09;注记转CAD&#xff08;dwg&#xff09;文字方法如下&#xff1a; 1、添加shp文件&#xff0c;标注要素&#xff0c;然后选标注转注记 2、 点击文件夹图标打开文件夹&#xff0c;选择保存路径。&#xff08;提前需新建好文件地理数据库、数据…

Arm Linux 设置系统日期时间的方法

一、设置系统日期时间的方法 1.命令行工具 date 命令&#xff1a;是Linux系统中用于查看和设置系统时间的常用命令行工具。通过date -s选项&#xff0c;可以手动设置系统时间。 sudo date -s "YYYY-MM-DD HH:MM:SS"hwclock 命令&#xff1a;用于查询和设置硬件时钟…

8月8日复习内容(基础的文件IO操作)

man手册 主要分为以下几个章节&#xff1a; User Commands&#xff08;用户命令&#xff09;&#xff1a;这一章节包含了普通用户&#xff08;非root用户&#xff09;可以执行的命令。这些命令通常用于日常的文件管理、文本编辑、程序执行等任务。 System Calls&#xff08;系…

【JavaEE初阶】常见的锁策略及synchronized实现原理

目录 &#x1f333; 常见的锁策略 &#x1f6a9; 乐观锁 vs 悲观锁 &#x1f6a9; 重量级锁 vs 轻量级锁 &#x1f6a9; 自旋锁 vs 挂起等待锁 &#x1f6a9; 可重入锁 vs 不可重入锁 &#x1f6a9; 公平锁 vs 非公平锁 &#x1f6a9; 互斥锁 vs 读写锁 &#x1f384; …

2024年8月8日(python基础)

一、检查并配置python环境&#xff08;python2内置&#xff09; 1、检测是否安装 [rootlocalhost ~]# yum list installed| grep python [rootlocalhost ~]# yum -y install epel-release 2、安装python3 [rootlocalhost ~]# yum -y install python3 最新版3.12可以使用源码安…

数据结构.

1:基本大纲 数据结构、算法线性表&#xff1a;顺序表、链表、栈、队列树&#xff1a;二叉树、遍历、创建查询方法、排序方式 2:数据结构&#xff08;逻辑结构&#xff0c;存储结构&#xff0c;操作&#xff08;数据的运算&#xff09;&#xff09; 2.1&#xff1a;数据&#xf…

RabbitMQ面试题汇总

RabbitMQ面试题 一、RabbitMQ基础1. 什么是RabbitMQ&#xff0c;它的基本架构是怎样的&#xff1f;2. RabbitMQ支持哪些协议&#xff1f;3. 说一下AMQP协议&#xff1f;4. 为什么要使用RabbitMQ&#xff1f;5. MQ的应用场景有哪些&#xff1f;6. 解耦、异步、削峰是什么&#x…

【Linux之·工程构建·Cmake】

系列文章目录 文章目录 前言一、概述二、CMake的基本概念2.1 CMake的工作原理和基本组成部分2.2 CMakeLists.txt文件的结构和语法2.2.1 变量操作2.2.2 注释2.2.3 日志2.2.4 宏定义 2.3 CMakeLists.txt文件的作用 三、CMake的常用命令和变量3.1 常用的CMake命令和变量3.1.1 字符…