• 对象的生命周期:

    • 全局对象在程序启动时分配,结束时销毁。
    • 局部对象在进入程序块时创建,离开块时销毁。
    • 局部static对象在第一次使用前分配,在程序结束时销毁。
    • 动态分配对象:只能显式地被释放。
  • 对象的内存位置:

    • 静态内存用来保存局部static对象、类static对象、定义在任何函数之外的变量。
    • 栈内存用来保存定义在函数内的非static对象。
    • 堆内存,又称自由空间,用来存储动态分配的对象。
  • 如果忘记释放内存就会产生内存泄漏;如果在尚有指针引用的情况下就释放就会产生引用非法内存的指针。

动态内存与智能指针

  • 动态内存管理:
    • new:在动态内存中为对象分配空间并返回一个指向该对象的指针。
    • delete:接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。
  • 智能指针:
    • 管理动态对象。
    • 行为类似常规指针。
    • 负责自动释放所指向的对象。
    • 智能指针也是模板。
    • 定义在memory头文件中。

shared_ptr类

shared_ptr和unique_ptr都支持的操作

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

shared_ptr独有的操作

最安全的分配和使用动态内存的方法是调用一个名为make_shared的标准库函数。

操作 解释
make_shared<T>(args) 返回一个shared_ptr,指向一个动态分配的类型为T的对象。使用args初始化此对象(类似于顺序容器的emplace)。
shared_ptr<T>p(q) pshared_ptr q的拷贝;此操作会递增q中的计数器。q中的指针必须能转换为T*
p = q pq都是shared_ptr,所保存的指针必须能互相转换。此操作会递减p的引用计数,递增q的引用计数;若p的引用计数变为0,则将其管理的原内存释放。
p.unique() p.use_count()是1,返回true;否则返回false
p.use_count() 返回与p共享对象的智能指针数量;可能很慢,主要用于调试。
  • 当用一个shared_ptr初始化另一个shared_ptr,或讲它作为参数传递给一个函数,以及作为函数的返回值时,它所关联的计数器就会递增。
  • 如果将shared_ptr存放在一个容器中,而后不再需要全部元素,而只使用其中的一部分,要记得使用erase删除不再需要的元素。
  • 使用动态内存的三种原因
    • 程序不知道自己需要使用多少对象(比如容器类)。
    • 程序不知道所需要对象的准确类型。(第15章会讲)
    • 程序需要在多个对象间共享数据。
  • 利用拷贝赋值运算符或拷贝构造vector时,都是深复制。

练习12.4

Q: 在我们的 check 函数中,没有检查 i 是否大于0。为什么可以忽略这个检查?

1
2
3
void check(size_type i, const string &msg) const {
if (i >= data->size()) throw std::out_of_range(msg);
}

A: 因为 size_type 是一个无符号整型,当传递给 check 的参数小于 0 的时候,参数值会转换成一个正整数。

直接管理内存

默认情况下,动态分配对象使用默认初始化(P40),内置类型或组合类型的对象的值是未定义的,而类类型对象使用默认构造函数进行初始化。

1
2
3
4
string *ps1 = new string;  // 默认初始化为空string
string *ps = new string(); // 值初始化为空string
int *pi1 = new int; // 默认初始化,*pi1的值未定义
int *pi2 = new int(); // 值初始化为0, *pi2为0

对于一个定义了默认构造函数的类类型,其const动态对象可以隐式初始化,而其他类型的对象必须显式初始化。

个人理解:显式初始化可以通过初始化设初值或列表初始化实现,由于const动态对象是底层const,指针的值是修改的,所以不一定要显式初始化。

1
2
3
4
5
6
7
class A {
public:
A(int x) { pci = new int(x); }
const int *pci;
const int ci = 10;
// const int ci; // error: uninitialized const member in 'const int'
};
  • new动态分配和初始化对象。
    • new分配const对象是合法的。分配并初始化一个const int: const int *pci = new const int(1024);
    • new无法为分配的对象命名(因为自由空间分配的内存是无名的),因此是返回一个指向该对象的指针。
    • int *pi = new int(123); 接小括号是初始化值,接中括号是动态数组(new数组时中括号后面还能加小括号,但小括号中不能设定任何初始化值,加小括号时编译器提供默认初始化值,不加小括号时不提供任何初始值)。
    • 一旦内存耗尽,会抛出类型是bad_alloc的异常。
      可以使用 placemen new (定位new)可以让new在不能分配所需内存时返回一个空指针,而非抛出异常:int *p2 = new (nothrow) int;
      bad_allocnothrow 的都定义在头文件 new 中。
  • delete销毁给定指针所指向的对象,并释放对应的内存。
    • 传递给delete的指针必须指向动态分配的内存,或是一个空指针。
      释放一块非new分配的内存,或讲相同的指针释放多次都是未定义的。
    • delete后的指针称为空悬指针(dangling pointer)。
    • 虽然const对象的值不能被改变,但可以被销毁。
  • 使用newdelete管理动态内存存在三个常见问题:
    • 1.忘记delete内存。
    • 2.使用已经释放掉的对象。
    • 3.同一块内存释放两次。
  • 坚持只使用智能指针可以避免上述所有问题。

练习12.8

Q: 下面的函数是否有错误?如果有,解释错误原因。

1
2
3
4
5
bool b() {
int* p = new int;
// ...
return p;
}

A: 有错误。p会被强制转换成bool,继而没有释放指针 p 指向的对象。

shared_ptr和new结合使用

  • 接受指针参数的智能指针构造函数是explicit的,不能将内置指针隐式的转换为一个智能指针:
1
2
3
4
5
6
7
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>
return shared_ptr<int>(new int(p)); // 正确
}
  • 不要混合使用普通指针和智能指针,也不要使用get初始化另一个智能指针或为智能指针赋值。
    get函数是为了这样一种情况设计的:我们需要向不能使用智能指针的代码传递一个内置指针。
  • 默认情况下,一个用来初始化智能指针的普通指针必须指向动态内存,因为智能指针默认使用delete释放所关联的对象(但也可提供自己的操作来代替delete,如下)。

定义和改变shared_ptr的其他方法

操作 解释
shared_ptr<T> p(q) p管理内置指针q所指向的对象;q必须指向new分配的内存,且能够转换为T*类型
shared_ptr<T> p(u) punique_ptr u那里接管了对象的所有权;将u置为空
shared_ptr<T> p(q, d) p接管了内置指针q所指向的对象的所有权。q必须能转换为T*类型。p将使用可调用对象d来代替delete
shared_ptr<T> p(p2, d) pshared_ptr p2的拷贝,唯一的区别是p将可调用对象d来代替delete
p.reset() p是唯一指向其对象的shared_ptrreset会释放此对象。若传递了可选的参数内置指针q,会令p指向q,否则会将p置空。若还传递了参数d,则会调用d而不是delete来释放q
p.reset(q) 同上
p.reset(q, d) 同上
  • 不能将一个指针赋予shared_ptr: p = new int(1024);
    可以使用reset来将一个新的指针赋予一个shared_ptr:

    1
    2
    3
    if (!p.unique())
    p.reset(new string(*p)); // 若我们不是唯一用户,则分配新的拷贝
    *p += newVal;

练习12.10

Q: 下面的代码调用了第413页中定义的process 函数,解释此调用是否正确。如果不正确,应如何修改?

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

A: 正确。正常执行shared_ptr拷贝构造构造一个临时对象,共享引用计数+1,拷贝给形参后引用计数再+1,这个时候临时对象销毁,引用计数-1,函数结束后,形参销毁,引用计数-1,引用计数现在为1此时也只剩p一个实例了,如果程序结束,引用计数-1,p的析构函数释放资源。

练习12.11

Q: 如果我们像下面这样调用 process,会发生什么?

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

A: 这样会创建一个新的智能指针,它的引用计数为 1,这个智能指针所指向的空间与 p 相同。在表达式结束后,这个临时智能指针会被销毁,引用计数为 0,所指向的内存空间也会被释放。而导致 p 所指向的空间被释放,使得 p 成为一个空悬指针。

练习12.12

Q: psp 的定义如下,对于接下来的对 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 :

  • (a) 合法。将sp 拷贝给 process函数的形参,在函数里面引用计数为 2,函数结束后引用计数为 1。
  • (b) 不合法。不能从内置指针隐式转换为智能指针。
  • © 不合法。不能从内置指针隐式转换为智能指针。
  • (d) 合法。但是智能指针和内置指针一起使用可能会出现问题,在表达式结束后智能指针会被销毁,它所指向的对象也被释放。而此时内置指针 p 依旧指向该内存空间。之后对内置指针 p 的操作可能会引发错误。

练习12.13

Q: 如果执行下面的代码,会发生什么?

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

A: 智能指针 sp 所指向空间已经被释放,再对 sp 进行操作会出现错误。

智能指针和异常

  • 如果使用智能指针,即使程序块由于异常过早结束,智能指针类也能确保在内存不需要的时候将其释放
  • 避免智能指针陷阱
    • 不用相同的内置指针初始化(或reset)多个智能指针
    • delete get()返回的指针。
    • 如果你使用get()返回的指针,记得当最后一个对应的智能指针销毁后,你的指针就无效了。
    • 如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器,比如connection。

unique_ptr

  • 某一个时刻只能有一个unique_ptr指向一个给定的对象。
  • 与shared_ptr不同,没有类似make_shared的标准函数。定义unique_ptr时,需要将其绑定到一个new返回的指针。与shared_ptr一样,必须采用直接初始化形式。
  • 不支持拷贝或者赋值操作
  • 向后兼容:auto_ptr:老版本,具有unique_ptr的部分特性。特别是,不能在容器中保存auto_ptr,也不能从函数返回auto_ptr

unique_ptr操作:

操作 解释
unique_ptr<T> u1 unique_ptr,可以指向类型是T的对象。u1会使用delete来是释放它的指针。
unique_ptr<T, D> u2 u2会使用一个类型为D的可调用对象来释放它的指针。
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) 释放u指向的对象,令u指向q指向的对象
u.reset(nullptr) u置空
  • 虽然不能拷贝或赋值 unique_ptr,但可以通过调用release或reset将指针的所有权从一个unique_ptr转移到另一个unique。
1
2
3
4
5
6
unique_ptr<string> p2(p1.release());
unique_ptr<string> p3(new string("Trex"));
p2.reset(p3.release()); // reset释放了p2原来指向的内存

p2.release(); // 错误:p2不会释放内存,而且我们丢失了指针
auto p = p2.release(); // 正确,但必须记得delete(p)
  • 不能拷贝 unique_ptr 的规则有一个例外:可以拷贝或赋值一个即将要被销毁的 unique_ptr。比较常见的例子是向函数传递unique_ptr参数和返回unique_ptr。

练习12.17

Q: 下面的 unique_ptr 声明中,哪些是合法的,哪些可能导致后续的程序错误?解释每个错误的问题在哪里。

1
2
3
4
5
6
7
8
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 时,需要将其绑定到一个new 返回的指针上。
  • (b) 不合法。理由同上。
  • © 合法。但是也可能会使得 pi2 成为空悬指针。
  • (d) 不合法。当 p3 被销毁时,它试图释放一个栈空间的对象。
  • (e) 合法。
  • (f) 不合法。p5p2 指向同一个对象,当 p5p2 被销毁时,会使得同一个指针被释放两次。

练习12.18

Q: shared_ptr 为什么没有 release 成员?

A: release 成员的作用是放弃控制权并返回指针,因为在某一时刻只能有一个 unique_ptr 指向某个对象,unique_ptr 不能被赋值,所以要使用 release 成员将一个 unique_ptr 的指针的所有权传递给另一个 unique_ptr。而 shared_ptr 允许有多个 shared_ptr 指向同一个对象,因此不需要 release 成员。

weak_ptr

  • weak_ptr是一种不控制所指向对象生存期的智能指针。
  • 指向一个由shared_ptr管理的对象,不改变shared_ptr的引用计数。
  • 一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放,不管有没有weak_ptr指向该对象。

weak_ptr操作:

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

动态数组

new和数组

  • new T[]一个动态数组:

    • 类型名之后加一对方括号,指明分配的对象数目(必须是整型,不必是常量)。
    • 返回指向第一个对象的指针
    • int *p = new int[size];
  • delete []一个动态数组:

    • delete [] p;
  • unique_ptr和数组:

    • 指向数组的unique_ptr不支持成员访问运算符(点和箭头),毕竟 unique_ptr 指向的是一个数组而不是单个对象。
    • 当一个unique_ptr指向一个数组时,可以使用下标运算符来访问数组中的元素。
  • shared_ptr和数组:

    • shared_ptr 不支持直接支持管理动态数组(使用默认删除器delete删除动态数组是未定义的),如果一定要用,必须提供自定义的删除器,如shared_ptr<int> sp(new int[10], [](int *p) {delete[] p;});
    • shared_ptr未定义下标运算符,而且智能指针类型不支持指针算术运算。为了访问数组中的元素,必须用get获取一个内置指针,然后用其来访问数组元素。
操作 解释
unique_ptr<T[]> u u可以指向一个动态分配的数组,整数元素类型为T
unique_ptr<T[]> u(p) u指向内置指针p所指向的动态分配的数组。p必须能转换为类型T*
u[i] 返回u拥有的数组中位置i处的对象。u必须指向一个数组。

allocator类

  • 标准库allocator类定义在头文件memory中,帮助我们将内存分配和对象构造分离开。
  • 分配的是原始的、未构造的内存。
  • allocator是一个模板。
  • allocator<string> alloc;

标准库allocator类及其算法

操作 解释
allocator<T> a 定义了一个名为aallocator对象,它可以为类型为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*的指针,指向一块原始内存;args被传递给类型为T的构造函数,用来在p指向的内存中构造一个对象。
a.destroy(p) pT*类型的指针,此算法对p指向的对象执行析构函数。
  • 还未construct对象的情况下就使用原始内存是未定义的;我们只能对真正构造了的元素进行destroy操作。
  • 一旦元素被销毁后,可以重新使用这部分内存来保存其他string,也可以通过deallocate来将其归还给系统。

allocator伴随算法

操作 解释
uninitialized_copy(b, e, b2) 从迭代器be给定的输入范围中拷贝元素到迭代器b2指定的未构造的原始内存中。b2指向的内存必须足够大,能够容纳输入序列中元素的拷贝。
uninitialized_copy_n(b, n, b2) 从迭代器b指向的元素开始,拷贝n个元素到b2开始的内存中。
uninitialized_fill(b, e, t) 在迭代器be执行的原始内存范围中创建对象,对象的值均为t的拷贝。
uninitialized_fill_n(b, n, t) 从迭代器b指向的内存地址开始创建n个对象。b必须指向足够大的未构造的原始内存,能够容纳给定数量的对象。
  • 定义在头文件memory中。
  • 在给定目的位置创建元素,而不是由系统分配内存给他们。
  • uninitialized_copy的目的位置迭代器必须指向还未构造的内存,与copy不同,uninitialized_copy会在给定目的位置构造元素。
  • 类似copy,uninitialized_copy会返回(递增后的)目的位置迭代器,即指向最后一个构造的元素之后的位置。