- 全局对象在程序启动时分配,在程序结束时销毁
- 对于局部自动对象,当我们进入其定义所在的程序块时被创建,在离开时销毁
- 局部 static 对象在第一次使用前分配,在程序结束时销毁
12.1 动态内存与智能指针
- 在C++中,动态内存的管理是通过一对运算符来完成的:
new
,在动态内存中为对象分配空间并返回一个指向该对象的指针,我们可以选择对对象进行初始化。delete
,接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。 - 动态内存的使用很容易出现问题,因为确保在正确的时间释放内存是及其困难的。有时我们会忘记释放内存,在这种情况下会产生
内存泄露
;有时在尚有指针引用内存的情况下我们就释放了它,在这种情况下就会产生引用非法内存的指针。 - 为了更容易(同时也更安全)地使用动态内存,新的标准库提供了两种智能指针类型来管理动态对象。智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。新标准库提供的这两种智能指针的区别在于管理底层指针的方式;
shared_ptr
允许多个指针指向同一个对象;unique_ptr
则'独占'所指向的对象。标准库还定义了一个名为weak_ptr
的伴随类,它是一种弱引用,指向 shared_ptr 所管理的对象。这三种类型都定义在 memory 头文件中
12.1.1 shared_ptr 类
shared_ptr 和 unique_ptr 都支持的操作
shared_ptr<T> 空指针,可以指向类型为 T 的对象
unique_ptr<T>
p 将 p 作为一个条件判断,若 p 指向一个对象,则为 true
*p 解引用 p ,获得它指向的对象
p->mem 等价于 (*p).mem
p.get() 返回 p 中保存的指针。要小心使用,若智能指针释放了其对象,返回的指针所指向的对象也就消失了
swap(p, q) 交换 p 和 q 中的指针
p.swap(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 共享对象的智能指针数量;可能很慢,主要用于调试
make_shared 函数
最安全的分配和使用动态内存的方法是调用一个名为 make_shared 的标准库函数。此函数在动态内存中分配一个对象并初始化它,返回指向此对象的 shared_ptr 。与智能指针一样,make_shared 也定义在头文件 memory 中
// 指向一个值为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>();
// 当然,我们通常用 auto 定义一个对象来保存 make_shared 的结果,这种方式比较简单
// p6 指向一个动态分配的空 vector<string>
auto p6 = make_shared<vector<string>>();
shared_ptr 的拷贝和赋值
- 当进行拷贝或赋值操作时,每个 shared_ptr 都会记录有多少个其他 shared_ptr 指向相同的对象
auto p = make_shared<int>(42); p 指向的对象只有 p 一个引用者
quto q(p); p q 指向相同的对象,此对象有两个引用者
- 我们可以认为每个 shared_ptr 都有一个关联的计数器,通常称其为引用计数。无论何时我们拷贝一个 shared_ptr,计数器都会递增。
- 例如,当用一个 shared_ptr 初始化另一个 shared_ptr,或将它作为参数传递给一个函数以及作为一个函数的返回值时,它所关联的计数器都会递增。
- 当我们给 shared_ptr 赋予一个新值或是 shared_ptr 被销毁时(例如一个局部的 shared_ptr 离开其作用域)时,计数器就会递减。
- 一旦一个 shared_ptr 的计数器变为 0,它就会自动释放自己所管理的对象
auto r = make_shared<int>(42); r 指向的 int 只有一个引用者
r = q; 给 r 赋值,令它指向另一个地址
递增 q 指向的对象的引用计数
递减 r 原来指向的对象的引用计数
r 原来指向的对象已没有引用者,会自动释放
shared_ptr 自动销毁所管理的对象
- 当指向一个对象的最后一个 shared_ptr 被销毁时,shared_ptr 类会自动销毁此对象。它通过另一个特殊的成员函数 - 析构函数来完成销毁操作
- shared_ptr 的析构函数会递减它所指向的对象引用计数。如果引用计数变为 0,shared_ptr 的析构函数就会销毁对象,并释放它占用的内存
shared_ptr 还会自动释放相关联的内存
- 当动态对象不再被使用时,shared_ptr 类会自动释放动态对象,这一特性使得动态内存的使用变得非常容易。例如,我们可能有一个函数,它返回一个 shared_ptr ,指向一个 Foo 类型的动态分配的对象,对象是通过一个类型为 T 的参数进行初始化的:
// factory 返回一个 shared_ptr ,指向一个动态分配的对象
shared_ptr<Foo> factory(T arg){
// 恰当处理 arg
// shared_ptr 负责释放内存
return make_shared<Foo>(arg);
}
- 由于 factory 返回一个 shared_ptr ,所以我们可以确保它分配的对象会在恰当的时刻被释放。例如,下面的函数将 factory 返回的 shared_ptr 保存在局部变量中:
void use_factory(T arg){
shared_ptr<Foo> p = factory(arg);
// 使用 p
} // p 离开了作用域,它指向的内存会被自动释放掉
-
由于 p 是 use_factory 的局部变量,在 use_factory 结束时它将被销毁。当 p 被销毁时,将递减其引用计数并检查它是否为 0。在此例中,p 是唯一引用 factory 返回的内存的对象。由于 p 将要销毁,p 指向的这个对象也会被销毁,所占用的内存会被释放
-
如果有其他 shared_ptr 也指向这块内存,它就不会被释放掉
shared_ptr<Foo> use_factory(T arg){
shared_ptr<Foo> p = factory(arg);
// 使用 p
return p; // 当我们返回 p 时,引用计数进行了递增操作
} // p 离开了作用域,但它指向的内存不会被释放掉
- 注意:如果你将 shared_ptr 存放在一个容器中,而后不再需要全部元素,而只使用其中一部分,要记得用 erase 删除不再需要的那些元素
12.1.3 直接管理内存
- C++语言定义了两个运算符来分配和释放动态内存。
- 运算符
new
分配内存,delete
释放 new 分配的内存 - 相对于智能指针,使用这两个运算符管理内存非常容易出错
使用 new 和 delete 管理动态内存存在的三个常见问题
- 忘记 delete 内存。忘记释放动态内存会导致人们常说的 "内存泄露" 问题,因为这种内存永远不可能被归还给自由空间了。查找内存泄露错误是非常困难的,因为通常应用程序运行很长时间后,真正耗尽内存时,才能检测到这种错误
- 使用已经释放掉的对象。通过在释放内存后将指针置为空,有时可以检测出这种错误
- 同一块内存释放两次。当有两个指针指向相同的动态分配对象时,可能发生这种错误。如果对其中一个指针进行了 delete 操作,对象的内存就被归还给自由空间了。如果我们随后又 delete 第二个指针,自由空间就可能被破坏
-
注意:
相对于查找和修正这些错误来说,制造出这些错误要简单得多 - 坚持只使用智能指针,就可以避免所有这些问题。对于一块内存,只有在没有任何智能指针指向它的情况下,智能指针才会自动释放它
空悬指针
delete 一个指针之后,指针值就变为无效了。虽然指针已经无效了,但在很多机器上仍然保存着(已经释放了的)动态内存的地址。在 delete 之后,指针就变成了人们所说的空悬指针,即,指向一块曾经保存数据但现在已经无效的内存的指针
网友评论