Loading

欢乐C++ —— 10. 动态内存管理

简介

一般而言程序使用动态内存有以下几种场景:

  1. 程序不知道自己需要多少对象。
  2. 程序不知道所需对象的准确类型。
  3. 程序需要在多个对象之间共享数据。

容器是由于第一种原因而使用动态内存的典型例子,第二种原因和继承有关,这节课主要跟第三个原因有关。

到现在,我们的都是手动的管理动态内存,手动调用new,并且手动释放。当代码量比较少时,不太容易出现内存泄漏、访问失效指针、无意浅拷贝等等问题。但如果代码量一大,留意内存管理就很费神。

因此,标准库提供了一组智能指针类,用来自动释放内存、防止拷贝等等,行为类似于指针,不过其会自动释放所指向的资源,其利用的是栈上对象出作用域自动析构的特点。

智能指针不是只能处理指针,我们可以向其添加适当的删除器,使它可以像管理指针一样管理其他资源。

头文件

直接管理动态内存

new / malloc delete / free

new 与 malloc

  • int *p1 = new int; // 内存开辟失败抛 bad_alloc 异常
    int *p2 = new (nothrow) int;  // 失败返回nullptr
    const int *p3 = new const int(20); //分配常量时
    int *p4 = new (p1) int(20);// 定位new  省略了自己开辟内存,直接在已有的内存上进行初始化
    
  • malloc 是库函数,而new 是C++运算符。

  • new = malloc + 初始化。

  • malloc 只负责开辟内存,不负责初始化,而new 不仅可以开辟内存,还可以进行初始化。

  • malloc 按字节数开辟内存,返回void * 需要进行类型强转。而new 开辟内存时需要指定类型,所以不需要类型强转。

  • 当开辟内存失败,malloc 返回 NULL 指针,而new抛出 bad_alloc 类型异常。

  • new 零个元素的数组也是允许的。

delete 与 free

  • free 是库函数,而delete 是运算符。

  • delete = free + 析构。

  • free只需要传入释放内存的起始地址就可以,而delete释放内存要决定是否是数组还是单个元素。

  • delete 因为是free + 析构,所以有下面的代码。

    class A;
    A * p = new A[20];
    //都可以释放new分配的内存。但有区别
    delete[]p;  //将调用这20个对象的析构函数。
    delete p;	//只调用p[0]的析构函数。在vs2017,在类中有自定义的析构函数时会运行时触发断点。
    
  • delete 空指针是无错的

  • delete 非new 的指针 / 已经被释放的指针 都是未定义的。

智能指针

概览

标准库提供的智能指针主要分为两类、四种。这几种智能指针的不同在于对裸指针的管理方式,它们都是模板类,定义在头文件

  • 不带引用计数的智能指针:一个资源只能由一个智能指针指向 auto_ptr unique_ptr

    • auto_ptr : auto_ptr 的拷贝,会将原先对象所指资源移交给新对象。

      由于这个拷贝时与我们认知不符,所以不常用,本文不涉及。详情可见 https://www.zhihu.com/question/37351146

    • unique_ptr: unique_ptr将左值拷贝和左值赋值定义为删除,从而阻止了拷贝。不过其对应的右值版本都存在。

  • 带引用计数的智能指针:一个资源可以由多个指针指向 shared_ptr weak_ptr

    • shared_ptr 当一个shared_ptr 指向一个资源时,会将该资源的引用计数+1,当析构时会-1。只有析构后引用计数变为0才会释放资源。

      cas atmoic 原子类型所以这个引用计数的改变是线程安全的。

      其引用计数一般存放在堆上

      使用时防止出现交叉引用。

    • weak_ptr 只是一个观察者,观察的是资源的引用计数,不能访问原资源,不修改引用计数。

    定义对象使用shared_ptr ,引用使用weak_ptr,防止shared_ptr交叉引用。

shared_ptr 和 unique_ptr 的使用和普通指针几乎没什么区别。

接下来我们看看每个智能指针是如何使用。

shared_ptr<>
创建方法

shared_ptr 有多种创建方法,有以下几点需要注意:

  1. 在创建时使用new 或 make_shared
  2. 拷贝shared_ptr 或 通过其它智能指针初始化
  3. 在初始化时可以指定删除器和空间配置器

最安全的方法就是使用make_shared<>() 该模板函数接受一个类型参数和一些实参,将会使用实参动态分配并初始化一个对象,并返回指向该对象的 shared_ptr<> 。

//智能指针的创建
shared_ptr<int> p1 = make_shared<int>(1) ;
auto p2 = make_shared<int>( );

//shared_ptr<int> p2 = new int( ); // 因为shared_ptr的构造函数声明为explicit,所以这样初始化是错误的
shared_ptr<int> p3(new int( ));

//引用计数
cout << p1.use_count( ) << endl;
auto p4 = p1;
cout << p4.use_count( ) << endl;
引用计数

每个shared_ptr 都有一个相关联的引用计数,它标记着一个资源在同一时刻被多少个shared_ptr指向。不同资源的引用计数互不影响。当我们拷贝shared_ptr 时引用计数会+1,当一个相关的shared_ptr 对象被析构,引用计数会-1,在析构后引用计数降为0时释放该资源。

引用计数是如何实现与标准库版本有关。一般都在堆上。

当不需要某些资源时,要及时析构shared_ptr 。例如将shared_ptr存放在容器中,如果后面不用的话应该将主动从容器中删除,从而达到释放资源的目的。

其它的成员函数
  • get() 获取智能指针所保存的裸指针。
    • 不要delete 这个裸指针,因为智能指针析构时会再释放一次,出现重复释放
    • 不要将这个裸指针赋值给其它的智能指针,因为两者不会共享一个引用计数,会出现重复释放
    • 在传递该裸指针时,小心其已失效
  • use_count() 返回该智能指针的引用计数。
  • unique() 如果该智能指针的引用计数为1 返回true 否则false

总之,慎用get() 再强调一遍:定义对象使用shared_ptr ,引用使用weak_ptr

unique_ptr<>

因为将其左值拷贝和左值赋值定义为删除(保留了空指针赋值),所以其特性是无法拷贝,保证其保存的对象只有一个智能指针指向它。

其它操作

  • = nullptr  会先释放其所指对象,然后赋空值
    
  • reset()  会先释放该智能指针所指向的对象,而后传入裸指针则令其指向这个指针,否则置空
    
  • release() 会放弃对所指对象的控制,返回所指资源的裸指针,并且将该智能指针置空
    
unique_ptr<int> p(new int( ));
p = nullptr;
p.reset();

auto p2= p.release();
weak_ptr<>

通常将其看作资源的观察者,其无权访问和修改所指资源,不会增加所指资源的引用计数。所以有时又称弱智能指针,因为shared_ptr才有引用计数,所以只能由shared_ptr 或 weak_ptr 初始化

常见方法

  • reset() 将该智能指针置空
  • use_count() 获得与weak_ptr 指向同一资源的shared_ptr 的数量
  • expired() 检测该弱指针所指向的资源是否还在, 会检查 use_count 的值,当其为0 返回true 否则 false
  • lock() 返回指向该weak_ptr 所指资源的shared_ptr

如果想检测两个weak_ptr 是否指向同一个资源,不能直接两个weak_ptr 用== 判断,应该使用lock 将其提升后判断。

动态数组

这个之前都使用过,不过这块规范一下相关概念。

我们用new [] 形式分配的数组称为动态数组。需要注意的是虽然它称作动态数组,但实际它不是数组,不能使用范围for 遍历

与智能指针的联系 我们可以在令智能指针指向数组,不过要注意使用下标运算符访问元素。

unique_ptr<int[]> p(new int[4] { 1,2,3,4 });
cout << *p << endl; //error 数组指针没有这种访问形式
cout << (*p)[0] << endl; //这种和下面等价
cout << p[0] << endl;

allocator 模板类

注意到当我们使用new 时,实际上做了两个工作:分配内存和构造对象;同样 delete 也做了两个工作:析构对象和释放内存。

在实际中,每次分配空间的同时构造对象有时是不必要的,特别是当我们只想要一个能够存放东西的容器。和vector 的 resize() reserve() 一样,我们可以使用allocator 将分配内存和构造对象分开。

allocator<int> alloc;
allocator<int> alloc2;

auto const p  = alloc.allocate(10);	//分配空间

printf("%x\n", p[1]);				
alloc.construct(p + 1,0x666);		//构造对象
printf("%x\n", p[1]);
alloc.destroy(p + 1);				//析构对象
printf("%x\n", p[1]);

alloc.deallocate(p, 10);			//释放空间
printf("%x\n", p[1]);

/* 结果
cdcdcdcd
666
666   //因为这块是int,属于内置类型,其析构对内存不做操作。但要记住这块实际上调用了析构函数	
dddddddd
*/

假设p指向内置类型动态分配的数组,delete p与delete[]p 没有区别,这块跟析构函数有点关系。

下面几个泛型函数往未初始化的内存中拷贝构造对象。

uninitialized_copy( );
uninitialized_copy_n( );
uninitialized_fill( );
uninitialized_fill_n( );

总结

如无必要,尽量使用智能指针管理动态内存

image-20200506212339410
posted @ 2020-04-01 22:08  沉云  阅读(192)  评论(0编辑  收藏  举报