《C++ Primer》 拾遗 第 12 章 动态内存
Notes Cpp Primer
Lastmod: 2021-08-01 周日 00:44:34

第 12 章 动态内存

静态内存用来存储 局部 static 对象,类 static 成员,定义在任何函数之外的变量。

栈内存用来存储 定义在函数内的非 static 对象。

除了这两部分,程序还拥有一个内存池。这部分内存被称为自由空间 free store 或 堆 heap。程序用堆来存储动态分配 dynamically allocate 的对象。

12.1 动态内存与智能指针

memory 头文件定义了 shared_ptr,unique_ptr 和 weak_ptr。

12.1.1 shared_ptr 类

智能指针使用类似于普通指针

shared_ptr<T> sp;
unique_ptr<T> up; // 空智能指针,指向类型为 T 的对象
p;                // p 作为条件判断,若 p 指向对象则返回 true
*p;              
p->mem;
p.get();          // p 中保存的指针。若释放了对象,则指针指向的对象也消失。
swap(p,q);
p.swap(q);        // 交换 p q 中的指针。

shared_ptr 独有的操作

make_shared<T>(args); // 返回一个 shared_ptr,指向一个动态分配的 
                      // T 类型的对象。使用 args 初始化对象。
shared_ptr<T> p(q);   // p 是 q 的拷贝,q 中指针必须能转化为 T*。
                      // 递增 q 中的计数器。
p = q;                // p q 都是 shared_ptr 且所保存的指针能够互相转化
                      // p 的计数器递减,q 的计数器递增
                      // 若 p 的计数器为 9 则将其管理的原内存释放
p.unique();           // 若 p.use_count() 为 1 则返回 true
p.use_count();        // 返回与 p 共享对象的智能指针数量;
                      // 可能很慢,用于调试。

每一个 shared_ptr 都有一个关联的计数器,称为引用计数 reference count

当引用计数为 0 时,shared_ptr 会调用析构函数 destructor 自动完成对象的销毁工作和释放所占的内存。

12.1.2 直接管理内存

动态分配 const 对象
const int *i = new const int(123);
内存耗尽

一般来说 new 不能分配所要求的的空间时会抛出 bad_alloc 异常。但我们也可以阻止抛出异常。

int *p1 = new int;
int *p2 = new (nothrow) int; // 分配失败则返回空指针

我们称这种 new 的形式为定位 new placement new

bad_alloc 和 nothrow 都定义在头文件 new 中。

释放动态内存

注意不要使用 delete 释放非动态分配的内存。

delete 之后重置指针值

应该在 delete 之后的指针被称为空悬指针 dangling pointer。 应把 delete 的指针置为空指针。但是如果有其他指针也指向了这个空间,则其他指针可能指向无效的内存。

12.1.3 shared_ptr 和 new 结合使用

我们除了可以使用 make_shared 也可以使用 new 返回的指针初始化 shared_ptr。

shared_ptr<int> p(new int(123));
shared_ptr<T> p(q);    // 接管 new 创建的指针指向的对象。
shared_ptr<T> p(u);  // 从 unique_ptr 接管对象所有权 u 置为空
shared_ptr<T> p(q, d); // 接管内置指针 q 的对象所有权
                       // 并且调用 d 代替 delelte
shared_ptr<T> p(p2, d);  // 拷贝另一个 shared_ptr 
                         // 并且调用 d 代替 delete
p.reset();            // 若 p 是唯一指向其对象的 shared_ptr 则释放对象
p.reset(q);           // 除了可能发生的释放,会将 p 指向 q
p.reset(q, d);

不要混用普通指针与智能指针

要避免将一块内存绑定到多个独立的 shared_ptr

不要使用 get 初始化另一个智能指针或为智能指针赋值。

可以配合 unique 来修改当前副本

if (!p.unique())
    p.reset(new string(*p));
*p += newVal;

12.1.4 智能指针和异常

注意无论是程序正常退出还是发生异常,局部对象都会被销毁,因此使用智能指针在异常发生时也可以正常运作,但 new 的指针不行。如果在 new 和对应的 delete 之间发生异常则内存不会被释放。

使用我们自己的释放操作

默认情况下 shared_ptr 指向动态内存,因此 shared_ptr 销毁时,我们对其中管理的指针进行 delete 操作。我们也可以使用 shared_ptr 来管理一些资源,此时需要我们定义一个函数作为删除器 deleter

void end_connection(connection *p) { disconnect(*p); }
void f(destination &d) {
    connection c = connect(&d);
    shared_ptr<connnection> p(&c, end_connection);
    ...
}
智能指针陷阱

注意非正常使用的智能指针也可能造成内存管理问题。要保证

不适用相同的内置指针初始化或 reset 多个智能指针。

不 delete get() 返回的指针。

不适用 get() 初始化或 reset 另一个只能指针。

如果使用 get 返回的指针,要知道最后一个对应的智能指针销毁后该指针也会失效。

如果使用智能指针管理资源而不是 new 分配的内存,记住传一个删除器。

12.1.5 unique_ptr

unique_ptr 会拥有它指向的对象。某时刻只能有一个 unique_ptr 指向一个给定对象。unique_ptr 被销毁时其指向的对象也被销毁。

定义 unique_ptr 时要绑定一个 new 返回的指针。也可以采用直接初始化的形式。不支持拷贝和赋值。

unique_ptr<T> u1;
unique_ptr<T, D> u2;      // D 为可调用删除器
unique_ptr<T, D> u(d); 
u = nullptr;              // 释放 u 指向的对象,将 u 置为空
u.release();              // u 放弃对指针的控制权,返回指针,并将 u 置空
u.reset();                // 释放 u 指向的对象
u.reset(q);               // 指向内置指针 q 的对象
u.reset(nullptr);

auto_ptr 具有 unique_ptr 的部分功能,因为向后兼容所以保留,但编写程序应该使用 unique_ptr。

向 unique_ptr 传递删除器与 shared_ptr 有所不同,除了传递函数,还需要在模板实例化时指定删除器的类型。

void f(dest &d) {
    connection c = connect(&d);
    unique_ptr<connection, decltype(end_connection)*>
        p(&c, end_connection);
}

12.1.6 weak_ptr

weak_ptr 不控制指向对象生存周期,它指向 shared_ptr 管理的对象。

将一个 weak_ptr 钢钉到 shared_ptr 不会改变 shared_ptr 的引用计数。

一旦最后一个指向对象的 shared_ptr 被销毁,对象会被释放。

weak_ptr<T> w;
weak_ptr<T> w(sp); // 绑定到 shared_ptr sp 指向的对象
w = p;             // p 是 shared_ptr 或 weak_ptr
w.reset();         // 将 w 置空
u.use_count();     // 与 w 共享对象的 shared_ptr 的数量
w.expired();       // 若 w.use_count() 为 0 则返回 true
w.lock();          // 如 expired 为 true 则返回空 shared_ptr
                   // 否则返回指向 w 的对象的 shared_ptr

我们一般访问 weak_ptr 的对象会用

if (shared_ptr<int> np = wp.lock()) {
   // np ... 
}

12.2 动态数组

动态数组并不是数组类型。

int *pa = new int[get_size()];

typedef int arrT[42];
int *p = new arrT;

不能对动态数组调用 begin 和 end。

初始化动态分配对象的数组
int *pia = new int[10];          // 未初始化
int *pia2 = new int[10]();       // 10 个 0
int *pia3 = new int[10]{0,1,2,3,4,5,6,7,8,9};
string *psa = new string[10];    // 10 个空 string
string *psa2 = new string[10](); // 10 个空 string
string *psa3 = new string[10]{"a","the",string(3,'x')};

注意使用列表初始化时,如果列表给出的元素数量大于申请的长度则会抛出 bad_array_new_length 异常。

动态分配空数组是合法的
// char arr[0]; // 非法
char *cp = new char[0]; // 正确但不能解引用,类似于尾后指针
动态指针和动态数组
unique_ptr<int[]> up(new int[10]);
// 不支持 . 和 -> 成员访问
up[i]; // 支持下标访问
up.release(); // 调用 delete[]

shared_ptr 不直接管理动态数组。如果需要则需要自定义删除器

shared_ptr<int> sp(new int[10], [](int *p){ delete[] p; });

访问数组元素也会复杂一些

for (size_t i = 0; i != 10; i++) {
    *(sp.get() + i) = i;
}

12.2.2 allocator 类

有时我们需要分开分配内存和初始化,这时我们就可以用 allocator。

allocator<T> a;       // 能够分配 T 类型的空间
a.allocate(n);        // 分配 n 个 T 类型的空间
a.deallocate(p, n);   // 释放 p 开头的 n 个 T 的空间
a.construct(p, args); // 使用 args 构造 p 指向的对象
a.destroy(p);         // 析构 p 指向的对象,析构后可以构造其他对象

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

uninitialized_copy(b, e, b2);
uninitialized_copy_n(b, n, b2);
uninitialize_fill(b, e, t);
uninitialize_fill_n(b, n, t);
Prev: 《C++ Primer》 拾遗 第 11 章 关联容器
Next: 《C++ Primer》 拾遗 第 13 章 拷贝控制