让内存无处可逃:智能指针[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

相关文章

C++右移运算符的一个小坑及解决

《C++右移运算符的一个小坑及解决》文章指出右移运算符处理负数时左侧补1导致死循环,与除法行为不同,强调需注意补码机制以正确统计二进制1的个数... 目录我遇到了这么一个www.chinasem.cn函数由此可以看到也很好理解总结我遇到了这么一个函数template<typename T>unsigned

C++统计函数执行时间的最佳实践

《C++统计函数执行时间的最佳实践》在软件开发过程中,性能分析是优化程序的重要环节,了解函数的执行时间分布对于识别性能瓶颈至关重要,本文将分享一个C++函数执行时间统计工具,希望对大家有所帮助... 目录前言工具特性核心设计1. 数据结构设计2. 单例模式管理器3. RAII自动计时使用方法基本用法高级用法

Redis实现高效内存管理的示例代码

《Redis实现高效内存管理的示例代码》Redis内存管理是其核心功能之一,为了高效地利用内存,Redis采用了多种技术和策略,如优化的数据结构、内存分配策略、内存回收、数据压缩等,下面就来详细的介绍... 目录1. 内存分配策略jemalloc 的使用2. 数据压缩和编码ziplist示例代码3. 优化的

深入解析C++ 中std::map内存管理

《深入解析C++中std::map内存管理》文章详解C++std::map内存管理,指出clear()仅删除元素可能不释放底层内存,建议用swap()与空map交换以彻底释放,针对指针类型需手动de... 目录1️、基本清空std::map2️、使用 swap 彻底释放内存3️、map 中存储指针类型的对象

Python内存优化的实战技巧分享

《Python内存优化的实战技巧分享》Python作为一门解释型语言,虽然在开发效率上有着显著优势,但在执行效率方面往往被诟病,然而,通过合理的内存优化策略,我们可以让Python程序的运行速度提升3... 目录前言python内存管理机制引用计数机制垃圾回收机制内存泄漏的常见原因1. 循环引用2. 全局变

C++ STL-string类底层实现过程

《C++STL-string类底层实现过程》本文实现了一个简易的string类,涵盖动态数组存储、深拷贝机制、迭代器支持、容量调整、字符串修改、运算符重载等功能,模拟标准string核心特性,重点强... 目录实现框架一、默认成员函数1.默认构造函数2.构造函数3.拷贝构造函数(重点)4.赋值运算符重载函数

C++ vector越界问题的完整解决方案

《C++vector越界问题的完整解决方案》在C++开发中,std::vector作为最常用的动态数组容器,其便捷性与性能优势使其成为处理可变长度数据的首选,然而,数组越界访问始终是威胁程序稳定性的... 目录引言一、vector越界的底层原理与危害1.1 越界访问的本质原因1.2 越界访问的实际危害二、基

c++日志库log4cplus快速入门小结

《c++日志库log4cplus快速入门小结》文章浏览阅读1.1w次,点赞9次,收藏44次。本文介绍Log4cplus,一种适用于C++的线程安全日志记录API,提供灵活的日志管理和配置控制。文章涵盖... 目录简介日志等级配置文件使用关于初始化使用示例总结参考资料简介log4j 用于Java,log4c

C++归并排序代码实现示例代码

《C++归并排序代码实现示例代码》归并排序将待排序数组分成两个子数组,分别对这两个子数组进行排序,然后将排序好的子数组合并,得到排序后的数组,:本文主要介绍C++归并排序代码实现的相关资料,需要的... 目录1 算法核心思想2 代码实现3 算法时间复杂度1 算法核心思想归并排序是一种高效的排序方式,需要用

使用Python构建智能BAT文件生成器的完美解决方案

《使用Python构建智能BAT文件生成器的完美解决方案》这篇文章主要为大家详细介绍了如何使用wxPython构建一个智能的BAT文件生成器,它不仅能够为Python脚本生成启动脚本,还提供了完整的文... 目录引言运行效果图项目背景与需求分析核心需求技术选型核心功能实现1. 数据库设计2. 界面布局设计3