• 第 12 章 动态内存


    第 12 章 动态内存

    标签: C++Primer 学习记录 动态内存


     

     


    12.1 动态内存与智能指针

    1. 不同的存储区域对应着不同生存周期的变量。

      • 静态内存——保存局部 static对象、类 static数据成员和定义在任何函数之外的变量,在第一次使用之前分配内存,在程序结束时销毁。
      • 栈内存——定义在函数内的非 static对象,当进入其定义所在的程序块时被创建,在离开块时被销毁。
      • 堆内存——存储动态分配的对象,即那些在程序运行时分配的对象。当动态对象不再使用时,必须由代码显式地销毁它们。
    2. 动态内存的使用很容易出问题。有时我们会忘记释放内存,在这种情况下就会产生内存泄漏;有时在尚有指针引用内存的情况下我们就释放了它,在这种情况下就会产生引用非法内存的指针。

    3. 为了更容易和安全地使用动态内存,新标准库提供了智能指针类型来管理动态对象。

      • shared_ptr,允许多个指针指向同一个对象。
      • unique_ptr,“独占”所指向的对象。
      • weak_ptr,弱引用,不控制所指向对象的生存期,指向 shared_ptr所管理的对象。
    4. 默认初始化的 shared_ptr对象是一个空指针,在使用之前需要进行初始化

      shared_ptr<string> p1;              // 空指针,使用之前需要初始化
      shared_ptr<string> p2 = make_shared<string>("temp");
      auto p3 = make_shared<string>("temp");
      shared_ptr<string> p4(new string("temp"));
      
    5. 因为在最后一个 shared_ptr销毁前,内存都不会释放,因此如果忘记销毁程序不再需要的 shared_ptr,程序仍会正确执行,但会浪费运行内存。一个例子就是将 shared_ptr存放于一个容器中,而后不再需要全部元素,而只是使用其中一部分,要记得掉用容器的 erase操作删除不再需要的元素。

    6. 程序使用动态内存,往往出于以下三种原因之一:

      • 程序不知道自己需要使用多少对象,比如说容器类。
      • 程序不知道所需对象的准确类型,可以 new一个基类指针用来指向派生类对象。
      • 程序需要在多个对象间共享数据,一般情况下对象的拷贝都是类值拷贝,会发生对象的拷贝构造和析构;而使用动态内存共享数据,则是类指针拷贝,所存储的数据没有发生变化,只是新定义一个指针来指向这些已有数据。
    7. 在自由空间分配的内存是无名的,因此 new无法为其分配的对象命名,而是返回一个指向该对象的指针。

      int *pi = new int;  // pi是一个指向动态分配的、未初始化的无名对象
      

           默认情况下,动态分配的对象是默认初始化的,这意味着内置类型或组合类型的对象的值将是未定义的,而类类型对象将用默认构造函数进行初始化。因此,对动态分配的对象进行初始化通常是个好主意。

      string *ps = new string;    // 初始化为空 string
      int *pi = new int;          // pi指向一个未初始化的 int
      
      • 可以使用直接初始化(圆括号、花括号)的方式或值初始化(空的圆括号)来初始化一个动态分配的对象。
      int *pi = new int(1024);
      int *pi2 = new int();    // 值初始化为 0,*pi2的值为 0
      vector<int> *ps = new vector<int>{1, 2, 3};
      
      • 如果提供了一个括号包围的初始化器,就可以使用 auto从此初始化器来推断出我们想要分配的对象的类型。也因为编译器要用初始化器来推断出想要分配的对象的类型,括号中只能有一个初始化器。
      auto p1 = new auto(obj);    // p1指向一个与 obj类型相同的对象
      auto p2 = new auto{a, b};   // 错误
      
      • 用 new分配 const对象是合法的,但是动态分配的 const对象必须进行初始化。对于一个定义了默认构造函数的类类型,其 const动态对象可以隐式初始化,而其他类型的对象就必须显式初始化。
      const int *pci = new const int(1024);  // const作为类型的一部分,也要出现在 new的后面
      const string *pcs = new const string;  // 默认初始化一个 const的空 string
      
      • 默认情况下,如果 new不能分配所要求的内存空间,会抛出一个类型为 bad_alloc的异常,可以使用定位 new形式并向其传递参数 nothrow来阻止它抛出异常。此时它会返回一个空指针。
      // 如果分配失败,抛出bad_alloc异常
      int *p1 = new int();
      // 如果分配失败,返回空指针
      int *p2 = new (nothrow) int();
      
    8. 释放一块并非 new分配的内存,或者将相同的指针释放多次,其行为是未定义的。通常情况下,编译器不能分辨一个指针指向的是静态还是动态分配的对象。类似的,编译器也不能分辨一个指针所指向的内存是否已经被释放了。

      int i, *pi1 = &i, *pi2 = nullptr;
      double *pd = new double(33), *pd2 = pd;
      delete i;    // 错误,i不是一个指针
      delete pi1;  // 错误,pi1指向静态分配的对象
      delete pd;   // 正确
      delete pd2;  // 错误,pd2指向的内存已经被释放掉了
      delete pi2;  // 正确,释放一个空指针总是没有错误的
      
    9. 动态内存的管理非常容易出错,存在三个常见问题:

      • 忘记 delete内存。
      • 使用已释放掉的对象。通过在释放内存后将指针置为空,在使用前检测指针是否为空,可以避免这种错误。
      • 同一块内存被释放两次。
    10. 空悬指针,指向一块曾经保存数据对象但现在已经无效的内存的指针。当我们 delete一个指针后,指针值就无效了。虽然指针已经无效,但在很多机器上指针仍然保存在(已经释放了的)动态内存的地址。有一种方法可以避免空悬指针的问题:在指针即将离开其作用域之前释放掉它所关联的内存,而如果需要保留指针,可以在 delete之后将 nullptr赋予指针,这样就清楚地指出指针不指向任何对象。

    int *p(new int(42));
    delete p;
    p = nullptr;
    
    1. 可以用 new返回的指针来初始化智能指针,但该接受指针参数的智能指针构造函数是 explicit的。因此,不能将一个内置指针隐式转换为一个智能指针,必须使用直接初始化形式来初始化一个普通指针
    shared_ptr<int> p1 = new int(1024);  // 错误,必须使用直接初始化形式
    shared_ptr<int> p2(new int(1024));   // 正确
    shared_ptr<int> clone(int p) {
        return new int(p);    // 错误,隐式转换为 shared_ptr<int>
    }
    
    • 默认情况下,一个用来初始化智能指针的普通指针必须指向动态内存,因为智能指针默认使用 delete释放它所关联的对象。也可以将智能指针绑定到一个指向其他类型的资源的指针上,但是我们必须提供自己的操作来代替 delete。
    • 轻易不要使用一个内置指针来访问一个智能指针所负责的对象,因为我们无法知道对象何时会被销毁。
    // 在函数被调用时 ptr被创建并初始化
    void process(shared_ptr<int> ptr)
    {
        // 使用 ptr
    }   // ptr离开作用域,被销毁
    // 使用此函数的正确方法是给它传递一个 shared_ptr
    shared_ptr<int> p(new int(42));  // 引用计数为 1
    process(p);  // 值拷贝 p会递增它的引用计数;在 process中引用计数值为 2
    int i = *p;  // 正确,引用计数为 1
    // 在传递一个临时的 shared_ptr后,就不能再用内置指针访问之前的内存了
    int *x(new int(1024));
    process(x);  // 错误,不能将 int*转换为一个 shared_ptr<int>
    process(shared_ptr<int>(x))  // 合法,但执行完此行代码后,智能指针所指向的内存会被释放!
    int j = *x;  // 错误, x是一个空悬指针
    
    • get用来将指针的访问权限传递给代码,只有在确定代码不会 delete指针的情况下,才能使用 get。特别是,永远不要用 get初始化另一个智能指针或者为另一个智能指针赋值。
    shared_ptr<int> p(new int(42));  // 引用计数为 1
    int *q = p.get();  // 正确,但使用 q时要注意,不要让它管理的指针被释放
    {
       // 未定义,两个独立的 shared_ptr指向相同的内存
       shared_ptr<int> (q);
    }  // 程序块结束,q被销毁,它所指向的内存被释放
    int foo = *p;  // 未定义,p所指向的内存已经被释放了
    
    • 可以用 reset来将一个新的指针赋予一个 shared_ptr。在改变底层对象之前,要检查自己是否是当前对象仅有的用户,可以通过unique来完成。如果不是,在改变之前要制作一份新的拷贝。
    if (!p.unique())
        p.reset(new string(*p));  // 不是唯一用户,需要分配新的拷贝
    *p += newVal;   // 现在可以确定自己确定是唯一用户,可以改变对象的值
    
    1. 使用智能指针可以确保程序在异常发生后资源能被正确地释放,与之相对,直接使用内置指针管理动态内存,当在 new之后且对应的 delete之前发生了异常,则内存不会被释放,造成内存泄漏。另外,对于没有良好定义的析构函数的类对象,也可以使用智能指针来管理,不管是否发生异常,当智能指针类对象不再使用时,会调用相应的删除器函数进行内存回收。
    void f()
    {
        shared_ptr<int> sp(new int(42));
        // 这段代码抛出一个异常,且在 f中未捕获
    }   // 在函数结束时 shared_ptr自动释放内存
    void f()
    {
        int *ip = new int(42);
        // 这段代码抛出一个异常,且在 f中未捕获
        delete ip;    // 在退出之前释放内存
    }   // 内存将永远都不会被释放
    
    1. 智能指针可以提供对动态分配的内存安全而又方便的管理,但这也需要坚持一些基本规范:
    • 不使用相同的内置指针初始化(或 reset)多个智能指针
    • 不 delete get()返回的指针
    • 不使用 get()初始化或 reset另一个智能指针,这可能会造成二次 delete
    • 当使用 get()返回的指针时,当最后一个对应的智能指针销毁后,get()返回的指针就变为无效了
    • 当使用智能指针来管理不是 new分配的内存资源时,记住传递给它一个删除器
    1. 对于 shared_ptr类模板,删除器是类模板的 function数据成员,可以通过拷贝构造函数或 reset函数进行更改。而 unique_ptr的删除器是一个具有默认模板实参的模板类型参数,在定义一个 unique_ptr时就要一并给出。

    2. 在某个时刻只能有一个 unique_ptr指向一个给定对象。当定义一个 unique_ptr时,需要将其绑定到一个 new返回的指针上。由于一个 unique_ptr独占它所指向的对象,因此 unique_ptr不支持普通的拷贝或赋值操作

    unique_ptr<int> p1(new int(42));
    unique_ptr<int> p2(p1);   // 错误, unique_ptr不支持拷贝
    unique_ptr<int> p3;
    p3 = p2;                  // 错误, unique_ptr不支持赋值
    
    • 虽然 unique_ptr不能被拷贝或赋值,但可以通过 release或 reset来将指针的所有权从一个 unique_ptr转移到另一个 unique_ptr。
    unique_ptr<int> p1(new int(42));
    // release将 p1置为空,将所有权从 p1转移给 p2
    unique_ptr<int> p2(p1.release());
    unique_ptr<int> p3(new int(0));
    // release将 p1置为空,reset将 p2置为空,再将所有权从 p3转移给 p2
    p2.reset(p3.release()); 
    p2.release();            // 错误, p2不会释放内存,而且丢失了指针
    auto p = p2.release();   // 正确,但是要记得 delete(p)
    
    • 不能拷贝 unique_ptr的规则有一个例外:可以拷贝或赋值一个将要被销毁的 unique_ptr,此时执行的是类对象的移动操作。因为移后源会被析构,所以还是只有一个 unique_ptr独占对象。
    unique_ptr<int> clone(int p) {
    // 正确,从 int*创建一个 unique_ptr<int>
    return unique_ptr<int> (new int(p));
    }
    
    • 对于 unique_ptr,删除器是类型的一部分,默认的删除器是 delete。但是要想重载删除器,必须在创建 unique_ptr对象时,就要提供一个指定类型的可调用对象(删除器)。
    // p指向一个类型为 objT的对象,并使用一个类型为 delT的对象释放 objT对象
    // 它会调用一个名为 fcn的 delT类型对象
    unique_ptr<objT, delT> p (new objT, fcn);
    
    1. weak_ptr,不控制所指向对象生存期的智能指针,指向由一个 shared_ptr管理的对象。将一个 weak_ptr绑定到一个 shared_ptr,不会改变 shared_ptr的引用计数。一旦最后一个指向对象的 shared_ptr被销毁,对象就会被释放,而不管是否有 weak_ptr指向该对象
    • 创建一个 weak_ptr时,要用一个 shared_ptr来初始化它。
    auto p = make_shared<int>(42);
    weak_ptr<int> wp(p);  // wp弱共享,p的引用计数为改变
    
    • 由于对象可能不存在,因此我们不能够使用 weak_ptr直接访问对象,而必须调用 lock来检查 weak_ptr指向的对象是否存在。
    if (shared_ptr<int> np = wp.lock()) {  // 如果 np不为空,则条件成立
        // 在 if中,np与 p共享对象
    }
    

    12.2 动态数组

    1. 在新标准下,当一个应用需要可变数量的对象时,应该使用标准库容器而不是动态分配的数组,使用容器更为简单、更不容易出现内存管理错误并且可能有着更好的性能。

    2. 可以使用 new T[]或类型别名的形式分配一个动态对象数组,默认情况下,该数组是未初始化的。方括号中的大小必须是整数,但不必是常量。

      // pia指向第一个 int
      int *pia = new int[get_size()];
      typedef int arrT[42];
      int *p = new arrT;  // 分配一个 42个 int的数组,p指向第一个 int
      
      • 使用 new分配一个数组会得到一个元素类型的指针,动态数组的长度可变,而对于普通数组类型而言,维度是数组类型的一部分,因此动态数组并不是数组类型。不能对动态数组调用 begin或 end函数,也不能用范围 for语句来处理动态数组中的元素
      • 普通数组的长度不能为 0,而动态数组的长度可以为 0。相当于定义了一个尾后指针,此指针可以执行比较操作,但是不能解引用。
      char arr[0];              // 错误,不能定义长度为 0的数组
      char *cp = new char[0];   // 正确,但 cp不能解引用
      
      • 默认情况下,new分配的对象,不管是单个分配的还是数组中的,都是默认初始化的。对数组中的元素进行值初始化,可以再大小之后跟一对空括号。与分配单个对象不同,分配数组对象,不能在圆括号内指定初始值。但是可以在花括号内提供元素初始化器,具体规则与使用大括号初始化内置数组类似。无法用 auto分配数组。
      int *pia = new int[10];               // 10个未初始化的 int
      int *pia2 = new int[10]();            // 10个值初始化为 0的 int
      int *pia3 = new int[10](1);           // 错误,不能在圆括号内指定初始值
      int *pia4 = new int[10]{0, 1, 2};     // 在列表中给定初始化器
      auto *pia5 = new auto[10]();          // 错误,未给出初始化器
      auto *pia6 = new auto[10]{0, 1, 2};   // 错误,花括号括起来的初始值无法与 new auto配合使用
      
    3. unique_ptr可以直接管理动态数组,但必须在对象类型后面跟上一对空方括号。unique_ptr不支持点和箭头运算符,因为其指向的是一个数组而不是元素,这些操作没有意义。unique_ptr支持下标运算符。

      unique_ptr<int[]> up(new int[10]);
      up[1] = 2;  // 使用下标运算符访问元素
      
    4. shared_ptr不直接支持管理动态数组,这是因为 shared_ptr默认是用 delete作为删除器,而动态数组的析构,需要使用 delete[]。因此,在使用 shared_ptr管理动态数组时,必须提供自己的删除器。另外,shared_ptr不支持下标运算。

      shared_ptr<int> sp(new int[10], [](int *p) { delete [] p; });
      *(sp.get() + 1) = 2;  // 使用 get()返回内置指针,用这个指针来访问元素
      
    5. new将内存分配和对象构造组合在了一起,delete将对象析构和内存释放组合在了一起。再分配单个对象时,因为几乎知道对象应该有什么值,所以我们希望将内存分配和对象构造组合在一起。而对于大块内存分配时,将内存分配和对象构造组合在一起,可能会造成不必要的浪费(多次赋值,一次在默认初始化时,一次在使用时)。更重要的是,如果一个类没有默认构造函数,就无法为其分配动态数组!

    6. allocator类将 new和 delete的功能都分了开来,主要包括分配内存、构造对象、对象析构和内存释放。

      allocator<string> alloc;
      auto const p = alloc.allocate(n);    // 分配 n个未初始化的 string
      auto q = p; 
      // 构造 string对象后,将 q后移一位,使 q永远指向最后构造的元素之后的位置
      alloc.construct(q++, "hi");
      // 对象析构,只能对真正构造了的元素进行 destroy操作
      while (q != p)
          alloc.destroy(--q);
      // 释放内存
      alloc.deallocate(p, n);
      
  • 相关阅读:
    SpringMVC的ServletContext、根上下文和MVC上下文上分别有什么东西?
    HTTP2密码组黑名单
    How to create a self-signed SSL Certificate ...
    oracle数据库的数据字典视图,数据来自哪个(些)表?
    关于GnuPG的subkey(子密钥)的使用
    签名别人的公钥以及验证签名的公钥
    GnuPG高级指导(6)在其他电脑上启用“我的密钥”
    Spring框架Controller层(表现层)针对方法参数是Bean时HttpServletRequest绑定参数值问题解释
    Mysql only_full_group_by以及其他关于sql_mode原因报错详细解决方案
    Maven生成可以直接运行的jar包的多种方式
  • 原文地址:https://www.cnblogs.com/taqikema/p/8250986.html
Copyright © 2020-2023  润新知