第十二章 动态内存
- 对象的生命周期:
- 全局对象在程序启动时分配,结束时销毁。
- 局部对象在进入程序块时创建,离开块时销毁。
- 局部
static
对象在第一次使用前分配,在程序结束时销毁。 - 动态分配对象:只能显式地被释放。
- 对象的内存位置:
- 静态内存用来保存局部
static
对象、类static
对象、定义在任何函数之外的变量。 - 栈内存用来保存定义在函数内的非
static
对象;由操作系统自动分配和释放,内存空间比较小 - 堆内存,又称自由空间,用来存储动态分配的对象;手动申请和释放,内存空间比较大
- 静态内存用来保存局部
12.1 动态内存与智能指针
12.1.0 动态内存
- 使用
new
动态分配内存,返回的是一个指向该对象的指针- 动态分配的对象是默认初始化的,也可以使用值初始化、直接初始化(圆括号中有初始值)、列表初始化方式来进行初始化
- 对于类而言,值初始化与默认初始化没有区别
- 对于内置类型而言,
- 值初始化有一个确定的初始值:
int *p = new int(); // 此时所指对象值为0
- 默认初始化的初始值未定:
int *p = new int;
- 直接初始化:
auto *p = new int(2);
- 值初始化有一个确定的初始值:
- 可以使用new分配const对象,返回指向const类型的指针,但是动态分配的const对象必须初始化
const int* pc = new const int(1);
- new失败会抛出
bad_alloc
异常- 使用定位new可以阻止抛出异常,定位new允许将new传递额外参数
- 如果传递
nothrow
给new,则new在分配失败之后会返回空指针:int* p = new(nothrow) int;
- 动态分配的对象是默认初始化的,也可以使用值初始化、直接初始化(圆括号中有初始值)、列表初始化方式来进行初始化
- 使用
delete
销毁对象,并释放内存delete
后的指针称为空悬指针(dangling pointer),应该在delete之后将指针值置空。
- 使用
new/delete
,要么容易忘记释放内存引起内存泄露,要么释放内存后再使用引起use after free - 智能指针:定义在头文件
memory
中shared_ptr
:允许多个指针指向同一个对象,共享内存unique_ptr
:独占所指向的对象weak_ptr
:是一种弱引用,指向shared_ptr
所管理的对象
12.1.1 shared_ptr
声明和初始化
|
|
- 语法相关:
- 使用内置指针
q
进行初始化必须使用直接初始化形式(如sp2
),不能使用拷贝初始化,因为调用了explicit的转换构造函数 - 默认内置指针
q
必须指向动态内存(因为智能指针默认使用delete
释放对象),如果将智能指针绑定到指向其他类型资源的指针上,需要使用自定义的删除器代替delete
- 使用内置指针
- 推荐使用
sp6
的初始化方式make_shared
,即不要混合使用智能指针和内置指针- 不推荐
sp2
方式进行初始化,因为同一个内置指针q
不能绑定到多个独立创建的shared_ptr,否则析构时多次delete - 如果使用
q
创建shared_ptr后(比如sp2
),不要再使用q
,因为q
无法知道对象何时被shared_ptr释放,随时可能变成空悬指针
1 2 3 4 5 6 7 8 9 10
void add(shared_ptr<int> p) {} int* p1 = new int(1); shared_ptr<int> sp1(p1); // shared_ptr<int> sp(p1); // 报错,p1绑定到独立的两个shared_ptr int *p2 = new int(2); add(shared_ptr<int>(p2)); // 函数调用完后,智能指针引用计数为0,p2所指向内存被释放,此时p2成为悬空指针,危险! sp3 = make_shared<int>(3); // 推荐 add(sp3);
- 不推荐
其他操作
操作 | 解释 |
---|---|
p.get() | 返回p 中保存的指针。如果智能指针释放了对象,则get返回一个悬空指针。 |
p.use_count() | 返回与p 共享对象的智能指针的数量 |
p.unique() | 当前对象是否被p 独占(或者当p.use_count()==1 时返回true ) |
p.reset() | 如果p 时唯一指向其对象的shared_ptr ,则释放此对象 |
p.reset(q) | 令p 指向内置指针q |
p.reset(q, d) | 令p 指向内置指针q ,并调用删除器d (而非默认删除器)来释放q |
- 智能指针不支持指针算数运算
get
函数- 智能指针的
get
函数返回一个内置指针,指向智能指针管理的对象,主要用于向不能使用智能指针的代码传递内置指针。 - 使用get返回的指针不能用来delete
- 不要使用get函数初始化另一个智能指针或为智能指针赋值(因为析构时多次delete)
- 只有在确定代码不会delete指针的情况下,才使用get
- 智能指针的
unique
函数通常用与reset
一起使用,检查shared_ptr是否独占当前对象,如果不是需要使用reset
指向新的元素或拷贝。例子。
使用建议
- 不使用相同的内置指针初始化或
reset
多个智能指针 - 不
delete get()
返回的指针。 - 不使用
get()
初始化或reset
另一个智能指针 - 如果你使用
get()
返回的指针,记得当最后一个对应的智能指针销毁后,你的指针就无效了。 - 如果你使用智能指针管理的资源不是
new
分配的内存,记住传递给它一个删除函数。
12.1.2 unique_ptr
声明和初始化
|
|
- unique_ptr将删除器类型放在尖括号中,因为删除器类型也是unique_ptr类型的一部分
- 同一时刻只能有一个
unique_ptr
指向一个给定的对象。当unique_ptr被销毁时,它指向的对象也被销毁1 2 3
int p = new int(1); uniuqe_ptr<int> u(p); // 正确,但是u销毁之后p成为空悬指针 uniuqe_ptr<int> up(new int(1)); // 推荐写法
- unique_ptr必须使用内置指针进行直接初始化(圆括号初始化),不支持拷贝或赋值操作(unique的含义,而且其拷贝构造函数是删除的)
- 例外:可以拷贝或赋值一个即将被销毁的unique_ptr(移动构造、移动赋值)
其他操作
操作 | 解释 |
---|---|
u.get() | 返回u 中保存的指针。如果智能指针释放了对象,则get 返回一个悬空指针。 |
u.release() | u 放弃对指针的控制权(但不会释放指向对象的内存),返回内置指针,并将u 置空。 |
u.reset() | 释放u 指向的对象 |
u.reset(q) | 令u 指向内置指针q 指向的对象,u 原来指向的对象被释放 |
- release返回的指针通常用来初始化另一个智能指针(reset)或给智能指针赋值
1 2 3 4
unique_ptr<int> p1(new int(1)), p2(new int(2)), p3(new int(3)); p1.release(); // p1放弃了对象的控制权,对象的内存没有释放,而且对象的指针丢失 int* p = p2.release(); // 使用p保存对象的指针,但是后续需要使用delete(p)释放内存 unique_ptr<int> u(p3.release()); // u接管p3
12.1.3 weak_ptr
声明和初始化
|
|
weak_ptr
是一种不控制所指向对象生存期的智能指针。
其他操作
操作 | 解释 |
---|---|
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 。 |
- weak_ptr不能直接访问对象。因为如果shared_ptr被销毁,即使有weak_ptr指向对象,对象仍然可能被释放
- 使用weak_ptr访问对象时,必须先调用lock函数,以检查指向的对象是否仍然存在
1 2 3
shared_ptr<int> sp = make_shared<int>(1); weak_ptr<int> wp(sp); if (shared_ptr<int> p = wp.lock()) cout<<*p<<endl;
12.2 动态数组
C++中提供了两种动态数组的分配方式:
- new动态数组,将内存分配和对象构造结合在一起,对应的delete将对象析构和内存释放结合在一起
- 使用allocator类,可以实现内存分配与对象构造的分离,管理内存更灵活
12.2.1 动态数组
new
和动态数组
new
一个动态数组,返回指向第一个对象的指针(返回的指针不是数组类型,而是数组元素类型)- 由于new分配的内存不是数组类型(比如
int[10]
),因此不能对动态数组调用begin和end,也不能使用range-for遍历元素
- 由于new分配的内存不是数组类型(比如
- new的数组可以进行值初始化、列表初始化
1 2 3 4
const int sz = 10; int *p1 = new int[sz]; // 没有初始化 int *p2 = new int[sz](); // 值初始化为0 int *p3 = new int[sz]{1,2,3,4,5}; // 列表初始化
- 因为值初始化时不能提供参数,所以没有默认构造函数的类是无法动态分配数组的。
- 动态分配一个空数组是合法的,此时返回一个合法的非空指针,类似于尾后指针
- 使用
delete []
释放动态分配的数组,使用delete
释放动态分配的对象
unique_ptr
和动态数组
可以使用unique_ptr管理new分配的数组
|
|
shared_ptr
和动态数组
shared_ptr
不支持直接管理动态数组,如果想用shared_ptr
管理动态数组,必须提供自定义的删除器(否则使用delete释放动态数组,报错)shared_ptr
未定义下标运算符,智能指针也不支持指针算数运算。可以通过get函数获取内置指针再进行访问
|
|
12.2.2 allocator类
- 标准库
allocator
类定义在头文件memory
中,帮助我们将内存分配和对象构造分离开。 - 分配的是原始的、未构造的内存,程序需要再内存中构造对象。(直接使用未构造的内存是未定义的行为)
- 对象使用完之后,需要对每个构造的元素调用destroy进行销毁
标准库allocator类及其方法
操作 | 解释 |
---|---|
allocator<T> a | 定义了一个名为a 的allocator 对象,它可以为类型为T 的对象分配内存 |
a.allocate(n) | 分配一段原始的、未构造的内存,保存n 个类型为T 的对象,返回一个指向类型T 的指针 |
a.deallocate(p, n) | 释放从T* 指针p 中地址开始的内存,这块内存保存了n 个类型为T 的对象;p 必须是一个先前由allocate 返回的指针。且n 必须是p 创建时所要求的大小。在调用deallocate 之前,用户必须对每个在这块内存中创建的对象调用destroy 。 |
a.construct(p, args) | p 必须是一个类型是T* 的指针,指向一块原始内存;args 被传递给类型为T 的构造函数,用来在p 指向的内存中构造一个对象。使用时需要p++ 移动指针 |
a.destroy(p) | p 为T* 类型的指针,此算法对p 指向的对象执行析构函数。 |
- construct和destroy一次只能构造或销毁一个对象,使用中可能需要使用指针对每个元素进行遍历
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 必须指向足够大的未构造的原始内存,能够容纳给定数量的对象。 |
- 进行拷贝和填充未初始化内存
- 返回最后一个构造元素的尾后位置