[C++11] 智能指针

news/2024/11/26 12:44:07/

         长路漫漫,唯剑作伴。        

目录

         长路漫漫,唯剑作伴。        

为什么需要智能指针

RAII

使用RAII思想管理内存

重载 * 和->

总结一下智能指针的原理:

C++的智能指针和拷贝问题

auto_ptr        (C++98)   ​编辑

auto_ptr的实现原理:管理权转移的思想,

unique_ptr        (C++11)

std::shared_ptr        (C++11)

        sheared_ptr 的简化实现

        关于循环引用的问题(Circular reference)

weak_ptr        打破循环引用(C++11)

        weak_ptr 的简化实现

定制删除器

定制删除器的使用

unique_ptr 定制删除器的模拟实现

相关链接 :


为什么需要智能指针

int div()
{int a, b;cin >> a >> b;if (b == 0)throw invalid_argument("除0错误");return a / b;
}void func()
{int* p1 = new int[10]; // 这里亦可能会抛异常int* p2 = new int[10]; // 这里亦可能会抛异常int* p3 = new int[10]; // 这里亦可能会抛异常int* p4 = new int[10]; // 这里亦可能会抛异常try{div();}catch (...){delete[] p1;delete[] p2;delete[] p3;delete[] p4;throw;}delete[] p1;delete[] p2;delete[] p3;delete[] p4;
}int main()
{try{func();}catch (const exception& e){cout << e.what() << endl;// ...}return 0;

        可以发现上面这段代码是典型的异常处理中可能会碰到内存泄漏或析构异常的例子,我们当然可以为每个new[]都尝试着抛出异常,但那样太麻烦了,所以我们引入了智能指针

RAII

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

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

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

使用RAII思想管理内存

// 使用RAII思想设计的SmartPtr类template<class T>
class SmartPtr{
public:SmartPtr(T* ptr = nullptr): _ptr(ptr){cout << "get " << _ptr << endl;}~SmartPtr(){if (_ptr)cout << "delet " <<_ptr<< endl;delete _ptr;}private:T* _ptr;
};void Func(){int* pa = new int(10);cout << " pa " << pa << endl;SmartPtr<int> sp1(pa);}
int main(){Func();return 0;
}

重载 * 和->

        上述的SmartPtr还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可 以通过->去访问所指空间中的内容,因此:SmartPtr模板类中还得需要将* 、->重载下,才可让其像指针一样去使用。

#include<iostream>
#include<memory>
using namespace std;//使用RAII 思想管理内存template<class T>
class SmartPtr{
public:SmartPtr(T* ptr = nullptr): _ptr(ptr){//cout << "get " << _ptr << endl;}~SmartPtr(){if (_ptr)//cout << "delet " <<_ptr<< endl;delete _ptr;}T& operator*(){ return *_ptr; }T* operator->(){ return _ptr; }private:T* _ptr;
};struct Date{int _year;int _month;int _day;
};int main(){SmartPtr<int> sp1(new int);*sp1 = 10;cout << *sp1 << endl;SmartPtr<Date> spDate(new Date);// 需要注意的是这里应该是spDate.operator->()->_year = 2018;// 本来应该是spDate->->_year这里语法上为了可读性,省略了一个->spDate->_year = 2060;spDate->_month = 6;spDate->_day = 14;cout << spDate->_year << "/" << spDate->_month << "/" << spDate->_day << endl;return 0;
}

        但是尽管如此,我们仍旧面临拷贝问题和析构问题,这里先抛出问题,文章后面会解释C++智能指针是如何解决这类的问题的,一步一步来,我们会解决拷贝问题,然后再解决析构问题(定制删除器

void copy_issue(){SmartPtr<int> sp1(new int);SmartPtr<int> sp2 = sp1;        //这里会析构2次而导致程序异常终止
}void destructor_issue(){SmartPtr<int> sp1(new int[10]);  //这里的析构存在类型不匹配,没办法正确的调用 delete[],//类似的还有文件描述符,也没办法正确 close()...
}

总结一下智能指针的原理:

  • RAII特性
  • 重载operator*和opertaor->,具有像指针一样的行为
  • 处理拷贝问题
  • 定制删除器

C++的智能指针和拷贝问题

auto_ptr        (C++98)   

auto_ptr的实现原理:管理权转移的思想,

        下面简化模拟实现了一份skate::auto_ptr来了解它的原理:

namespace skate{template<class T>class auto_ptr{public:auto_ptr(T* ptr):_ptr(ptr){}auto_ptr(auto_ptr<T>& sp)   //拷贝构造:_ptr(sp._ptr){// 管理权转移sp._ptr = nullptr;}auto_ptr<T>& operator=(auto_ptr<T>& ap){ //赋值重载// 检测是否为自己给自己赋值if (this != &ap){// 释放当前对象中资源if (_ptr)delete _ptr;// 转移ap中资源到当前对象中_ptr = ap._ptr;ap._ptr = NULL;}return *this;}~auto_ptr(){if (_ptr){cout << "delete:" << _ptr << endl;delete _ptr;}}// 像指针一样使用T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;};
}//结论:auto_ptr是一个失败设计,很多公司明确要求不能使用auto_ptr
int main(){skate::auto_ptr<int> sp1(new int);*sp1 = 10;skate::auto_ptr<int> sp2(sp1); // 管理权转移//sp1悬空*sp1 = 20;			//空指针异常*sp2 = 30;cout << *sp2 << endl;cout << *sp1 << endl;return 0;
}

unique_ptr        (C++11)

C++11中开始提供更靠谱的unique_ptr 


unique_ptr的实现原理:简单粗暴的防拷贝,下面简化模拟实现了一份UniquePtr来了解它的原理

// C++11库才更新智能指针实现
// C++11出来之前,boost搞除了更好用的scoped_ptr/shared_ptr/weak_ptr
// C++11将boost库中智能指针精华部分吸收了过来
// C++11->unique_ptr/shared_ptr/weak_ptr// unique_ptr/scoped_ptr
// 原理:简单粗暴 -- 防拷贝
namespace skate{template<class T>class unique_ptr{public:unique_ptr(T* ptr):_ptr(ptr){}~unique_ptr(){if (_ptr){cout << "delete:" << _ptr << endl;delete _ptr;}}// 像指针一样使用T& operator*(){return *_ptr;}T* operator->(){return _ptr;}unique_ptr(const unique_ptr<T>& sp) = delete;unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;private:T* _ptr;};
}

std::shared_ptr        (C++11)

C++11中开始提供更靠谱的并且支持拷贝shared_ptr 
shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。

  1. shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。
  2. 对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
  3. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源
  4.  如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。

class MyClass{
public:MyClass(){std::cout << "MyClass constructor called" << std::endl;}~MyClass(){std::cout << "MyClass destructor called" << std::endl;}
};int main(){std::shared_ptr<MyClass> ptr1(new MyClass);  //调用构造函数std::cout << ptr1.get() << " _ptr-> ptr1.use_count() = " << ptr1.use_count() << std::endl;std::shared_ptr<MyClass> ptr2 = ptr1;//调用赋值重载    计数++std::cout << ptr2.get() << " _ptr-> ptr2.use_count() = " << ptr2.use_count() << std::endl;std::shared_ptr<MyClass> ptr3(ptr1);//调用拷贝构造  计数++std::cout << ptr3.get() << " _ptr-> ptr3.use_count() = " << ptr3.use_count() << std::endl;ptr1.reset(); //计数--ptr2.reset();//计数--ptr3.reset();//计数为0  调用析构std::cout << ptr3.get() << " _ptr-> ptr3.use_count() = " << ptr3.use_count() << std::endl;return 0;
}

 

sheared_ptr 的简化实现

	template<class T>class shared_ptr{public:void Release()// 释放指向的对象和引用计数{if (--(*_pCount) == 0 && _ptr) //计数为0进行析构{cout << "delete" << _ptr << endl;delete _ptr;_ptr = nullptr;delete _pCount;_pCount = nullptr;}}// RAII思想shared_ptr(T* ptr):_ptr(ptr), _pCount(new int(1)){}~shared_ptr(){Release();//析构先时候 --计数,后确认析构}shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr), _pCount(sp._pCount){(*_pCount)++;}// sp1 = sp3shared_ptr<T>& operator=(const shared_ptr<T>& sp){//if (this != &sp)if (_ptr != sp._ptr){Release();//先 --计数,后确认析构_ptr = sp._ptr;_pCount = sp._pCount;++(*_pCount);}return *this;}// 像指针一样T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T* get() const{return _ptr;}int use_count(){return *_pCount;}private:T* _ptr;int* _pCount;};

        需要注意的是,这只是一份简单的shared_ptr模拟实现,并没有考虑线程安全等方面的问题。实际上,C++11中的标准库中的shared_ptr实现更加复杂和完善,具有线程安全等特性。

关于循环引用的问题(Circular reference)

        循环引用是指两个或多个智能指针对象直接或间接地引用彼此,形成一个引用链,导致资源无法正确释放。当涉及的对象之间的引用计数永远不会降为0时,就会发生循环引用问题。

        这种情况下,我们可以使用weak_ptr来打破循环引用,从而避免内存泄漏的问题。

 循环引用分析: 

  1.  node1和node2两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动delete。
  2. node1的_next指向node2,node2的_prev指向node1,引用计数变成2。
  3. node1和node2析构,引用计数减到1,但是_next还指向下一个节点。但是_prev还指向上一个节点。
  4. 也就是说_next析构了,node2就释放了。
  5. 也就是说_prev析构了,node1就释放了。
  6. 但是_next属于node的成员,node1释放了,_next才会析构,而node1由_prev管理,_prev属于node2成员,所以这就叫循环引用,谁也不会释放。

weak_ptr        打破循环引用(C++11)

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<T>& operator=(const shared_ptr<T>& sp){if (_ptr != sp.get()){_ptr = sp.get();}return *this;}// 像指针一样T& operator*(){return *_ptr;}T* operator->(){return _ptr;}public:T* _ptr;};
}

定制删除器

定制删除器的使用

class Date{
public:~Date(){cout << "~Date()" << endl;}
private:int _year = 1;int _month = 1;int _day = 1;
};// unique_ptr/shared_ptr  默认释放资源用的delete
// 如何匹配申请方式去对应释放呢?template<class T>
struct DeleteArray{void operator()(T* ptr){cout << "delete[]" << ptr << endl;delete[] ptr;}
};template<class T>
struct Free{void operator()(T* ptr){cout << "free" << ptr << endl;free(ptr);}
};struct Fclose{void operator()(FILE* ptr){cout << "fclose" << ptr << endl;fclose(ptr);}
};
void test_unique(){cout << "\ntest_unique()\n";// unique_ptr传类型std::unique_ptr<Date> up1(new Date);std::unique_ptr<Date, DeleteArray<Date>> up2(new Date[5]);std::unique_ptr<Date, Free<Date>> up3((Date*)malloc(sizeof(Date) * 5));std::unique_ptr<FILE, Fclose> up4((FILE*)fopen("Test.cpp", "r"));
}
void test_shared(){cout << "\ntest_shared()\n";// shared_ptr传对象std::shared_ptr<Date> sp1(new Date[5], DeleteArray<Date>());std::shared_ptr<Date> sp2(new Date[5], [](Date* ptr){cout << "lambda delete[] " << ptr << endl;; delete[] ptr; });
}
//定制删除器int main(){test_unique();test_shared();return 0;
}

unique_ptr 定制删除器的模拟实现

ps:

C++11中的标准库中的shared_ptr实现更加复杂和完善,具有线程安全等特性。封装的也更加彻底,这里就不写模拟实现了

相关链接 :

https://cplusplus.com/reference/memory/auto_ptr/

https://cplusplus.com/reference/memory/unique_ptr/

https://cplusplus.com/reference/memory/shared_ptr/

https://cplusplus.com/reference/memory/weak_ptr/


http://www.ppmy.cn/news/409186.html

相关文章

20分钟做一套采购审批系统

1、设计输入模板 excel画表格界面 # 公式代表新建时以默认值代替 2、设置单元格为签名控件 双击单元格后&#xff0c;会默认显示当前用户的信息,用于签名 3、设置要合计的数据 生成的合计公式会默认放到下一行 4、设置单元格的ID与标题&#xff0c;在添加或者删除行或者列时&am…

C/C++基础讲解(一百一十三)之经典篇(宏#define命令)

C/C++基础讲解(一百一十三)之经典篇(宏#define命令) 程序之美 前言 很多时候,特别是刚步入大学的学子们,对于刚刚开展的计算机课程基本上是一团迷雾,想要弄明白其中的奥秘,真的要花费一些功夫,我和大家一样都是这么啃过来的,从不知到知知,懵懂到入门,每一步都走的很…

java面经03-虚拟机篇-jvm内存结构垃圾回收、内存溢出类加载、引用悲观锁HashTable、引用finalize

文章目录 虚拟机篇1. JVM 内存结构2. JVM 内存参数3. JVM 垃圾回收4. 内存溢出5. 类加载6. 四种引用7. finalize 虚拟机篇 1. JVM 内存结构 要求 掌握 JVM 内存结构划分尤其要知道方法区、永久代、元空间的关系 结合一段 java 代码的执行理解内存划分 执行 javac 命令编译源…

Ubuntu有线宽带网络连接

本博客指导有线网络连接&#xff0c;请插入网线后按照本教程进行网络连接。 1.ctrlaltT 打开终端 2.输入nmcli con edit type pppoe con-name "My DSL" &#xff08;“My DSL”是你设置的网络的名字&#xff09; 3.输入set pppoe.username ISPUsername ISPUsername…

计算机网络属性设置方法,电脑网络连接属性怎么设置

方法1在安装系统时对Internet连接进行设置在安装WindowsXP过程中&#xff0c;进行Internet连接设置时&#xff0c;可以按提示输入用户名和密码并设置成开机自动连接宽带&#xff0c;这样在安装完成后每次开机就能自动连接宽带了。方法2将宽带连接添加到启动组首先&#xff0c;设…

如何设置网络一直连接到服务器,windows10系统设置网络连接到服务器的方法

正常情况下&#xff0c;网络运营商ISP都会为上网用户提供一个账号和密码。如果他们使用的是PPPOE协议&#xff0c;那么在本机单除设置上网时就得添加一个宽带连接才可以。下面&#xff0c;系统城小编给大家分享一些关于win10设置网络连接到服务器的方法。感兴趣的朋友们不妨一起…

计算机如何设置网络,如何设置宽带连接

在这个互联网时代里,现在很多家庭都在使用网络,那么你知道如何设置宽带连接吗?很多电脑新手都不知道电脑的宽带连接如何去设置&#xff0c;本文将以图文方式介绍如何创建新的宽带连接。 如何设置宽带连接? 1、首先打开开始菜单&#xff0c;在开始菜单中打开控制面板 &#xf…

如何设置宽带自动连接网络?

每次打开电脑&#xff0c;都要点击宽带连接才可以上网&#xff0c;是很麻烦的重复劳动&#xff0c;下面介绍一种方法&#xff0c;开机后自动建立宽带连接&#xff0c;很方便&#xff0e; 具体方法是&#xff1a;右键点“网上邻居”——“属性”&#xff0c;进入“网络连接”窗口…