第十二章:动态内存
静态内存区(全局区):保存局部 static 对象、类 static 数据成员以及定义在任何函数之外的变量,分配在静态内存的对象由编译器自动创建和销毁,static 对象在使用之前分配,在程序结束时销毁
栈内存区:用来保存定义在函数内的非 static 对象,分配在栈内存的对象由编译器自动创建和销毁,对于栈对象,仅在其定义的程序块运行时才存在
堆内存区:程序用堆来存储动态分配的对象——即那些在程序运行时分配的对象,动态对象的生存期由程序来控制,当动态对象不再使用时,我们的代码必须显式地销毁它们
12.1 动态内存与智能指针
为了更容易及安全的使用动态内存,新的标准库提供了两种智能指针类型来管理动态对象
智能指针的行为类似常规指针,区别在于它负责自动释放所指向的对象
这两种智能指针的区别在于管理底层指针的方式:
- shared_ptr 允许多个指针指向同一个对象
- unique_ptr 独占所指向的对象
标准库还定义了一个名为 weak_ptr 的伴随类,这是一种弱引用,指向 shared_ptr 所管理的对象
注:这三种类型都定义在 memory 头文件中
1. shared_ptr 类
1)make_shared 函数:最安全的分配和使用动态内存的方式是调用一个名为 make_shared 的标准库函数,此函数在动态内存中分配一个对象并初始化它,返回指向此对象的 shared_ptr
make_shared(args)
shared_ptr<int> p3 = make_shared<int>(42);
shared_ptr<string> p4 = make_shared<string>(10, '9');
shared_ptr<int> p5 = make_shared<int>();
- 调用 make_shared 函数时,传递的参数必须与其给定类型的某个构造函数相匹配
- 如果我们不传递任何参数,对象就会进行值初始化
通常,我们使用 auto 定义一个对象来保存 make_shared 的结果,这样比较简单
auto p6 = make_shared<vector<string>>();
2)shared_ptr 的拷贝和赋值:每个 shared_ptr 都有一个关联的计数器,通常称之为引用计数
引用计数递增的情况:
- 当用一个 shared_ptr 初始化另一个 shared_ptr
- 将一个 shared_ptr 作为参数传递给一个函数
- 将一个 shared_ptr 作为函数的返回值
引用计数递减的情况: - 当给 shared_ptr 赋予一个新值
- shared_ptr 被销毁(例如,一个局部的 shared_ptr 离开其作用域)
当一个 shared_ptr 的计数器变为 0,它就会自动释放自己所管理的对象
3)shared_ptr 自动销毁所管理的对象并自动释放相关联的内存 - shared_ptr 的析构函数递减它所指向的对象的引用计数,若引用计数变为 0,shared_ptr 的析构函数会销毁对象并释放它占用的内存
- 若将 shared_ptr 存放于一个容器中,而后不再需要全部元素,而只使用其中一部分,要记得用 erase 删除不再需要的那些元素
4)使用动态生存期的资源的类
程序使用动态内存主要有以下三个原因 - 程序不知道自己需要使用多少对象(容器类)
- 程序不知道所需对象的准确类型(第15章)
- 程序需要在多个对象间共享数据,允许多个对象共享相同的状态(下面的例子)
例:定义一个名为 Blob 的类,保存一组元素,并希望 Blob 对象的不同拷贝之间共享相同的元素
Blob<string> b1; //空 Blob
{
Blob<string> b2 = {
"a", "an", "the"};
b1 = b2; //b1 和 b2共享相同的元素
} //b2 被销毁了,但是 b2 中的内容没有销毁
//b1 指向最初由 b2 创建的元素
2.直接管理内存
C++定义了两个运算符来分配和释放内存,运算符 new 分配内存,delete 释放 new 分配的内存
1)使用 new 动态分配和初始化对象
- 默认情况下,动态分配的对象是默认初始化的,这意味着内置类型或组合类型的对象的值是未定义的,而类类型对象将使用默认构造函数进行初始化
int *pi = new int; //pi指向一个未初始化的int
string *ps = new string; //初始化为空string
- 可以使用直接初始化方式来初始化一个动态分配的对象,也可以使用传统的构造方式(使用圆括号),也可以使用列表初始化(花括号)
int *pi = new int(1024); //直接初始化
string *ps = new string(10, '9'); //传统的构造方式
vector<int> *pv = new vector<int>{
0, 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
2)动态分配的 const 对象
类似其他任何 const 对象,一个动态分配的 const 对象必须进行初始化,对于一个定义了默认构造函数的类类型,其 const 动态对象可以隐式初始化,而其他类型的对象就必须进行显式初始化
//分配并初始化一个 const int
const int *pi = new const int(1024); //显式初始化
//分配并默认初始化一个 const 的空 string
const string *pcs = new const string; //隐式初始化
由于分配的对象是 const 的,new 返回的指针是一个指向 const 的指针
3)内存耗尽(p 408)
4)释放动态内存
- 通过 delete 表达式来将动态内存归还给系统,delete 表达式接受一个指针指向我们想要释放的对象
- delete 表达式执行两个动作:销毁给定的指针指向的对象、释放对应的内存
- 由内置指针(而不是智能指针)管理的动态内存在被显示释放之前一直都会存在
5)使用 new 和 delete 管理动态内存存在的三个常见问题 - 忘记 delete 内存,这回导致内存泄露问题
- 使用已经释放掉的对象
- 同一块内存释放两次
坚持只使用智能指针,可以避免所有这些问题,对于一块内存,只有在没有任何智能指针指向它的情况下,智能指针才会自动释放它
6)delete 之后重置指针值,但也只提供有限的保护 - 空悬指针:当 delete 一个指针后,指针值就变为无效了,虽然指针无效但在很多容器上指针仍然保存着已经释放了的动态内存的地址,即指向一块曾经保存数据对象但现在已经无效的内存的指针
- 为避免空悬指针问题,在指针即将离开其作用域之前释放掉它所关联的内存,这样在指针关联的内存被释放后就没有机会继续使用指针了
- 若需要保留指针,可以在 delete 之后将 nullptr 赋予指针,这样该指针不指向任何对象
- delete 内存之后重置指针的方法只对这个指针有效,对其他任何指向(已释放的)内存的指针无效,在实际系统中,查找指向相同内存的指针是很困难的
int *p(new int(42)); //p指向动态内存
auto q = p; //p和q指向相同的内存
delete p; //p和q均变为无效
p = nullptr; //指出p不再绑定到任何对象
在我们释放 p 所指向的内存时,q 也变为无效了,但重置 p 对 q 没有任何作用
3. shared_ptr 和 new 结合使用
- 如果不初始化一个智能指针,它就会被初始化为一个空指针;我们也可以用 new 返回的指针来初始化智能指针
shared_ptr<double> p1; // shared_ptr 可以指向一个 double
shared_ptr<int> p2(new int(24)); //p2 指向一个值为42的int
- 接受指针参数的智能指针构造函数是 explicit 的,因此我们不能将一个内置指针隐式转换为一个智能指针,必须使用直接初始化形式来初始化一个智能指针
shared_ptr<int> p1 = new int(1024); //错误:必须使用直接初始化形式
shared_ptr<int