动态内存的管理是通过一对运算符来完成的:new
,在动态内存中为对象分配空间并返回一个指向该对象的指针,我们可以选择对对象进行初始化;delete
,接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。
对象的生命周期:
-
全局对象在程序启动时分配,结束时销毁。
-
局部对象在进入程序块时创建,离开块时销毁。
-
局部
static
对象在第一次使用前分配,在程序结束时销毁。 -
动态分配对象:只能显式地被释放。
-
对象的内存位置:
- 静态内存用来保存局部
static
对象、类static
对象、定义在任何函数之外的变量。 - 栈内存用来保存定义在函数内的非
static
对象。 - 堆内存,又称自由空间,用来存储动态分配的对象。
- 静态内存用来保存局部
一、new和delete直接管理内存
1、new动态分配内存
1.1 用new动态分配和初始化对象
new
无法为分配的对象命名(因为自由空间分配的内存是无名的),因此是返回一个指向该对象的指针。
|
|
1.2 初始化new分配的对象的方法
- 默认情况下,动态分配的对象是默认初始化的。这意味着内置类型和组合类型的对象的值将是未定义的,而类类型对象将用默认构造函数进行初始化
|
|
- 可以使用直接初始化的方式来初始化一个动态分配的对象。即传统的圆括号构造和新标准下的列表初始化
|
|
- 也可以对动态分配的对象进行值初始化。只需要在其后加一对括号即可
|
|
1.3 动态分配const对象
new返回的也是一个const指针 除非有默认构造函数的类,不然必须显示化初始化变量
1.4 内存耗尽
一旦内存耗尽,会抛出类型是bad_alloc
的异常。
2、delete释放内存
2.1 释放动态内存
用delete
将动态内存归还给系统。
接受一个指针,这个指针必须指向动态分配的内存或者一个空指针。完成两个动作 :
- 销毁给定的指针指向的对象
- 释放对应的内存。
2.2 delete和指针
编译器不能分辨一个指针指向的是静态还是动态分配的对象,也不能判断是否指针已经被delete
3、直接管理动态内存存在的常见问题
1.忘记delete
内存。
2.使用已经释放掉的对象。
3.同一块内存释放两次。
4.多个指针指向同一块动态内存的删除问题
5.空悬指针问题:delete
后的指针称为[[对 delete -ptr 的理解(释放内存、空悬指针、重复释放).pdf | 空悬指针]](dangling pointer)
C/C++ 中为什么 delete 一块内存后,该内存不可复用
坚持只使用智能指针可以避免上述所有问题。
二、动态内存与智能指针
动态内存的使用很容易出问题,因为确保在正确的时间释放内存是极其困难的。有时我们会忘记释放内存,在这种情况下就会产生内存泄漏;有时在尚有指针引用内存的情况下我们就释放了它,在这种情况下就会产生引用非法内存的指针。 为了更容易(同时也更安全)地使用动态内存,新的标准库提供了两种智能指针(smart pointer)类型来管理动态对象。
智能指针:
- 管理动态对象。
- 行为类似常规指针。
- 负责自动释放所指向的对象。
- 智能指针也是模板。
shared_ptr
允许多个指针指向同一个对象;
unique_ptr
则"独占”所指向的对象。
标准库还定义了一个名为weak_ptr
的伴随类,它是一种弱引用,指向shared_ptr所 管理的对象。
这三种类型都定义在memory
头文件中。
1、shared_ptr类
1.1 定义
|
|
1.2 初始化
1.2.1 结合new初始化
用new返回的指针来初始化,但是必须直接初始化 ,因为智能指针的构造函数是explicit,所以不能将一个内置指针隐式转化为智能指针
|
|
1.2.2 make_shared函数
- 为什么使用 :最安全的分配和使用动态内存的方法是调用一个名为make_shared 的标准函数
- 作用 : 此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr
- 用法 : make_shared 用其参数来构造给定类型的对象当要用
make_shared
时,必须指定想要创建的对象的类型。定义方式与模板类相同, 在函数名之后跟一个尖括号,在其中给出类型
|
|
1.2.3 使用非动态内存初始化
默认情况下,一个用来初始化智能指针的普通指针必须指向动态内存,因为智能指针默认使用delete释放它所关联的对象。我们可以将智能指针绑定到一个指向其他类型的 资源的指针上,但是为了这样做,必须提供自己的操作来替代delete。
|
|
1.3 定义和改变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) |
同上 |
1.4 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) |
交换p 和q 中的指针 |
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.unique() |
若p.use_count() 是1,返回true ;否则返回false |
p.use_count() |
返回与p 共享对象的智能指针数量;可能很慢,主要用于调试。 |
使用动态内存的三种原因 |
- 程序不知道自己需要使用多少对象(比如容器类)
- 程序不知道所需要对象的准确类型。
- 程序需要在多个对象间共享数据。
1.5 智能指针和异常
- 如果使用智能指针,即使程序块由于异常过早结束,智能指针类也能确保在内存不需要的时候将其释放。
- 智能指针陷阱:
- 不用相同的内置指针初始化(或
reset
)多个智能指针 - 不
delete get()
返回的指针。 - 如果你使用
get()
返回的指针,记得当最后一个对应的智能指针销毁后,你的指针就无效了。 - 如果你使用智能指针管理的资源不是
new
分配的内存,记住传递给它一个删除器。
- 不用相同的内置指针初始化(或
2、unique_ptr
- 某一个时刻只能有一个
unique_ptr
指向一个给定的对象,并“拥有”该对象。 - 当定义一个unique_ptr时,需要将其绑定到一个new返回的指针上,必须采用直接初始化形式。
- 不支持拷贝或者赋值操作。只能可以通过调用release或 reset将指针的所有权从一个(非const) unique_ptr转移给另一个 unique
|
|
- 向后兼容:
auto_ptr
:老版本,具有unique_ptr
的部分特性。特别是,不能在容器中保存auto_ptr
,也不能从函数返回auto_ptr
。 - 不能拷贝unique_ptr的规则有一个例外:我们可以拷贝或赋值一个将要被销毁的unique_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 指向q 指向的对象 |
u.reset(nullptr) |
将u 置空 |
3、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_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 。 |
创建一个weak_ptr
我们创建一个weak_ptr时,要用一个shared_ptr来初始化它
|
|
由于对象可能不存在,我们不能使用weak_ptr直接访问对象,必须调用lock此函数检查weak_ptr指向的对象是否仍存在。如果存在,lock返回一个指向共享对象的 shared_ptr。
|
|
三、动态数组
1、new和数组
1.1 new
一个动态数组
- 类型名之后加一对方括号,指明分配的对象数目(必须是整型,不必是常量)。
- 返回指向第一个对象的指针。
|
|
- 方括号中的大小必须是整形,但不必是常量。也可以用一个表示数组类型的类型别名来分配一个数组这样new表达式中就不需要方括号了。
|
|
- 动态分配一个空数组也是合法的,这样的指针就类似于尾后指针。
1.2 初始化动态数组
- 方法一:在数组大小之后跟一对空括号
|
|
- 方法二:提供一个元素初始化器的花括号列表
|
|
2、delete
释放一个动态数组
为了释放动态数组,我们使用一种特殊形式的delete— 在指针前加上一个空方括号对
|
|
方括号对是必需的:它指示编译器此指针指向一个对象数组的第一个元素。即使使用类型别名,也需要方括号对
3、智能指针unique_ptr
和动态数组
3.1 定义与销毁
- 在定义时,在对象类型后面跟一对空方括号。
|
|
类型说明符中的方括号(<int[]>
)指 出 up 指向一个int数组而不是一个int。由于 up指向一个数组,当 up销毁它管理的指针时,会自动使用delete[]
3.2 操作
指向数组的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
不直接支持管理动态数组,如果希望使用则需要定义自己的删除器。并且其未定义下标运算符,而且智能指针类型不支持指针算数术运算。为了访问元素,必须用get获取一个内置指针然后访问元素
4、allocator类
4.1 new、delete 管理动态数组之困
new、detele将内存分配和对象构造组合在了一 起 但是存在一种情况,希望将内存分配和对象构造分离。这意味着我们可以分配大块内存,但只在真正需要时才真正执行对象创建操作。 这暴露了这种管理动态数组的缺点:可能会造成不必要的浪费
|
|
问题1:在不知道有多少输入字符串时,就预先分配了n个元素的数组 问题2:赋值了两次,第一次默认初始化,随后又赋值 问题3:没有默认构造函数的类就不能动态分配数组
4.2 allocator类
标准库allocator
类定义在头文件memory
中,帮助我们将内存分配和对象构造分离开。
- 分配的是原始的、未构造的内存。
allocator
是一个模板。当一个 allocator对象分配内存时,它会根据给定的对象类型来确定恰当的内存大小和对齐位置
4.2.1 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* 的指针,指向一块原始内存;args 被传递给类型为T 的构造函数,用来在p 指向的内存中构造一个对象。 |
a.destroy(p) |
p 为T* 类型的指针,此算法对p 指向的对象执行析构函数。 |
allocator分配的内存是未构造的(unconstructed)。我们按需要在此内存中构造对 象。在新标准库中,construct成员函数接受一个指针和零个或多个额外参数,在给定位置构造一个元素。额外参数用来初始化构造的对象
|
|
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 必须指向足够大的未构造的原始内存,能够容纳给定数量的对象。 |
- 定义在头文件
memory
中。 - 在给定目的位置创建元素,而不是由系统分配内存给他们。动态内存的管理是通过一对运算符来完成的:new,在动态内存中为对象 分配空间并返回一个指向该对象的指针,我们可以选择对对象进行初始化;delete,接 受一个动态对象的指针,销毁该对象,并释放与之关联的内存。