0%

C++ Primer - 第 12 章 动态内存

  1. 全局对象在程序启动时分配,在程序结束时销毁。对于局部自动对象,当我们进入其定义所在的程序块时被创建,在离开块时销毁。局部 static 对象在第一次使用前分配,在程序结束时销毁。

  2. 静态内存用来保存局部 static 对象、类 static 数据成员以及定义在任何函数之外的变量。栈内存用来保存定义在函数内的非 static 对象。分配在静态或栈内存中的对象由编译器自动创建和销毁。对于栈对象,仅在其定义的程序块运行时才存在;static 对象在使用之前分配,在程序结束时销毁。

  3. 除了静态内存和栈内存,每个程序还拥有一个内存池。这部分内存被称作自由空间(free store)或(heap)。程序用堆来存储动态分配(dynamically allocate)的对象——即,那些在程序运行时分配的对象。

12.1 动态内存与智能指针

  1. new,在动态内存中为对象分配空间并返回一个指向该对象的指针,我们可以选择对对象进行初始化;delete,接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。

  2. 智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。新标准库提供的两种智能指针的区别在于管理底层指针的方式:shared_ptr 允许多个指针指向同一个对象;unicue_ptr 则“独占”所指向的对象。标准库还定义了一个名为 weak_ptr 的伴随类,它是一种弱引用,指向 shared_ptr 所管理的对象。这三种类型都定义在 memory 头文件中。

12.1.1 shared_ptr 类

  1. 类似 vector,智能指针也是模板。因此,当我们创建一个智能指针时,必须提供额外的信息——指针可以指向的类型。与 vector 一样,我们在尖括号内给出类型,之后是所定义的这种智能指针的名字:

    1
    2
    shared_ptr<string> p1;    // shared_ptr,可以指向 string
    shared_ptr<list<int>> p2; // shared ptr,可以指向 int 的 list

    默认初始化的智能指针中保存着一个空指针。

  2. 解引用一个智能指针返回它指向的对象。如果在一个条件判断中使用智能指针,效果就是检测它是否为空。

  3. 以下操作同时适用于 shared_ptrunique_ptr

    shared_ptr 和 unique_ptr 都支持的操作
    shared_ptr<T> sp 空智能指针,可以指向类型为 T 的对象
    unique_ptr<T> up
    p 将 p 用作一个条件判断,若 p 指向一个对象,则为 true
    *p 解引用 p,获得它指向的对象
    p->mem 等价于 (*p).mem
    p.get() 返回 p 中保存的指针。要小心使用,若智能指针释放了其对象,返回的指针所指向的对象也就消失了
    swap(p, q) 交换 p 和 q 中的指针
    p.swap(q)

    以下操作则仅适用于 shared_ptr

    shared_ptr 独有的操作
    make_shared<T> (args) 返回一个 shared_ptr,指向一个动态分配的类型为 T 的对象。使用 args 初始化此对象
    shared_ptr<T> p(q) p 是 shared_ptr q 的拷贝;此操作会递增 q 中的计数器。q 中的指针必须能转换为 T*
    p = q p 和 q 都是 shared_ptr,所保存的指针必须能相互转换。此操作会递减 p 的引用计数,递增 q 的引用计数;若 p 的引用计数变为 0,则将其管理的原内存释放
    p->mem 等价于 (*p).mem
    p.unique() 若 p.use_count() 为 1,返回 true;否则返回 false
    p.use_count() 返回与 p 共享对象的智能指针数量;可能很慢,主要用于调试
  4. 最安全的分配和使用动态内存的方法是调用一个名为 make_shared 的标准库函数。此函数在动态内存中分配一个对象并初始化它,返回指向此对象的 shared_ptr。与智能指针一样,make_shared 也定义在头文件 memory 中。

    1
    2
    3
    4
    5
    6
    // 指向一个值为 42 的 int 的 shared_ptr
    shared_ptr<int> p3 = make_shared<int>(42);
    // p4 指向一个值为“9999999999"的 string
    shared_ptr<string> p4 = make_shared<string>(10, '9');
    // p5 指向一个值初始化的 int,即,值为 0
    shared_ptr<int> p5 = make_shared<int>();

    之我见。make_shared 做的三件事:
    1、申请分配动态内存;
    2、在动态内存上调用模板参数类型对应的构造函数进行对象构造;
    3、返回指向新分配动态内存的 shared_ptr 对象。

  5. 通常用 auto 定义一个对象来保存 make_shared 的结果:

    1
    2
    // p6 指向一个动态分配的空 vector<string>
    auto p6 = make_shared<vector<string>>();
  6. 当进行拷贝或赋值操作时,每个 shared_ptr 都会记录有多少个其他 shared_ptr 指向相同的对象。可以认为每个 shared_ptr 都有一个关联的计数器,通常称其为引用计数(reference count)。无论何时我们拷贝一个 shared_ptr,计数器都会递增。例如,当用一个 shared_ptr 初始化另一个 shared_ptr,或将它作为参数传递给一个函数以及作为函数的返回值时,它所关联的计数器就会递增。当我们shared_ptr 赋予一个新值或是 shared_ptr 被销毁(例如一个局部的 shared_ptr 离开其作用域)时,计数器就会递减。一旦一个 shared_ptr 的计数器变为 0,它就会自动释放自己所管理的对象:

    1
    2
    3
    4
    5
    auto r = make_shared<int>(42); // r 指向的 int 只有一个引用者
    r = q; // 给 r 赋值,令它指向另一个地址
    // 递增 q 指向的对象的引用计数
    // 递减 r 原来指向的对象的引用计数
    // r 原来指向的对象已没有引用者,会自动释放
  7. 当指向一个对象的最后一个 shared_ptr 被销毁时,shared_ptr 类会自动销毁此对象。shared_ptr 的析构函数会递减它所指向的对象的引用计数。如果引用计数变为 0,shared_ptr 的析构函数就会销毁对象,并释放它占用的内存。

  8. 由于在最后一个 shared_ptr 销毁前内存都不会释放,保证 shared_ptr 在无用之后不再保留就非常重要了。如果你忘记了销毁程序不再需要的 shared_ptr,程序仍会正确执行,但会浪费内存。 share_ptr 在无用之后仍然保留的一种可能情况是,你将 shared_ptr 存放在一个容器中,随后重排了容器,从而不再需要某些元素。在这种情况下,你应该确保用 erase 删除那些不再需要的 shared_ptr 元素。

    Note: 如果你将 shared_ptr 存放于一个容器中,而后不再需要全部元素,而只使用其中一部分,要记得用 erase 删除不再需要的那些元素。

  9. Note: 使用动态内存的一个常见原因是允许多个对象共享相同的状态。

12.1.2 直接管理内存

  1. 在自由空间分配的内存是无名的,因此 new 无法为其分配的对象命名,而是返回一个指向该对象的指针。默认情况下,动态分配的对象是默认初始化的,这意味着内置类型或组合类型的对象的值将是未定义的,而类类型对象将用默认构造函数进行初始化。

  2. 初始化动态分配的对象:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    int *pi = new int(1024);          // pi 指向的对象的值为 1024
    string *ps = new string(10, '9'); // *ps 为 "9999999999"

    // vector 有 10 个元素,值依次从 0 到 9
    vector<int> *pv = new vector<int>{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};

    string *ps1 = new string; // 默认初始化为空 string
    string *ps = new string(); // 值初始化为空 string
    int *pi1 = new int; // 默认初始化;*pi1 的值未定义
    int *pi2 = new int(); // 值初始化为 0;*pi2 为 0
  3. 最佳实践: 出于与变量初始化相同的原因,对动态分配的对象进行初始化通常是个好主意。

  4. 如果我们提供了一个括号包围的初始化器,就可以使用 auto 从此初始化器来推断我们想要分配的对象的类型。但是,由于编译器要用初始化器的类型来推断要分配的类型,只有当括号中仅有单一初始化器时才可以使用 auto

    1
    2
    3
    auto pl = new auto(obj);      // p 指向一个与 obj 类型相同的对象
    // 该对象用 obj 进行初始化
    auto p2 = new auto {a, b, c}; // 错误:括号中只能有单个初始化器
  5. new 分配 const 对象是合法的:

    1
    2
    3
    4
    // 分配并初始化一个 const int
    const int *pci = new const int(1024);
    // 分配并默认初始化一个 const 的空 string
    const string *pcs = new const string;

    类似其他任何 const 对象,一个动态分配的 const 对象必须进行初始化。对于一个定义了默认构造函数的类类型,其 const 动态对象可以隐式初始化,而其他类型的对象就必须显式初始化。由于分配的对象是 const 的,new 返回的指针是一个指向 const 的指针。

  6. 如果 new 不能分配所要求的内存空间,它会抛出一个类型为 bad_alloc 的异常。我们可以改变使用 new 的方式来阻止它抛出异常:

    1
    2
    int *p1 = new int;           // 如果分配失败,new 抛出 std::bad_alloc
    int *p2 = new (nothrow) int; // 如果分配失败,new 返回一个空指针

    称这种形式的 new定位 new(placement new),定位 new 表达式允许我们向 new 传递额外的参数。bad_allocnothrow 都定义在头文件 new 中。

  7. 我们传递给 delete 的指针必须指向动态分配的内存,或者是一个空指针。释放一块并非 new 分配的内存,或者将相同的指针值释放多次,其行为是未定义的。

  8. 通常情况下,编译器不能分辨一个指针指向的是静态还是动态分配的对象。类似的,编译器也不能分辨一个指针所指向的内存是否已经被释放了。对于这些 delete 表达式,大多数编译器会编译通过,尽管它们是错误的。

  9. 虽然一个 const 对象的值不能被改变,但它本身是可以被销毁的。如同任何其他动态对象一样,想要释放一个 const 动态对象,只要 delete 指向它的指针即可。

  10. WARNING: 由内置指针(而不是智能指针)管理的动态内存在被显式释放前一直都会存在。

  11. 使用 newdelete 管理动态内存存在三个常见问题:

    1. 忘记 delete 内存;
    2. 使用已经释放掉的对象;
    3. 同一块内存释放两次。
  12. 最佳实践: 坚持只使用智能指针。

  13. new 出来的动态内存在 delete 之后应将相应的指针变量赋值为 nullptr

12.1.3 shared_ptr 和 new 结合使用

  1. 接受指针参数的智能指针构造函数是 explicit 的。因此,我们不能将一个内置指针隐式转换为一个智能指针,必须使用直接初始化形式来初始化一个智能指针:

    1
    2
    shared_ptr<int> p1 = new int(1024); // 错误:必须使用直接初始化形式
    shared_ptr<int> p2(new int(1024)); // 正确:使用了直接初始化形式
  2. 定义和改变 shared_ptr 的其他方法:

    定义和改变 shared_ptr 的其他方法
    shared_ptr<T> p(q) p 管理内置指针 q 所指向的对象;q 必须指向 new 分配的内存,且能够转换为 T* 类型
    shared_ptr<T> p(u) p 从 unique_ptr u 那里接管了对象的所有权;将 u 置为空
    shared_ptr<T> p(q, d) p 接管了内置指针 q 所指向的对象的所有权。q 必须能转换为 T* 类型。p 将使用可调用对象 d 来代替 delete
    shared_ptr<T> p(p2, d) p 是 shared_ptr p2 的拷贝,唯一的区别是 p 将用可调用对象 d 来代替 delete
    p.reset() 若 p 是唯一指向其对象的 shared_ptr,reset 会释放此对象。若传递了可选的参数内置指针 q,会令 p 指向 q,否则会将 p 置为空。若还传递了参数 d,将会调用 d 而不是 delete 来释放 q
    p.reset(q)
    p.reset(q, d)
  3. 假设我们有这样一个 process 函数:

    1
    2
    3
    4
    void process(shared_ptr<int> ptr)
    {
    // 使用 ptr
    } // ptr 离开作用域,被销毁

    process 函数的参数传递方式为传值,在其内部会创建所传入的 shared_ptr 实参的副本,增加对所使用的动态内存的引用计数(至少为 2),process 结束时,方才使用的动态内存的引用计数会递减,但不会变为 0,因此不会被释放,正确使用 process 函数的方式是传递给它一个 shared_ptr

    1
    2
    3
    shared_ptr<int> p(new int(42)); // 引用计数为 1
    process(p); // 拷贝 p 会递增它的引用计数;在 process 中引用计数值为 2
    int i = *p; // 正确:引用计数值为 1

    process 传入一个由内置指针显式构造的临时的 shared_ptr 可能会引发错误:

    1
    2
    3
    4
    int *x(new int(1024));       // 危险:x是一个普通指针,不是一个智能指针
    process(x); // 错误:不能将 int* 转换为一个 shared_ptr<int>
    process(shared_ptr<int>(x)); // 合法的,但内存会被释放!
    int j = *x; // 未定义的:x 是一个空悬指针!

    在上面的调用中,我们将一个临时 shared_ptr 传递给 process。当这个调用所在的表达式结束时,这个临时对象就被销毁了。销毁这个临时变量会递减引用计数,此时引用计数就变为 0 了。因此,当临时对象被销毁时,它所指向的内存会被释放。但 x 继续指向(已经释放的)内存,从而变成一个空悬指针。如果试图使用 x 的值,其行为是未定义的。

  4. 当将一个 shared_ptr 绑定到一个普通指针时,我们就将内存的管理责任交给了这个 shared_ptr。一旦这样做了,我们就不应该再使用内置指针来访问 shared_ptr 所指向的内存了。

  5. WARNING: 使用一个内置指针来访问一个智能指针所负责的对象是很危险的,因为我们无法知道对象何时会被销毁。

  6. WARNING: get 用来将指针的访问权限传递给代码,你只有在确定代码不会 delete 指针的情况下,才能使用 get。特别是,永远不要用 get 初始化另一个智能指针或者为另一个智能指针赋值。

  7. reset 成员经常与 unique 一起使用,来控制多个 shared_ptr 共享的对象。在改变底层对象之前,我们检查自己是否是当前对象仅有的用户。如果不是,在改变之前要制作一份新的拷贝:

    1
    2
    3
    if (!p.unique())
    p.reset(new string(*p)); // 我们不是唯一用户;分配新的拷贝
    *p += newVal; // 现在我们知道自己是唯一的用户,可以改变对象的值
  8. 练习 12.10: 下面的代码调用了上文中定义的 process 函数,解释此调用是否正确。如果不正确,应如何修改?

    1
    2
    shared_ptr<int> p(new int(42));
    process(shared_ptr<int>(p));

    答:正确,创建 p 的临时拷贝,p 中的引用计数会增加。

  9. 练习 12.11: 如果我们像下面这样调用 process,会发生什么?

    1
    process(shared_ptr<int>(p.get()));

    答:p.get() 返回 p 所管理的动态内存的内置指针,使用该指针初始化一个临时的 shared_ptr,离开 process 后动态内存会被释放,后面对 p 的使用存在危险。

  10. 练习 12.12: p 和 q 的定义如下,对于接下来的对 process 的每个调用,如果合法,解释它做了什么,如果不合法,解释错误原因:

    1
    2
    3
    4
    5
    6
    auto p = new int();
    auto sp = make_shared<int>();
    (a) process(sp);
    (b) process(new int());
    (c) process(p);
    (d) process(shared_ptr<int>(p));

    答:(a)合法,创建一个 sp 的临时拷贝,process 内 sp 的引用计数加 1,离开 process 后 sp 的引用计数恢复;(b)不合法,不存在内置指针向 shared_ptr 的隐式转换;(c)不合法,同(b);(d)合法,创建临时的 shared_ptr 管理 p 所指向的内存,离开 process 后内存会被释放,p 将继续指向已不再可用的内存,虽然合法,但是危险。

  11. 练习 12.13: 如果执行下面的代码,会发生什么?

    1
    2
    3
    auto sp = make_shared<int>();
    auto p = sp.get();
    delete p;

    答:sp 管理一块动态内存,p 通过 sp.get() 获得这块动态内存的访问权限,随后 delete 释放了内存,但 sp 的引用计数并未随之减 1,后续 sp 析构时会再次释放内存。

12.1.4 智能指针和异常

  1. 如果使用内置指针管理内存,且在 new 之后在对应的 delete 之前发生了异常,则内存不会被释放。

  2. 当一个 shared_ptr 被销毁时,它默认地对它管理的指针进行 delete 操作。当我们创建一个 shared_ptr 时,可以传递一个(可选的)指向删除器函数的参数:

    1
    2
    3
    4
    5
    6
    7
    void f(destination &d /* 其他参数 */)
    {
    connection c = connect(&d);
    shared_ptr<connection> p(&c, end_connection);
    // 使用连接
    // 当 f 退出时(即使是由于异常而退出),connection 会被正确关闭
    }
  3. 为了正确使用智能指针,我们必须坚持一些基本规范:

    • 不使用相同的内置指针值初始化(或 reset)多个智能指针;
    • delete get() 返回的指针;
    • 不使用 get() 初始化或 reset 另一个智能指针;
    • 如果你使用 get() 返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变为无效了;
    • 如果你使用智能指针管理的资源不是 new 分配的内存,记住传递给它一个删除器。

12.1.5 unique_ptr

  1. 一个 unique_ptr“拥有”它所指向的对象。与 shared_ptr 不同,某个时刻只能有一个 unique_ptr 指向一个给定对象。当 unique_ptr 被销毁时,它所指向的对象也被销毁。

  2. shared_ptr 不同,没有类似 make_shared 的标准库函数返回一个 unique_ptr。当我们定义一个 unique_ptr 时,需要将其绑定到一个 new 返回的指针上。

  3. 由于一个 unique_ptr 拥有它指向的对象,因此 unique_ptr 不支持普通的拷贝或赋值操作。

  4. unituq_ptr 支持的操作:

    unique_ptr 支持的操作
    unique_ptr<T> u1 空 unique_ptr,可以指向类型为 T 的对象。u1 会使用 delete 来释放它的指针;u2 会使用一个类型为 D 的可调用对象来释放它的指针
    unique_ptr<T, D> u2
    unique_ptr<T, D> u(d) 空 unique_ptr,指向类型为 T 的对象,用类型为 D 的对象 d 代替 delete
    u = nullptr 释放 u 指向的对象,将 u 置为空
    u.release() u 放弃对指针的控制权,返回指针,并将 u 置为空
    u.reset() 释放 u 指向的对象
    u.reset(q) 如果提供了内置指针 q,令 u 指向这个对象;否则将 u 置为空
    u.reset(nullptr)
  5. 虽然我们不能拷贝或赋值 unique_ptr,但可以通过调用 releasereset 将指针的所有权从一个(非 constunique_ptr 转移给另一个 unique_ptr

    1
    2
    3
    4
    5
    // 将所有权从 p1 转移给 p2
    unique_ptr<string> p2(p1.release()); // release 将 p1 置为空
    unique_ptr<string> p3(new string("Trex"));
    // 将所有权从 p3 转移给 p2
    p2.reset(p3.release()); // reset 释放了 p2 原来指向的内存

    release 成员返回 unique_ptr 当前保存的指针并将其置为空。调用 release 会切断 unique_ptr 和它原来管理的对象间的联系。release 返回的指针通常被用来初始化另一个智能指针或给另一个智能指针赋值。在本例中,管理内存的责任简单地从一个智能指针转移给另一个。但是,如果我们不用另一个智能指针来保存 release 返回的指针,我们的程序就要负责资源的释放。

  6. 不能拷贝 unique_ptr 的规则有一个例外:我们可以拷贝或赋值一个将要被销毁的 unique_ptr。最常见的例子是从函数返回一个 unique_ptr

    1
    2
    3
    4
    5
    unique_ptr<int> clone(int p)
    {
    // 正确:从 int* 创建一个 unique_ptr<int>
    return unique_ptr<int>(new int(p));
    }

    还可以返回一个局部对象的拷贝:

    1
    2
    3
    4
    5
    6
    unique_ptr<int> clone(int p)
    {
    unique_ptr<int> ret(new int(p));
    // ...
    return ret;
    }

    对于两段代码,编译器都知道要返回的对象将要被销毁。在此情况下,编译器执行一种特殊的“拷贝”。

  7. 向后兼容:auto_ptr 标准库的较早版本包含了一个名为 auto_ptr 的类,它具有 unique_ptr 的部分特性,但不是全部。特别是,我们不能在容器中保存 auto_ptr,也不能从函数中返回 auto_ptr。虽然 auto_ptr 仍是标准库的一部分,但编写程序时应该使用 unique_ptr

    《Effective C++》第三版中使用的仍然是 auto_ptr

  8. unique_ptr 对象指定删除器的方式与 shared_ptr 有所不同:

    1
    2
    3
    // p 指向一个类型为 objT 的对象,并使用一个类型为 delT 的对象释放 objT 对象
    // 它会调用一个名为 fcn 的 delT 类型对象
    unique_ptr<objT, delT> p(new objT, fcn);

    我们使用 unique_ptr 代替 shared_ptr 重写前面的连接程序:

    1
    2
    3
    4
    5
    6
    7
    8
    void f(destination &d /* 其他需要的参数 */)
    {
    connectionc = connect(&d); // 打开连接
    // 当 p 被销毁时,连接将会关闭
    unique_ptr<connection, decltype(end_connection) *> p(&c, end_connection);
    // 使用连接
    // 当 f 退出时(即使是由于异常而退出),connection 会被正确关闭
    }
  9. 练习 12.17: 下面的 unique_ptr 声明中,哪些是合法的,哪些可能导致后续的程序错误?解释每个错误的问题在哪里。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    int ix = 1024, *pi = &ix, *pi2 = new int(2048);
    typedef unique_ptr<int> IntP;

    (a) IntP p0(ix);
    (b) IntP p1(pi);
    (c) IntP p2(pi2);
    (d) IntP p3(&ix);
    (e) IntP p4(new int(2048));
    (f) IntP p5(p2.get());

    答:(a)不合法,unique_ptr 对象需要用指针进行初始化;(b)合法,但 pi 是个栈区地址;(c)合法,但很危险,因为通过 unique_ptr 和 pi2 都可以操作内存,有可能出现一方释放了内存,但另一方仍在使用内存;(d)合法,但存在和(b)相同的问题;(e)合法,且安全;(f)合法,但两个 unique_ptr 共享对一块内存的拥有权,应使用 release 方法。

12.1.6 weak_ptr

  1. weak_ptr 是一种不控制所指向对象生存期的智能指针,它指向由一个 shared_ptr 管理的对象。将一个 weak_ptr 绑定到一个 shared_ptr 不会改变 shared_ptr 的引用计数。一旦最后一个指向对象的 shared_ptr 被销毁,对象就会被释放。即使有 weak_ptr 指向对象,对象也还是会被释放。

  2. weak_ptr 支持的操作:

    weak_ptr
    weak_ptr<T> w 空 weak_ptr 可以指向类型为 T 的对象
    weak_ptr<T> w(sp) 与 shared_ptr sp 指向相同对象的 weak_ptr。T 必须能转换为 sp 指向的类型
    w = p p 可以是一个 shared_ptr 或一个 weak_ptr。赋值后 w 与 p 共享对象
    w.reset() 将 w 置为空
    w.use_count() 与 w 共享对象的 shared_ptr 的数量
    w.expired() 若 w.use_count() 为 0,返回 true,否则返回 false
    w.lock() 如果 expired 为 true,返回一个空 shared_ptr;否则返回一个指向 w 的对象的 shared_ptr

12.2 动态数组

  1. 最佳实践: 大多数应用应该使用标准库容器而不是动态分配的数组。使用容器更为简单、更不容易出现内存管理错误并且可能有更好的性能。

12.2.1 new 和数组

  1. 在下例中,new 分配要求数量的对象并(假定分配成功后)返回指向第一个对象的指针:

    1
    2
    // 调用 get_size 确定分配多少个 int
    int *pia = new int[get_size()]; // pia 指向第一个 int

    方括号中的大小必须是整型,但不必是常量。

  2. 也可以用一个表示数组类型的类型别名来分配一个数组,这样,new 表达式中就不需要方括号了:

    1
    2
    typedef int arrT[42]; // arrT 表示 42 个 int 的数组类型
    int *p = new arrT; // 分配一个 42 个 int 的数组;p 指向第一个 int
  3. 当用 new 分配一个数组时,我们并未得到一个数组类型的对象,而是得到一个数组元素类型的指针。即使我们使用类型别名定义了一个数组类型,new 也不会分配一个数组类型的对象。由于分配的内存并不是一个数组类型,因此不能对动态数组调用 beginend,这些函数使用数组维度来返回指向首元素和尾后元素的指针。出于相同的原因,也不能用范围 for 语句来处理(所谓的)动态数组中的元素。

  4. WARNING: 动态数组并不是数组类型。

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

    1
    2
    3
    4
    int *pia = new int[10];          // 10 个未初始化的 int
    int *pia2 = new int[10](); // 10 个值初始化为 0 的 int
    string *psa = new string[10]; // 10 个空 string
    string *psa2 = new string[10](); // 10 个空 string

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

    1
    2
    3
    4
    // 10 个 int 分别用列表中对应的初始化器初始化
    int *pia3 = new int[10]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    // 10 个 string,前 4 个用给定的初始化器初始化,剩余的进行值初始化
    string *psa3 = new string[10]{"a", "an", "the", string(3, 'x')};

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

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

  7. 释放动态数组:

    1
    delete[] pa; // pa 必须指向一个动态分配的数组或为空

    数组中的元素按逆序销毁,即,最后一个元素首先被销毁,然后是倒数第二个,依此类推。其中的 [] 是必须的,即使是用 typedef 定义的数组类型别名申请的动态内存:

    1
    2
    3
    typedef int arrT[42]; // arrT 是 42 个 int 的数组的类型别名
    int *p = new arrT; // 分配一个 42 个 int 的数组;p 指向第一个元素
    delete[] p; // 方括号是必需的,因为我们当初分配的是一个数组

    对应《Effective C++》第三版条款 16。

  8. 可以使用 unique_ptr 方便地管理动态数组:

    1
    2
    3
    // up 指向一个包含 10 个未初始化 int 的数组
    unique_ptr<int[]> up(new int[10]);
    up.release(); // 自动用 delete[] 销毁其指针

    由于 up 指向一个数组,当 up 销毁它管理的指针时,会自动使用 delete[]

  9. 当一个 unique_ptr 指向一个数组时,我们不能使用点和箭头成员运算符,可以使用下标运算符来访问数组中的元素:

    1
    2
    for (size_t i = 0; i != 10; ++i)
    up[i] = i; // 为每个元素赋予一个新值
  10. unique_ptr 不同,shared_ptr 不直接支持管理动态数组。如果希望使用 shared_ptr 管理一个动态数组,必须提供自己定义的删除器

    1
    2
    3
    // 为了使用 shared_ptr,必须提供一个删除器
    shared_ptr<int> sp(new int[10], [](int *p) { delete[] p; });
    sp.reset(); // 使用我们提供的 lambda 释放数组,它使用 delete[]

    如果未提供删除器,这段代码将是未定义的。默认情况下,shared_ptr 使用 delete 销毁它指向的对象。如果此对象是一个动态数组,对其使用 delete 所产生的问题与释放一个动态数组指针时忘记 [] 产生的问题一样。

  11. shared_ptr 不直接支持动态数组管理这一特性会影响我们如何访问数组中的元素:

    1
    2
    3
    // shared_ptr 未定义下标运算符,并且不支持指针的算术运算
    for (size_t i = 0; i != 10; ++i)
    *(sp.get() + i) = i; // 使用 get 获取一个内置指针

    shared_ptr 未定义下标运算符,而且智能指针类型不支持指针算术运算。因此,为了访问数组中的元素,必须用 get 获取一个内置指针,然后用它来访问数组元素。

12.2.2 allocator 类

  1. 标准库 allocator 类定义在头文件 memory 中,它帮助我们将内存分配和对象构造分离开采。它提供一种类型感知的内存分配方法,它分配的内存是原始的、未构造的。

  2. allocator 是一个模板,当一个 allocator 对象分配内存时,它会根据给定的对象类型来确定恰当的内存大小和对齐位置:

    1
    2
    allocator<string> alloc;          // 可以分配 string 的 allocator 对象
    auto const p = alloc.allocate(n); // 分配 n 个未初始化的 string

    这个 allocate 调用为 n 个 string 分配了内存。

  3. allocator 支持的操作:

    标准库 allocator 类及其算法
    allocator<T> a 定义了一个名为 a 的 allocator 对象,它可以为类型为 T 的对象分配内存
    a.allocate(n) 分配一段原始的、未构造的内存,保存 n 个类型为 T 的对象
    a.deallocate(p, n) 释放从 T* 指针 p 中地址开始的内存,这块内存保存了 n 个类型为 T 的对象;p 必须是一个先前由 allocate 返回的指针,且 n  必须是 p 创建时所要求的大小。在调用 deallocate 之前,用户必须对每个在这块内存中创建的对象调用 destroy
    a.construct(p, args) p 必须是一个类型为 T* 的指针,指向一块原始内存;arg 被传递给类型为 T 的构造函数,用来在 p 指向的内存中构造一个对象
    a.destroy(p) p 为 T* 类型的指针,此算法对 p 指向的对象执行析构函数
  4. WARNING: 为了使用 allocate 返回的内存,我们必须用 construct 构造对象。使用未构造的内存,其行为是未定义的。

  5. 标准库还为 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
    2
    3
    4
    5
    6
    // 分配比 vi 中元素所占用空间大一倍的动态内存
    auto p = alloc.allocate(vi.size() * 2);
    // 通过拷贝 vi 中的元素来构造从 p 开始的元素
    auto q = uninitialized_copy(vi.begin(), vi.end(), p);
    // 将剩余元素初始化为 42
    uninitialized_fill_n(q, vi.size(), 42);

    类似 copyuninitialized_copy 返回(递增后的)目的位置迭代器。因此,一次 uninitialized_copy 调用会返回一个指针,指向最后一个构造的元素之后的位置。

12.3 使用标准库:文本查询程序

12.3.1 文本查询程序设计

12.3.2 文本查询程序类的定义

小结

术语表

第 12 章术语表


Thank you for your donate!