C++ 智能指针的简单原理

  • 为什么会有智能指针的原因
    • delete引起的内存泄漏
  • 智能指针的使用及其原理
    • RAII
    • auto_ptr
    • std::unique_ptr
    • std::shared_ptr(线程安全)
    • 删除器
  • weak_ptr
  • RAII 设计的守卫锁

所引用的头文件 : #include<memory>

为什么会有智能指针的原因

下面这个代码,当主函数调用Fun()的时候,在Fun函数内部使用了new 出来一个数组,new的作用和C语言中的malloc差不多,都是在堆中开辟相应大小的空间。

但是堆中开辟的空间是需要我们手动去释放的,不然就会出现内存泄漏的问题

#include "vld.h"
#include "head.hpp"void Fun()
{int* arr = new int[10];
}int main()
{Fun();return 0;
}

delete引起的内存泄漏

有时候加了delete却还是会出现内存泄漏,但是不加delete却又不会出现内存泄漏

  • 加了delete却还是会出现内存泄漏
void Fun()
{int* arr = new int[100];delete arr;//delete[] arr;
}

对于基本的数值类型,delete 和 delete[]可以说是没有区别的,因为new在开辟空间的时候,会记录所开辟空间的大小,然后delete的时候可以正确释放内存。

这是由于数值类型没有析构函数,所以在delete的时候,不需要调用析构函数来释放其他的指针,但是对于C++中的自定义对象,就像string,当使用new申请了自定义对象类型的数组string*,那么在他析构的时候需要做两件事:

  1. 释放最初申请的那部分空间
  2. 调用析构函数完成清理工作


那么问题就是出现在了析构函数中,我们自定义一个类,打印析构函数的情况(dev编译器下这种情况不会报崩溃的错误,vs直接崩掉)


也就是说,对于自定义对象数组的指针,我们析构的时候如果是delete arr,那么造成的结果就是new所开辟的空间被释放了,但是我们在对象中的清理函数却没有全部执行,只是执行了0号下标的析构函数(因为arr 即为 0号下标的地址)。


这样,当我们自定义对象中还存在new的对象后,还是会存在内存泄漏的,所以对于自定义类型的对象,需要使用delete[],在释放空间的同时,逐一调用每个对象的析构函数

  • 不加delete却又不会出现内存泄漏
int main()
{int* arr = new int[100];return 0;
}

在这种情况下,arr数组的作用域是main函数的开始到结束,也就是生命周期是跟随整个进程的。当函数结束的时候,操作系统会自动回收掉,也就不算内存泄漏了。

所以对于这种忘记delete以及delete不完全的问题,C++有个智能指针的概念,就是用一个类对指针进行封装,这样出了作用域后,就会自动调用这个类的析构函数,不用我们手动去delete了。

内存泄漏以及内存泄漏的危害:C&C++内存管理(如何检测内存泄漏)

智能指针的使用及其原理

RAII

RAII(Resource Acquisition Is Initialization),也称为资源获取就是初始化,是C++语言的一种管理资源、避免泄漏的惯用法。C++标准保证任何情况下,已构造的对象最终会销毁,即它的析构函数最终会被调用。简单的说,RAII 的做法是使用一个对象,在其构造时获取资源,在对象生命期控制对资源的访问使之始终保持有效,最后在对象析构的时候释放资源。(百度百科)


就是利用了面向对象中封装的特性,构造即初始化,析构即释放。将指针的两个过程封装在一个类中,让他的初始化和释放不用我们操心,都交给类去完成。

auto_ptr

C++98版本的库中就提供了auto_ptr的智能指针,他的设计思想不是很完美,主要就是一个权限转移的原理。

我们在使用指针的时候,会有一些拷贝的操作

int* pa = new int;
int* pb = pa;

这时候需要我们在类中重载=,我们一般的拷贝函数,就是把这个指针的地址拷贝过去,让两个类中的指针指向同一块地址,也就是这样的

     // = 赋值  有BUG的写法auto_ptr<T>& operator=(auto_ptr<T>& obj){if (_ptr != obj._ptr){_ptr = obj._ptr;}return *this;}

但是这样有个BUG,就是析构函数的调用,因为两个类并不相同,所以作用域结束的时候,调用析构函数时,会对同一块地址调用两次析构函数,也就是调用了两次delete,就会导致越界访问的


所以,C++98的auto_ptr智能指针中,做出了一个改变,那就是发生拷贝后,进行权利转移

也就是说,当发生拷贝之后,原本的智能指针就不能再指向之前的地址了,而是被置为了nullptr。保证了同一个对象,只能同时由一个智能指针进行管理。这样的设计有点不靠谱

之后我们还需要重载*->

 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>& obj):_ptr(obj._ptr){obj._ptr = nullptr;}// = 赋值auto_ptr<T>& operator=(auto_ptr<T>& obj){if (_ptr != obj._ptr){if (_ptr != nullptr){delete _ptr;_ptr = nullptr;}_ptr = obj._ptr;obj._ptr = nullptr;}return *this;}T* operator*(){return _ptr;}T& operator&(){return *_ptr;}private:T*    _ptr;};

std::unique_ptr

C++11中提出了unique_ptr(参考了boost库中的scoped_ptr的实现),他的实现原理:简单粗暴的防拷贝

也就是不允许使用拷贝对象的函数,即只进行声明,不进行实现

     //C++ 11unique_ptr<T>& operator=(unique_ptr<T>& obj) = delete;unique_ptr(unique_ptr<T>& obj) = delete;private://C++ 98unique_ptr<T>& operator=(unique_ptr<T>& obj);unique_ptr(unique_ptr<T>& obj);

std::shared_ptr(线程安全)

参考boost库中的shared_ptr/shared_array,实现了计数器版本的std::shared_ptr(支持拷贝对象)。

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

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

这里需要注意,我们的计数器也得是一个指针,不然无法做到一致性(引用也不行,因为引用只能在初始化的时候调用一次)

 template<class T>class share_point{public:share_point(T* ptr):_ptr(ptr), _pCount(new int(1)){}share_point(share_point<T>& tp):_ptr(tp._ptr), _pCount(tp._pCount){(*_pCount)++;}~share_point(){if (--(*_pCount) == 0 && _ptr){cout << "delete ing " << endl;delete _pCount;_pCount = nullptr;delete _ptr;_ptr = nullptr;}}share_point<T>& operator=(const share_point<T>& tp){if (_ptr != tp._ptr){//防止自己本身就是最后一个资源对象if (--(*_pCount) == 0){delete _pCount;delete _ptr;}*_pCount = tp._pCount;_ptr = tp._ptr;}return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T* GetPtr()const{return _ptr;}private:int* _pCount;T* _ptr;};

这里对计数器的++,–,以及赋值是原子性的吗?

很显然不是,在多线程的情况下还是会出现意外的情况。所以我们在对临界资源(计数器和指针变量)进行访问的时候,需要加锁

为了避免耦合度太高,我们对临界资源的访问进行封装

 private:void add_ref_count(){_pMutex->lock();(*_pCount)++;_pMutex->unlock();}void Release(){bool deleteMutex = false;_pMutex->lock();if (--(*_pCount) == 0 && _ptr){cout << "delete ing " << endl;delete _pCount;_pCount = nullptr;delete _ptr;_ptr = nullptr;deleteMutex = true;}_pMutex->unlock();if (deleteMutex == true)delete _pMutex;}

由于互斥锁的底层也是不支持拷贝的,所以我们对互斥锁变量也应该是一个指针

 template<class T>class share_point{public:share_point(T* ptr):_ptr(ptr), _pCount(new int(1)), _pMutex(new mutex()){}share_point(share_point<T>& tp):_ptr(tp._ptr), _pCount(tp._pCount), _pMutex(tp._pMutex){add_ref_count();}~share_point(){//线程安全Release();}share_point<T>& operator=(const share_point<T>& tp){if (_ptr != tp._ptr){Release();*_pCount = tp._pCount;add_ref_count();_ptr = tp._ptr;}return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T* GetPtr()const{return _ptr;}private:void add_ref_count(){_pMutex->lock();(*_pCount)++;_pMutex->unlock();}void Release(){bool deleteMutex = false;_pMutex->lock();if (--(*_pCount) == 0 && _ptr){cout << "delete ing " << endl;delete _pCount;_pCount = nullptr;delete _ptr;_ptr = nullptr;deleteMutex = true;}_pMutex->unlock();if (deleteMutex == true)delete _pMutex;}private:int* _pCount;T* _ptr;mutex* _pMutex;};

删除器

他的作用就是给那些不是new 开辟出来的对象,一个可以进行析构释放空间的函数

template<class T>
struct Freearr{void operator()(T* ptr){cout << "free " << ptr << endl;free(ptr);}
};void test2(){Freearr<int> farr;std::shared_ptr<int> sp((int*)malloc(sizeof(int)), farr);//STL 中的 shared_ptr
}

weak_ptr

如果share_ptr在使用时,他的类型是一个链表的节点这样的,那么会出现什么问题?

struct node
{int _data;point::share_point<node> _prev;point::share_point<node> _next;~node(){ cout << "~node()" << endl; }
};
int main()
{point::share_point<node> node1(new node);point::share_point<node> node2(new node);cout << node1.GetPtr() << endl;cout << node2.GetPtr() << endl;node1->_next = node2;node2->_prev = node1;return 0;
}


在还没有进行析构的时候,share_ptr所管理的指针是内部是这样的,两个指针的内部的计数器分别为2

这个时候,因为两个空间都分别被两个智能指针管理着,但是析构的时候都分别只调用了一次,也就是*_pCount计数器分别只减去了node1node2两个share_ptr所管理的计数,但是作为链表,他还有 _pre_next

  • node1 节点_next指针指向的是node2节点的空间,他并没调用析构函数
  • node2 节点_pre指针指向的是node1节点的空间,他也没调用析构函数

所以说节点所指的空间并没有被释放,所以这就造成了内存泄漏

解决方法:使用 weak_ptr

原理: node1->_next = node2node2->_prev = node1 时weak_ptr的_next和_prev不会增加node1和node2的引用计数

 template<class T>class weak_ptr{public:weak_ptr() = default;weak_ptr(const share_point<T>& tp):_ptr(tp.GetPtr()){}weak_ptr<T>& operator=(const share_point<T>& tp){_ptr = tp.GetPtr();return &this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;};

RAII 设计的守卫锁

守卫锁的作用:就是我们可以不用刻意的在每一个函数作用域可能结束的地方进行锁的释放。

当我们使用互斥锁的时候,为了避免函数退出作用域后没有解锁的情况,我们需要在每一个函数可能终止的地方都需要进行解锁。要是有没有考虑到的地方,那么很容易出现函数退出了,但是锁资源没有释放的情况

  • 作为互斥锁,他是不能进行拷贝和赋值的,所以我们在设计的时候也需要禁用拷贝函数
  • 互斥锁之间无法拷贝和赋值,所以对于成员变量中的互斥锁,我们需要使用引用
 //守卫锁template<class Mutex>class lock_guard{public:lock_guard(const Mutex& mu):_mutex(mu){cout << "lock ing" << endl;_mutex.lock();}~lock_guard(){cout << "unlock ing" << endl;_mutex.unlock();}//不允许拷贝lock_guard(const lock_guard<Mutex>& obj) = delete;lock_guard<mutex> operator=(const lock_guard<mutex>& obj) = delete;private:Mutex& _mutex;//锁不可以赋值,只能使用引用类型};

C++ 智能指针的简单原理相关推荐

  1. 智能指针shared_ptr的原理、用法和注意事项

    智能指针shared_ptr的原理.用法和注意事项 1 前言 2 shared_ptr原理 3 shared_ptr的基本用法 3.1 初始化 3.2 获取原始指针 4 智能指针和动态数组 4.1 c ...

  2. C++智能指针及其简单实现

    原文:http://www.cnblogs.com/xiehongfeng100/p/4645555.html C++智能指针及其简单实现 本文将简要介绍智能指针shared_ptr和unique_p ...

  3. 智能指针(一)—— 智能指针的底层原理(RAII特性)

    我们使用 new关键字 或者 malloc函数 开辟一块空间时,因为这块空间是在堆上开辟的,如果不手动释放,即便出了作用域,这块空间也依然存在,这个时候就会造成内存泄漏. 为了保证资源的释放,我们可以 ...

  4. 【C++ 语言】智能指针 引入 ( 内存泄漏 | 智能指针简介 | 简单示例 )

    文章目录 I . 智能指针 引入 II . 智能指针 简介 III . 智能指针 简单示例 I . 智能指针 引入 1 . 示例前提 : 定义一个 Student 类 , 之后将该类对象作为智能指针指 ...

  5. C++ — 智能指针的简单实现以及循环引用问题

    http://blog.csdn.net/dawn_sf/article/details/70168930 智能指针 _________________________________________ ...

  6. C++ 几种智能指针的简单实现

    #pragma once // 智能指针 // 定义个类来封装资源的分配和释放,在构造 函数完成资源的分配和初始化,在析构函数完成资源的 // 清理,可以 保证资源的正确初始化和释放. // 这里简单 ...

  7. C++智能指针(一)智能指针的简单介绍

    https://blog.csdn.net/nou_camp/article/details/70176949 C++智能指针  在正式了解智能指针前先看一下下面的一段代码 #include<i ...

  8. 【c++复习笔记】——智能指针详细解析(智能指针的使用,原理分析)

  9. Android系统的智能指针(轻量级指针、强指针和弱指针)的实现原理分析【转】...

    Android系统的运行时库层代码是用C++来编写的,用C++ 来写代码最容易出错的地方就是指针了,一旦使用不当,轻则造成内存泄漏,重则造成系统崩溃.不过系统为我们提供了智能指针,避免出现上述问题,本 ...

最新文章

  1. 使用SecureCRT设置linux系统登录的ssh公钥认证
  2. 数学是什么?_题跋—数学是什么?
  3. Linux内核网络丢包查看工具dropwatch的安装和使用
  4. 程序员为什么要单身?
  5. HTML导航页面结构
  6. 如何查找SAP Fiori UI上某个字段对应的底层数据库表
  7. 李倩星r语言实战_《基于R的统计分析与数据挖掘》教学大纲
  8. MyEclipse 2015 运行tomcat 内存溢出的解决方法
  9. oracle lpad 字符集,oracle Lpad()函数和Rpad()函数的用法
  10. 后台传一个状态值,如果在vue设置成正常停用?
  11. android怎么实现记住密码功能,Android App“记住密码”功能的实现逻辑
  12. mysql sql trace_SQL_TRACE及 Tkprof用法以及问题分析
  13. 少儿编程之Scratch入门汇总篇
  14. nodejs--数据库与身份验证:初识数据库、安装并配置 MySQL、MySQL 的基本使用、SQL语法、在项目中操作 MySQL
  15. 发票信息批量提取到 excel 软件 2.3
  16. 【杂】孔明锁6根解法 九连环的拆卸方法及还原
  17. 如何协助企业IT架构转型
  18. poco不断重启?看这6点就够了
  19. 海康、大华、华为等GB28181国标平台向上级联给LiveGBS GB28181平台的操作示例
  20. 【Spark ML】第 2 章: Spark和Spark简介

热门文章

  1. web前端实习第一天的内心os
  2. 谷歌大脑的-Swish-激活函数与-ReLU-激活函数对比
  3. 闪电演讲,黑大师,这周的 Hack for Wuhan 还能怎么浪?
  4. 自然辩证法与计算机科学与技术,自然辩证法和科学技术有什么关系
  5. 硬盘可用空间的计算方法
  6. CocosCreator | 给节点施加力、冲量、防止穿墙
  7. 抖音很火的卡通表白动态页面
  8. php文本聊天室源码,PHP文本聊天室PHP
  9. 缺少移动驾驶舱构建的经验?这家银行清算中心这样选择
  10. php添加超链接到html,总结几种实现超链接html代码