• 动态内存——动态数组


    一、动态数组

      为了支持一次性为很多元素分配内存的需求,C++语言和标准库提供了两种一次分配一个对象数组的方法。C++语言定义了另一种new表达式语法,可以分配并初始化一个对象数组。标准库中包含了一个名为allocator的类,允许我们将分配和初始化分离。使用allocator通常会提供更好的性能和更灵活的内存管理能力。

      大多数应用都没有直接访问动态数组的需求。因此大多数应用应该使用标准库容器而不是动态分配的数组,使用容器更为简单、更不容易出现内存管理错误并且可能有更好的性能。

    1、new和数组

      为了让new分配一个对象数组,我们要在类型名之后跟一对方括号,在其中指明要分配的对象的数目,方括号中的大小必须是整型,但不必是常量。new分配要求的数量并(假定分配成功后)返回指向第一个对象的指针。也可以用一个表示数组类型的类型别名来分配一个数组。。

     1 #include <iostream>
     2 #include <memory>
     3 #include <string>
     4 
     5 int main()
     6 {
     7 
     8     int *p = new int[10]; // p指向分配的第一个int
     9 
    10     typedef int arrT[10]; // arrT表示10个int的数组类型
    11     int *p2 = new arrT; // 分配10个int的数组,p2指向第一个int
    12     return 0;
    13 }
    View Code

    1)分配一个数组会得到一个元素类型的指针

      当用new分配一个数组时,我们并未得到一个数组类型的对象,而是得到一个数组元素类型的指针。即使我们使用一个类型别名定义一个数组类型,new也不会分配一个数组类型的对象。new返回的是一个元素类型的指针。

      注意:动态数组不是数组类型,这是很重要的

    2)初始化动态分配对象的数组

      默认情况下,new分配的对象,不管是单个分配的还是数组中的,都是默认初始化的。可以对数组中的元素进行值初始化,方法是在大小之后跟一对空括号,不能在括号中给出初始化器。

     1 #include <iostream>
     2 #include <memory>
     3 #include <string>
     4 
     5 int main()
     6 {
     7     int *p1 = new int[10]();
     8     int *p2 = new int();
     9     std::cout << *p1 << "  " << *p2 << std::endl;
    10     return 0;
    11 }
    View Code

    在新标准中,我们还可以提供一个元素初始化器的花括号列表:

     1 #include <iostream>
     2 #include <memory>
     3 #include <string>
     4 
     5 int main()
     6 {
     7     int *p = new int[5]{1, 2, 3};
     8     for (int i = 0; i < 5; ++i)
     9         std::cout << *(p + i) << " ";
    10     std::cout << std::endl;
    11     return 0;
    12 }
    View Code

    初始化器会用来初始化动态数组中开始部分的元素。如果初始化器数目小于元素数目,剩余元素将进行值初始化。如果初始化器数目大于元素数目,则new表达式失败,不会分配任何内存。

    3)动态分配一个空数组是合法的

     1 #include <iostream>
     2 #include <memory>
     3 #include <string>
     4 
     5 int main()
     6 {
     7     int n = 0;
     8     int *p = new int[n];
     9     for (int *q = p; q != p + n; ++q)
    10         std::cout << *q << std::endl;
    11     std::cout << "hello" << std::endl;
    12     return 0;
    13 }
    View Code

    当我们用new分配一个大小为0的数组时,new返回一个合法的非空指针。此指针保证与new返回的任何其他指针都不相同。对于零长度的数组来说,此指针就像尾后指针一样,我们可以像使用尾后指针一样使用这个指针。可以用次指针进行比较操作,可以向此指针加上(或减去)0,也可以从此指针减去自身得到0。但此指针不能解引用——毕竟它不指向任何元素。

     4)释放动态数组

       为了释放动态数组,我们使用一种特殊形式的delete——在指针前加上一个空方括号对,空方括号对是必需的。

     1 #include <iostream>
     2 #include <memory>
     3 #include <string>
     4 
     5 class Blob
     6 {
     7 public:
     8     Blob() :x(0){}
     9     Blob(int _x) :x(_x){}
    10     ~Blob(){
    11         std::cout << x << " ~Blob" << std::endl;
    12     }
    13     int x;
    14 };
    15 int main()
    16 {
    17     Blob *p = new Blob[3]{1, 2, 3};
    18     delete [] p;
    19     return 0;
    20 }
    View Code

    释放p指向的数组中的元素,并释放对象的内存。数组中的元素按逆序销毁,即,最后一个元素首先被销毁,然后是倒数第二个,依次类推。

    5)智能指针和动态数组

       标准库提供了一个可以管理new分配的数组的unique_ptr版本。为了用一个unique_ptr管理动态数组,我们必须在对象类型后面跟一对空方括号:

     1 #include <iostream>
     2 #include <memory>
     3 #include <string>
     4 
     5 class Blob
     6 {
     7 public:
     8     Blob() :x(0){}
     9     Blob(int _x) :x(_x){}
    10     ~Blob(){
    11         std::cout << x << " ~Blob" << std::endl;
    12     }
    13     int x;
    14 };
    15 int main()
    16 {
    17     std::unique_ptr<Blob[]> up(new Blob[3]);
    18     for (int i = 0; i < 3; ++i)
    19         std::cout << up[i].x << " ";
    20     std::cout << std::endl;
    21     return 0;
    22 }
    View Code

    类型说明符中的方括号(<Blob[]>)指出up指向一个Blob数组。由于up指向一个数组,当up销毁它管理的指针时,会自动使用delete []。

      指向数组的unique_ptr提供的操作:

      指向数组的unique_ptr不支持成员运算符(点和箭头运算符)。毕竟unique_ptr指向的是一个数组而不是单个对象,因此这些运算符时无意义的。其他unique_ptr操作不变。

    操作 说明
    unique_ptr<T[]> u u可以指向一个动态分配的数组,数组元素类型为T
    unique_ptr<T[]> u(p) u指向内置指针p所指向的动态分配的数组。p必须能转换为类型T*
    u[i] 返回u拥有的数组中位置i处的对象。u必须指向一个数组

       shared_ptr不支持直接管理动态数组,如果希望使用shared_ptr管理一个动态数组,必须提供自定义的删除器。为了访问数组中的元素,必须用get获取一个内置指针,然后用它来访问数组元素。

     1 #include <iostream>
     2 #include <memory>
     3 #include <string>
     4 
     5 class Blob
     6 {
     7 public:
     8     Blob() :x(0){}
     9     Blob(int _x) :x(_x){}
    10     ~Blob(){
    11         std::cout << x << " ~Blob" << std::endl;
    12     }
    13     int x;
    14 };
    15 int main()
    16 {
    17     int n = 3;
    18     std::shared_ptr<Blob> p(new Blob[n], [](Blob *p){delete[] p; });
    19     for (int i = 0; i < n; ++i)
    20         std::cout << (p.get() + i)->x << " ";
    21     std::cout << std::endl;
    22     return 0;
    23 }
    View Code

    2、allocator类

       new有一些灵活上的局限,其中一方面表现在它将内存分配和对象构造组合在了一起。类似的,delete将对象析构和内存释放组合在了一起。我们分配单个对象时,通常希望将内存分配和对象初始化组合在一起。因为在这种情况下,我们就几乎肯定知道对象应该有什么值。

      当分配一大块内存时,我们通常计划在这块内存上按需构造对象。在此情况下,我们希望将内存分配和对象构造分离。这意味着我们可以分配大块内存,但只在真正需要时才真正执行对象创建操作(同时付出一定开销)。

      一般情况下,将内存分配和对象构造组合在一起可能会导致不必要的浪费。

    1)allocator类

      标准库allocator类定义在头文件memory中,它帮助我们将内存分配和对象构造分离开来。它提供一种类型感知的内存分配方法,它分配的内存是原始的、未构造的。allocator是一个模板。为了定义一个allocator对象,我们必须指明这个allocator可以分配的对象类型。当一个allocator对象分配内存时,它会根据给定的对象类型来确定恰当的内存大小和对齐位置。

      标准库allocator及其算法:

    操作 说明
    allocator<T> a 定义了一个名为a的allocator对象,它可以为类型为T的对象分配内存
    a.allocate(n) 分配一段原始的、未构造的内存,保存n个类型为T的对象。返回一个T*的指针,指向第一个分配的内存的地址
    a.deallocate(p, n)

    释放从T*指针p中地址开始的内存,这块内存保存了n个类型为T的对象;p必须是一个先前由allocator返回的指针,且n必须是p创建时所要求的大小。

    在调用deallocate之前,用户必须对每个在这块内存中创建的对象调用destroy

    a.construct(p, args) p必须是一个类型为T*的指针,指向一块原始内存;args被传递给类型为T的构造函数,用来在p指向的内存中构造一个对象
    a.destroy(p) p为T*类型的指针,此算法对p指向的对象执行析构函数

    1)allocator分配未构造的内存

      allocator分配的内存是未构造的。我们按需要在此内存中构造对象。在新标准库中,construct成员函数接受一个指针和零个或多个额外参数,在给定位置构造一个元素。额外参数用来初始化构造的对象,这些额外参数必须是与构造的对象的类型相匹配的合法的初始化器。

      注意:为了使用allocator返回的内存,我们必须用construct构造对象。使用未构造的内存,其行为是未定义的。

      当我们用完对象后,必须对每个构造的元素调用destroy来销毁它们。我们只能对真正构造了的元素指向destroy操作。

      一旦元素被销毁后,就可以重新使用这部分内存来保存其他同类型的元素,也可以将其归还给系统。

     1 #include <iostream>
     2 #include <memory>
     3 #include <string>
     4 
     5 class Blob
     6 {
     7 public:
     8     Blob() :x(0){}
     9     Blob(int _x) :x(_x){}
    10     ~Blob(){
    11         std::cout << x << " ~Blob" << std::endl;
    12     }
    13     int x;
    14 };
    15 int main()
    16 {
    17     int n = 3;
    18     std::allocator<Blob> a;
    19     Blob *const p = a.allocate(n);
    20     Blob * q = p;
    21     a.construct(q++, 1);
    22     a.construct(q++, 2);
    23     a.construct(q++, 3);
    24     while (q!=p)
    25     {
    26         a.destroy(--q); // 析构
    27     }
    28     std::cout << "---------" << std::endl;
    29     a.deallocate(p, n); // 将内存还给系统
    30     return 0;
    31 }
    View Code

    2)拷贝和填充未初始化内存的算法

      标准库还为allocator类定义了两个伴随算法,可以在未初始化内存中创建对象,都定义在头文件memory中。

      allocator算法:

      这些函数在给定目的位置创建元素,而不是由系统分配内存给它们。

    操作 说明
    uninitialized_copy(b, e, b2)

    从迭代器b和e指出的输入范围中拷贝元素到迭代器b2指定的未构造的原始内存中。b2指向的内存必须足够大,能容纳输入序列中元素的拷贝。

    会返回一个指针,指向最后一个构造的元素之后的位置

    uninitialized_copy_n(b, n, b2) 从迭代器b指向的元素开始,拷贝n个元素到b2开始的内存中
    uninitialized_fill(b, e, t) 在迭代器b和e指定的原始内存范围中创建对象,对象的值均为t的拷贝
    uninitialized_fill_n(b, n, t) 从迭代器b指向的内存地址开始创建n个对象。b必须指向足够大的未构造的原始内存,能够容纳给定数量的对象
     1 #include <iostream>
     2 #include <memory>
     3 #include <string>
     4 #include <vector>
     5 
     6 int main()
     7 {
     8     int n = 3;
     9     std::vector<int> v = { 1, 2, 3 };
    10     std::allocator<int> a;
    11     auto p = a.allocate(n * 2);
    12     auto q = std::uninitialized_copy(v.begin(), v.end(), p);
    13     std::uninitialized_fill_n(q, v.size(), 6);
    14     for (auto i = 0; i < 2 * n; ++i)
    15         std::cout << *(p + i) << " ";
    16     std::cout << std::endl;
    17     for (int i = 0; i < 2 * n; ++i)
    18         a.destroy(p + i);
    19     a.deallocate(p, n * 2);
    20     return 0;
    21 }
    View Code

  • 相关阅读:
    javascript定时器,取消定时器,及js定时器优化方法
    Systen,IO
    批量地理位置解析
    数据库分区分表(sql、mysql)
    数据库还原的多种方式
    js前端文件收集(一)
    NPOI解决由于excel删除数据导致空行读取问题
    echarts2.0tooltip边框限制导致tooltip显示不全解决办法
    数据库备份通用脚本
    echarts 用marlkline画线 同时配置中含有datazoom,怎么设置markline
  • 原文地址:https://www.cnblogs.com/ACGame/p/10280893.html
Copyright © 2020-2023  润新知