让内存无处可逃:智能指针[C++11]

2023-12-10 05:12

本文主要是介绍让内存无处可逃:智能指针[C++11],希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

智能指针

文章目录

  • 智能指针
    • 前言
    • RAII
    • 什么是智能指针
      • 智能指针的应用示例
    • C++98的auto_ptr
    • 共享型智能指针:shared_ptr
      • shared_ptr的使用
        • 初始化
        • 获取原生指针
        • 指定删除器
          • 默认删除器default_delete
          • 指定删除器
          • 指定删除器管理动态数组
      • shared_ptr的伪实现
      • shared_ptr的注意事项
        • 避免一个原始指针初始化多个shared_ptr
        • 不要在函数实参中创建shared_ptr
        • 不要通过shared_ptr返回this指针
        • 避免循环引用
    • 独占型智能指针:unique_ptr
      • unique_ptr的删除器
        • 默认删除器
        • 指定删除器
    • 弱引用的智能指针:weak_ptr
      • weak_ptr的基本用法
        • 获取检测资源引用计数
        • 判断是否释放
        • 获取监测的shared_ptr
      • weak_ptr返回this指针
      • weak_ptr解决循环引用问题
    • 智能指针管理第三方库分配内存
    • 总结

前言

C#和Java中有自动垃圾回收机制,因此,在C#和Java中,内存管理不是大问题。但是C++语言没有垃圾回收机制,必须自己去释放分配的堆内存,否则就会内存泄露。相信大部分C++开发人员都遇到过内存泄露的问题,而查找内存泄露的问题往往要花大量的精力。为了解决这个问题C++11提供了3种智能指针: shared_ptr、unique_ptr和weak_ptr,使用时需要引用头文件<memory>,本文将分别介绍这3种智能指针。

RAII

在这里插入图片描述

RAII(Resource Acquisition Is Initialization)是由c++之父Bjarne Stroustrup提出的,他说:使用局部对象来管理资源的技术称为资源获取即初始化(RAII);这里的资源主要是指操作系统中有限的东西如内存、网络套接字等等,局部对象是指存储在栈的对象,它的生命周期是由操作系统来管理的,无需人工介入。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:

  • 不需要显式地释放资源。

  • 采用这种方式,对象所需的资源在其生命期内始终保持有效

什么是智能指针

智能指针是一个模板类,支持创建任意类型的指针对象,当对象生命周期结束时,自动调用其析构函数释放资源。

下面给出了一份智能指针的框架,当然它显然不能满足我们对于避免内存泄露的需求。

template <class T>
class SmartPtr
{
public:SmartPtr(T *ptr) : _ptr(ptr) {}T &operator*(){return *_ptr;}T *operator->(){return &(*_ptr);}~SmartPtr(){if (_ptr){cout << "delete ptr" << endl;::delete _ptr;}}private:T *_ptr;
};

智能指针的应用示例

尽管我们上面给出的样例代码不够完善,但在某些情境下已经可以处理一些问题了。

我们用智能指针创建一个对象,不手动释放内存空间,观察程序结束时的结果。

#include <iostream>
#include <memory>
using namespace std;
#include "SmartPointer.h"int divide(int x, int y)
{if (!y){throw invalid_argument("Divide Zero Error\n");}return x / y;
}
int main()
{SmartPtr<int> p(new int(10));int x = 1, y = 0;try{cout << divide(x, y);}catch (exception &e){cout << e.what();}return 0;
}

运行结果

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

由于对象在栈上,当其生命周期结束时,都会调用其析构函数,而我们内存的释放托管给了析构函数,从而避免了手动释放内存空间。

那么我们上面智能指针的缺陷在哪呢?

int main()
{int *pt = new int(10);SmartPtr<int> p1(pt);SmartPtr<int> p2(p1);return 0;
}

当两个智能指针管理同一块内存空间时,两次析构对应两次delete,导致了程序崩溃,可见是智能指针绝非我们样例中的那么简单,那么C++11是如何解决的呢?

C++98的auto_ptr

对于内存泄漏问题早在C++98中就有了解决方案,它在委托释放的基础上加了一条规则——管理权转移,即,当出现像我们前面智能指针的拷贝时,被拷贝对象的指针就会被置空。(这种处理方案显然也有漏洞)

下面是auto_ptr拷贝构造函数的代码

//关于release的定义
release() throw()
{element_type* __tmp = _M_ptr;_M_ptr = 0;return __tmp;
}
auto_ptr(auto_ptr& __a) throw() : _M_ptr(__a.release()) { }

可见当调用拷贝构造时,被拷贝对象的指针被赋空,我们很容易找到其破绽。

int main()
{int *pt = new int(10);std::auto_ptr p1(pt);std::auto_ptr p2(p1);cout << *p1;return 0;
}

当p2拷贝p1后,我们再访问p1管理的内容,程序直接就崩溃了。那么C++11是如何解决的呢?

共享型智能指针:shared_ptr

shared_ptr的解决方案是引用计数,每一个shared_ptr支持拷贝,每一个拷贝都指向相同的内容,当最后一个shared_ptr对象析构时才会释放管理的内存。

我们可以在shared_ptr基类的源码内找到如下代码段:

//shared_ptr_base.h
__shared_count<_Lp>  _M_refcount;    // Reference counter.

可见shared_ptr底层是专门封装了一个引用计数器类的,我们其实可以通过指针来伪实现一下。

shared_ptr的使用

初始化

可以通过构造函数、std::make_shared<T>辅助函数和reset来初始化shared_ptr。

构造函数、std::make_shared<T>初始化

std::make_shared<T>相对来说更为高效

    std::shared_ptr<int> p(new int(1));std::shared_ptr<int> p1 = p;std::shared_ptr<int> ptr;ptr.reset(new int(1));if (ptr){std::cout << "ptr isn't null" << std::endl;}std::shared_ptr<int> p2 = std::make_shared<int>(1);std::cout << "value of p2 is : " << *p2;//ptr isn't null//value of p2 is : 1

不要直接将原生指针赋值给智能指针。

//错误用法
std::shared_ptr<int> p = new int(1);

reset初始化

shared_ptr调用reset接口时,如果该shared_ptr有值,那么引用计数-1

    std::shared_ptr<A> p = std::make_shared<A>(A());p.reset();system("pause");//~A()
//~A()

我们可以看到reset的源码,其实就是用一个临时对象去和当前shared_ptr做swap,函数结束,临时对象自动析构

//shared_ptr_base.h
void reset() noexcept
{ __shared_ptr().swap(*this); }
获取原生指针

原生指针我们可以调用get接口来获取。

    std::shared_ptr<int> p = std::make_shared<int>(1);int *p1 = p.get();std::cout << *p1;system("pause");
指定删除器
默认删除器default_delete

shared_ptr当引用计数为0时,会调用删除器来释放托管内存,默认删除器为default_delete,它只是对delete做了一层封装

指定删除器

shared_ptr初始化时还可以指定删除器。当shared_ptr的引用计数为1时会自动调用该删除器,删除器可以是函数也可以是lambda表达式。

//void MyDeleter(int *p)
//{
//    delete p;
//    std::cout << "delete p" << std::endl;
//}std::shared_ptr<int> p(new int(1), MyDeleter);std::shared_ptr<int> p1(new int(1), [](int *p){    delete p;std::cout << "delete p" << std::endl; });
//delete p
//delete p
指定删除器管理动态数组

由于default_delete只是对delete的浅封装,这也就意味着当我们用智能指针管理动态数组时需要对删除器进行指定。

当然,我们也可以仍使用default_delete,只不过要将模板参数改为数组

class A
{
public:~A(){std::cout << "~A()" << std::endl;}
};
std::shared_ptr<A> p(new A[10], std::default_delete<A[]>());
std::shared_ptr<A> p1(new A[10], [](A *p){ delete[] p; });

shared_ptr的伪实现

我们可以用指针来伪替代stl库中的引用计数器,同时加上互斥锁保证线程安全从而伪实现shared_ptr。

template <class T>
class shared_ptr
{
public:shared_ptr(T *p) : _p(p), _counter(new int(1)), _pmtx(new std::mutex){}shared_ptr(const shared_ptr<T> &sp) : _p(sp._p), _counter(sp._counter), _pmtx(sp._pmtx){add_ret_count();}shared_ptr<T> operator=(const shared_ptr<T> &sp){if (this != &sp){release();_p = sp._p;_counter = sp._counter;_pmtx = sp._pmtx;add_ret_count();}return *this;}T *operator->(){return _p;}T operator*(){return *_p;}T *get(){return _p;}void add_ret_count(){_pmtx->lock();(*_counter)++;_pmtx->unlock();}int &use_count(){return *(_counter);}void release(){bool flag = false;_pmtx->lock();if (--(*_counter) == 0){delete _p;delete _counter;_p = _counter = nullptr;flag = true;std::cout << "delete _p" << std::endl;}_pmtx->unlock();if (flag){delete _pmtx;_pmtx = nullptr;}}~shared_ptr(){release();}private:T *_p;std::mutex *_pmtx;int *_counter;
};

shared_ptr的注意事项

避免一个原始指针初始化多个shared_ptr
    int *p = new int(1);std::shared_ptr<int> p1(p);std::shared_ptr<int> p2(p);

显然这样会导致重复析构的问题

不要在函数实参中创建shared_ptr
func(std::shared_ptr<int>(new int(1)), g());

我们C++很多时候遵循stdcall,也就是从右到左的调用约定,但有时也会从左到右,所以很可能先创建了new int,然后调用g(),但是g()发生了异常,这就导致内存泄漏了。

std::shared_ptr<int> p(new int(1));
func(p, g());
不要通过shared_ptr返回this指针

下面的例子,由于用同一个this构造了两个shared_ptr,这其实就和我们第一种情况类似了,两个智能指针各自析构导致了重复析构

class A 
{
public:std::shared_ptr<A> getSelf(){return shared_ptr<A>(this);}~A(){std::cout << "~A()" << std::endl;}
};int main()
{std::shared_ptr<A> p(new A());std::shared_ptr<A> pa = p->getSelf();return 0;
}

如果想要获取this,正确方法是令管理的类继承std::enable_shared_from_this<T>

用enable_shared_from_this的原因,我们后面讲解weak_ptr时会解释。

class A : public std::enable_shared_from_this<A>
{
public:std::shared_ptr<A> getSelf(){return std::shared_from_this();}~A(){std::cout << "~A()" << std::endl;}
};int main()
{std::shared_ptr<A> p(new A());std::shared_ptr<A> pa = p->getSelf();return 0;
}
避免循环引用

循环引用问题是shared_ptr很容易踩的一个陷阱,前面几项逻辑上的错误还是很明显的,容易规避,但是循环引用却很容易让我们落入陷阱。

如下例,我们在实现某些数据结构时用到了三叉链结构,那么三叉链的父子关系就会导致循环引用,此时parent的左孩子为child,child的父节点为parent,导致了parent和child的引用计数都为2,当程序结束,parent和child自动调用析构函数,此时就会导致二者的引用计数都为1,没有释放内存。

对于循环计数的解决方案,C++11专门又引入了weak_ptr来进行解决,我们在后面会介绍。

struct Node
{std::shared_ptr<Node> _parent;std::shared_ptr<Node> _left;std::shared_ptr<Node> _right;~Node(){std::cout << "~Node()" << std::endl;}
};
int main()
{std::shared_ptr<Node> parent(new Node), child(new Node);parent->_left = child;child->_parent = parent;return 0;
}
//无输出

独占型智能指针:unique_ptr

从名称就可以看出,unique_ptr做的很绝,直接ban掉左值拷贝构造函数和左值赋值函数

在源码中,我们可以找到如下代码,禁用了左值构造和左值赋值。

      // Disable copy from lvalue.unique_ptr(const unique_ptr&) = delete;unique_ptr& operator=(const unique_ptr&) = delete;

但是我们可以通过移动语义将其转化为右值来进行赋值。当然,移动语义后原先的unique_ptr自然就失去了对原先资源的管理权。

    std::unique_ptr<int> p(new int(1));std::unique_ptr<int> p1 = std::move(p);//p放弃管理权std::cout<< *p1;

C++11时还未引入类似于make_shared的make_unique来构建unique_ptr,C++14才提供了make_unique,但是我们可以自己实现一个make_unique方法。

template<class T, class... Args> inline
typename std::enable_if<!std::is_array<T>::value, std::unique_ptr<T>>::type
make_unique(Args&&... args)
{return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}//动态数组
template<class T> inline
typename std::enable_if<std::is_array<T>::value && std::extent<T>::value == 0, std::unique_ptr<T>>::type
make_unique(size_t size)
{typedef typename std::remove_extent<T>::type U;return std::unique_ptr<T>(new U[size]());
}//过滤掉静态数组
template<class T, class... Args> inline
typename std::enable_if<std::extent<T>::value != 0, void>::type
make_unique(Args&&... args) = delete;
int main()
{std::unique_ptr<int[]> p = make_unique<int[]>(10);for (int i = 0; i < 10; i++)p[i] = i;for (int i = 0; i < 10; i++)std::cout << p[i] << " ";return 0;
}
//0 1 2 3 4 5 6 7 8 9

unique_ptr的删除器

默认删除器

unique_ptr也使用了默认删除器default_delete,但是由于析构时处理方式不同,所以unique_ptr是可以管理动态数组的。

但是如果是shared_ptr就不行了。

	std::unique_ptr<A[]> p(new A[10]);
//~A() ~A() ~A() ~A() ~A() ~A() ~A() ~A() ~A() ~A()
指定删除器

unique_ptr删除器由于参数模板是要传类型过去,所以不能直接写lambda表达式。

    std::unique_ptr<int,std::function<void(int*)>> p1(new int(1), [](int* x) {delete x; });std::unique_ptr<int, MyDeleter> p2(new int(1), MyDeleter());

关于shared_ptr 和unique_ ptr 的使用场景要根据实际应用需求来选择,如果希望只有一个智能指针管理资源或者管理数组就unique_ptr,如果希望多个智能指针管理同-一个资源就用shared_ptr。

弱引用的智能指针:weak_ptr

弱引用指针weak_ ptr 是用来监视shared_ptr的,不会使引用计数加1它不管理shared_ptr内部的指针,主要是为了监视shared_ptr的生命周期,更像是shared_ptr的一个助手。weak_ ptr没有重载操作符*和->,因为它不共享指针,不能操作资源,主要是为了通过shared_ptr获得资源的监测权,它的构造不会增加引用计数,它的析构也不会减少引用计数,纯粹只是作为一个旁观者来监视shared_ptr中管理的资源是否存在weakptr还可以用来返回this指针和解决循环引用的问题。

weak_ptr的基本用法

获取检测资源引用计数

use_count接口可以获取引用计数

    std::shared_ptr<int> p(new int(1));std::weak_ptr<int> guard(p);std::cout << guard.use_count();//1
判断是否释放

expired接口返回bool值来判断资源是否释放。

    std::shared_ptr<int> p(new int(1));std::weak_ptr<int> guard(p);if (guard.expired())std::cout << "resource has expired" << std::endl;elsestd::cout << "resource is still valid" << std::endl;[]()//resource is still valid
获取监测的shared_ptr

lock接口返回监测的shared_ptr

std::weak_ptr<int> guard;
void foo()
{if (guard.expired()){std::cout << "resource has expired" << std::endl;}else{auto pp = guard.lock();std::cout << *pp << std::endl;}
}
int main()
{{std::shared_ptr<int> p(new int(1));guard = p;foo();}foo();return 0;
}
//1
//resource has expired

weak_ptr返回this指针

我们前面介绍shared_ptr的时候提到了不能够直接用shared_ptr返回this指针,但是如果类为enable_shared_from_this的派生类就可以通过shared_from_this获取this指针,因为enable_shared_from_this中有一个weak_ptr,shared_from_this实际上是通过weak_ptr的lock接口来获取shared_ptr

class A : public std::enable_shared_from_this<A>
{
public:int a = 1;std::shared_ptr<A> getSelf(){return shared_from_this();}~A(){std::cout << "~A()" << std::endl;}
};int main()
{std::shared_ptr<A> p(new A());std::shared_ptr<A> pa = p->getSelf();std::cout << pa->a << std::endl;return 0;
}
//1
//~A()

weak_ptr解决循环引用问题

再回到前面的三叉链的例子里,由于循环引用,导致parent和child的引用计数都是2,生命周期计数也只能减少到1,所以导致内存泄漏。

struct Node
{std::shared_ptr<Node> _parent;std::shared_ptr<Node> _left;std::shared_ptr<Node> _right;~Node(){std::cout << "~Node()" << std::endl;}
};
int main()
{std::shared_ptr<Node> parent(new Node), child(new Node);parent->_left = child;child->_parent = parent;return 0;
}
//无输出

解决方案就是把三叉链的指针域改为weak_ptr

struct Node
{std::weak_ptr<Node> _parent;std::weak_ptr<Node> _left;std::weak_ptr<Node> _right;~Node(){std::cout << "~Node()" << std::endl;}
};
int main()
{std::shared_ptr<Node> parent(new Node), child(new Node);parent->_left = child;child->_parent = parent;return 0;
}
//~Node()
//~Node()

智能指针管理第三方库分配内存

智能指针可以很方便地管理当前程序库动态分配的内存,还可以用来管理第三方库分配的内存。第三方库分配的内存一般需要通过第三方库提供的释放接口才能释放,由于第三方库返回的指针一般都是原始指针,在用完之后如果没有调用第三方库的释放接口,就很容易造成内存泄露。比如下面的代码:

    void *p = GetHnadle()->Create();// do somethingGetHnadle()->Release(p);

这段代码逻辑其实是很危险的,因为在调用第三方库分配内存的过程中,可能忘记调用Release接口,也可能不小心返回了,也可能出现了异常,导致无法调用Release接口,但是如果是智能指针的话就不必有这样的担心了,因为离开作用域自动释放。

    void *p = GetHnadle()->Create();std::shared_ptr<void> sp(p, [&](void* p){ GetHnadle()->Release(p); });// do something

我们可以专门写一个函数来进行这种资源释放的委托。

std::shared_ptr<void> Guard(void *p)
{return std::shared_ptr<void>(p, [&](void* p){ GetHnadle()->Release(p); });
}
int main()
{void *p = GetHnadle()->Create();auto sp = Guard(p);// do somethingreturn 0;
}

但是还是不够安全,因为总有小可爱程序员会写成这样:

std::shared_ptr<void> Guard(void *p)
{return std::shared_ptr<void>(p, [&](void* p){ GetHnadle()->Release(p); });
}
int main()
{void *p = GetHnadle()->Create();Guard(p);//直接提前析构了可还行// do somethingreturn 0;
}

那么我们可以更进一步,用宏来解决这个问题:

#define GUARD(p) std::shared_ptr<void> p##p(p, [&](void*p) { GetHnadle()->Release(p); });
int main()
{void *p = GetHnadle()->Create();GUARD(p);// do somethingreturn 0;
}

总结

智能指针是为没有垃圾回收机制的语言解决可能的内存泄露问题的利器,但是在实际应用中使用智能指针有一些需要注意的地方,好在这些问题都可以解决。

  • shared_ptr 和unique_ptr 使用时如何选择:如果希望只有一个智能指针管理资源或者管理数组,可以用unique_ptr ;如果希望多个智能指针管理同一个资源,可以用shared_ptr。

  • weak_ptr是shared_ptr的助手,只是监视shared_ptr管理的资源是否被释放,本身并不操作或者管理资源。用于解决shared_ptr 循环引用和返回this指针的问题。

这篇关于让内存无处可逃:智能指针[C++11]的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/476108

相关文章

基于Python打造一个智能单词管理神器

《基于Python打造一个智能单词管理神器》这篇文章主要为大家详细介绍了如何使用Python打造一个智能单词管理神器,从查询到导出的一站式解决,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1. 项目概述:为什么需要这个工具2. 环境搭建与快速入门2.1 环境要求2.2 首次运行配置3. 核心功能使用指

C#如何调用C++库

《C#如何调用C++库》:本文主要介绍C#如何调用C++库方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录方法一:使用P/Invoke1. 导出C++函数2. 定义P/Invoke签名3. 调用C++函数方法二:使用C++/CLI作为桥接1. 创建C++/CL

C++如何通过Qt反射机制实现数据类序列化

《C++如何通过Qt反射机制实现数据类序列化》在C++工程中经常需要使用数据类,并对数据类进行存储、打印、调试等操作,所以本文就来聊聊C++如何通过Qt反射机制实现数据类序列化吧... 目录设计预期设计思路代码实现使用方法在 C++ 工程中经常需要使用数据类,并对数据类进行存储、打印、调试等操作。由于数据类

Linux下如何使用C++获取硬件信息

《Linux下如何使用C++获取硬件信息》这篇文章主要为大家详细介绍了如何使用C++实现获取CPU,主板,磁盘,BIOS信息等硬件信息,文中的示例代码讲解详细,感兴趣的小伙伴可以了解下... 目录方法获取CPU信息:读取"/proc/cpuinfo"文件获取磁盘信息:读取"/proc/diskstats"文

Python实现word文档内容智能提取以及合成

《Python实现word文档内容智能提取以及合成》这篇文章主要为大家详细介绍了如何使用Python实现从10个左右的docx文档中抽取内容,再调整语言风格后生成新的文档,感兴趣的小伙伴可以了解一下... 目录核心思路技术路径实现步骤阶段一:准备工作阶段二:内容提取 (python 脚本)阶段三:语言风格调

C++使用printf语句实现进制转换的示例代码

《C++使用printf语句实现进制转换的示例代码》在C语言中,printf函数可以直接实现部分进制转换功能,通过格式说明符(formatspecifier)快速输出不同进制的数值,下面给大家分享C+... 目录一、printf 原生支持的进制转换1. 十进制、八进制、十六进制转换2. 显示进制前缀3. 指

go 指针接收者和值接收者的区别小结

《go指针接收者和值接收者的区别小结》在Go语言中,值接收者和指针接收者是方法定义中的两种接收者类型,本文主要介绍了go指针接收者和值接收者的区别小结,文中通过示例代码介绍的非常详细,需要的朋友们下... 目录go 指针接收者和值接收者的区别易错点辨析go 指针接收者和值接收者的区别指针接收者和值接收者的

在Spring Boot中浅尝内存泄漏的实战记录

《在SpringBoot中浅尝内存泄漏的实战记录》本文给大家分享在SpringBoot中浅尝内存泄漏的实战记录,结合实例代码给大家介绍的非常详细,感兴趣的朋友一起看看吧... 目录使用静态集合持有对象引用,阻止GC回收关键点:可执行代码:验证:1,运行程序(启动时添加JVM参数限制堆大小):2,访问 htt

C++中初始化二维数组的几种常见方法

《C++中初始化二维数组的几种常见方法》本文详细介绍了在C++中初始化二维数组的不同方式,包括静态初始化、循环、全部为零、部分初始化、std::array和std::vector,以及std::vec... 目录1. 静态初始化2. 使用循环初始化3. 全部初始化为零4. 部分初始化5. 使用 std::a

C++ vector的常见用法超详细讲解

《C++vector的常见用法超详细讲解》:本文主要介绍C++vector的常见用法,包括C++中vector容器的定义、初始化方法、访问元素、常用函数及其时间复杂度,通过代码介绍的非常详细,... 目录1、vector的定义2、vector常用初始化方法1、使编程用花括号直接赋值2、使用圆括号赋值3、ve