- 静态内存用来保存局部
static
对象、类static
数据成员以及定义在任何函数之外的变量 - 栈内存用来保存定义在函数内的非
static
对象 - 分配在静态或栈内存中的对象由编译器自动创建和销毁
- 对于栈对象,仅在其定义的程序块运行时才存在;
static
对象在使用之前分配,在程序结束时销毁 - 除了静态内存和栈内存,每个程序还拥有一个内存池,这部分内存被称作自由空间(free store)或堆(heap)
- 程序用堆来存储动态分配(dynamically allocate)的对象,即那些在程序运行时分配的对象
- 动态对象的生存期由程序来控制,即当动态对象不再使用时,代码必须显式地销毁它们
动态内存与智能指针
容易出错的两种运算符
new
- 在动态内存中为对象分配空间并返回一个指向该对象的指针
- 可以选择对对象进行初始化
delete
- 接受一个动态对象的指针,销毁该对象,并释放与之关联的内存
动态内存的使用很容易出问题,因为确保在正确的时间释放内存是极其困难的
- 忘记释放内存 - 产生内存泄漏
- 在尚有指针引用内存的情况下就释放了它 - 产生引用非法内存的指针
为了更容易更安全地使用动态内存,新的标准库提供了两种智能指针(smart pointer)类型来管理动态对象。智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。新标准库提供的这两种智能指针的区别在于管理底层指针的方式:
shared_ptr
允许多个指针指向同一个对象unique_ptr
则“独占”所指向的对象- 标准库还定义了一个名为
weak_ptr
的伴随类,它是一种弱引用,指向shared_ptr
所管理的对象 - 这三种类型都定义在 memory 头文件中
shared_ptr 类
(前6个为 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
中的指针
- 交换
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
的标准库函数。
// 指向一个值为42的 int 的 shared_ptr
shared_ptr<int> p3 = make_shared<int> (42);
// p4 指向一个值为“9999999999”的 string
// 传递的参数需与 string 的某个构造函数相匹配
shared_ptr<string> p4 = make_shared<string> (10, '9');
// p5 指向一个值初始化的 int,即值为0
shared_ptr<int> p5 = make_shared<int> ();
// p6 指向一个动态分配的空 vector<string>
auto p6 = make_shared<vector<string>>();
shared_ptr 的拷贝和赋值
-
每个
shared_ptr
都有一个关联的计数器,通常称其为引用计数(reference count) -
无论何时拷贝一个
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
的析构函数会递减它所指向的对象的引用计数- 如果引用计数变为0,
shared_ptr
的析构函数就会销毁对象,并释放它占用的内存
shared_ptr 自动释放相关联的内存
// factory 返回一个 shared_ptr,指向一个动态分配的对象
shared_ptr<Foo> factory(T arg)
{
// 恰当地处理 arg
// shared_ptr 负责释放内存
return make_shared<Foo> (arg);
}
void use_factory(T arg)
{
shared_ptr<Foo> p = factory(arg);
// 使用 p
} // p 离开了作用域,它指向的内存会被自动释放掉
shared_ptr<Foo> use_factory(T arg)
{
shared_ptr<Foo> p = factory(arg);
// 使用 p
return p; // 当我们返回 p 时,引用计数进行了递增操作
} // p 离开了作用域,但它指向的内存不会被释放掉
// 在此版本中,use_factory 中的 return 语句向此函数的调用者返回了一个 p 的拷贝
由于在最后一个 shared_ptr
销毁前内存都不会释放,保证 shared_ptr
在无用之后不再保留就非常重要了。如果忘记了销毁程序不再需要的 shared_ptr
,程序仍会正确执行,但会浪费内存。
shared_ptr
在无用之后仍然保留的一种可能情况是,将 shared_ptr
存放在一个容器中,随后重排了容器,从而不再需要某些元素。在这种情况下,应该确保用 erase
删除那些不再需要的 shared_ptr
元素。
使用了动态生存期的资源的类
程序使用动态内存出于以下三种原因之一:
1. 程序不知道自己需要使用多少对象(比如:容器)
2. 程序不知道所需对象的准确类型
3. 程序需要在多个对象间共享数据
直接管理内存
使用 new 动态分配和初始化对象
在自由空间分配的内存是无名的,因此 new
无法为其分配的对象命名,而是返回一个指向该对象的指针。
对象初始化
-
默认初始化
- 内置类型或组合类型的对象的值将是未定义的
- 类类型对象将用默认构造函数进行初始化
string *ps = new string; // 初始化为空string int *pi = new int; // pi 指向一个未初始化的 int
-
直接初始化
- 传统的构造方式(使用圆括号)
- 在新标准下,可以使用列表初始化(使用花括号)
int *pi = new int(1024); string *ps = new string(10, '9'); vector<int> *pv = new vector<int>{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
-
值初始化
- 在类型名之后跟一对空括号
string *ps1 = new string; // 默认初始化为空 string string *ps = new string(); // 值初始化为空 string int *pi1 = new int; // 默认初始化;*pi1 的值未定义 int *pi2 = new int(); // 值初始化为0;*pi2 为0
-
auto
:推断想要分配的对象的类型auto p1 = new auto(obj); // p 指向一个与 obj 类型相同的对象,该对象用 obj 进行初始化 auto p2 = new auto{a, b, c}; // 错误:括号中只能有单个初始化器
动态分配的 const 对象
一个动态分配的 const
对象必须进行初始化。
对于一个定义了默认构造函数的类类型,其 const
动态对象可以隐式初始化,而其他类型的对象就必须显式初始化。
由于分配的对象是 const
的,new
返回的指针是一个指向 const
的指针。
内存耗尽
一旦一个程序用光了它所有可用的内存,new
表达式就会失败。
int *p1 = new int; // 如果分配失败,new 抛出 std::bad_alloc
int *p2 = new (nothrow) int; // 如果分配失败,new 返回一个空指针
定位 new(placement new)
- 允许向
new
传递额外的参数- 比如
nothrow
:不能抛出异常,这种形式的new
不能分配所需内存,会返回一个空指针 bad_alloc
和nothrow
都定义在头文件 new 中
- 比如
释放动态内存
delete
- 销毁给定的指针指向的对象
- 释放对应的内存
指针指和 delete
-
传递给
delete
的指针必须指向动态分配的内存,或者是一个空指针 -
释放一块并非
new
分配的内存,或者将相同的指针值释放多次,其行为是未定义的 -
虽然一个
const
对象的值不能被改变,但它本身是可以被销毁的const int *pci = new const int(1024); delete pci;
动态对象的生存期直到被释放时为止
由 shared_ptr
管理的内存在最后一个 shared_ptr
销毁时会被自动释放。但对于一个由内置指针管理的动态对象,直到被显式释放之前他它都是存在的。
void use_factory(T arg)
{
Foo *p = factory(arg); // p 是一个内置指针,而不是一个智能指针
// ...
delete p;
}
Foo* use_factory(T arg)
{
Foo *p = factory(arg);
// ...
return p; // 调用者必须释放内存
}
使用 new
和 delete
管理动态内存存在三个常见问题:
- 忘记
delete
内存 - 使用已经释放掉的对象
- 同一块内存释放两次
delete 之后重置指针值
当我们 delete
一个指针后,指针值就变为无效了。虽然指针已经无效,但在很多机器上指针仍然保存着(已经释放了的)动态内存的地址。
在 delete
之后,指针就变成了人们所说的空悬指针(dangling pointer),即,指向一块曾经保存数据对象但现在已经无效的内存的指针。
未初始化指针的所有缺点空悬指针也都有。
可以在指针即将要离开其作用域之前释放掉它所关联的内存,这样就没有机会继续使用指针了。
如果需要保留指针,可以在 delete
之后将 nullptr
赋予指针,这样就清楚地指出指针不指向任何对象。但这个方法也只对这个指针有效,对其他任何仍指向(已释放的)内存的指针是没有作用的。
shared_ptr 和 new 结合使用
不能将一个内置指针隐式转换为一个智能指针,必须使用直接初始化形式来初始化一个智能指针。
shared_ptr<int> p1 = new int(1024); // 错误:必须使用直接初始化形式
shared_ptr<int> p2(new int(1024)); // 正确:使用了直接初始化形式
shared_ptr<int> clone(int p) {
return new int(p); // 错误:隐式转换为 shared_ptr<int>
}
shared_ptr<int> clone(int p) {
// 正确:显式地用 int* 创建 shared_ptr<int>
return shared_ptr<int>(new int(p));
}
定义和改变 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.reset(q)
/p.reset(q, d)
- 若
p
是唯一指向其对象的shared_ptr
,reset
会释放此对象 - 若传递了可选的参数内置指针
q
,会令p
指向q
,否则会将p
置为空 - 若还传递了参数
d
,将会调用d
而不是delete
来释放q
- 若
不要混合使用普通指针和智能指针,也不要用 get
初始化另一个智能指针或为智能指针赋值。
// 在函数被调用时 ptr 被创建并初始化
// process 的参数是传值方式传递的,因此实参会被拷贝到 ptr 中
void process(shared_ptr<int> ptr)
{
// ...
} // ptr 离开作用域,被销毁
shared_ptr<int> p(new int(42)); // 引用计数为1
process(p); // 拷贝 p 会递增他的引用计数;在 process 中引用计数值为2
int i = *p; // 正确:引用计数值为1
int *x(new int(1024)); // 危险:x 是一个普通指针,不是一个智能指针
process(x); // 错误:不能将 int* 转换为一个 shared_ptr<int>
process(shared_ptr<int> (x)); // 合法的,但内存会被释放!
int j = *x; // 未定义的:x 是一个空悬指针
get
返回一个内置指针,指向智能指针管理的对象。
当将一个 shared_ptr
绑定到一个普通指针时,我们就将内存的管理责任交给了这个 shared_ptr
。一旦这样做了,我们就不应该再使用内置指针来访问 shared_ptr
所指向的内存了。
shared_ptr<int> p(new int(42)); // 引用计数为1
int *q = p.get(); // 正确:但使用 q 时要注意,不要让它管理的指针被释放
{ // 新程序块
// 未定义:两个独立的 shared_ptr 指向相同的内存,各自的引用计数都是1
shared_ptr<int> (q);
} // 程序块结束,q 被销毁,它指向的内存被释放
int foo = *p; // 未定义:p 指向的内存已经被释放了,p 变成一个空悬指针
get
用来将指针的访问权限传递给代码,只有在确定代码不会 delete
指针的情况下,才能使用 get
。特别是,永远不要用 get
初始化另一个智能指针或者为另一个智能指针赋值。
其他 shared_ptr 操作
与赋值类似,reset
会更新引用计数,如果需要的话,会释放 p
指向的对象。
p = new int(1024); // 错误:不能将一个指针赋予 shared_ptr
p.reset(new int(1024)); // 正确:p 指向一个新对象
if (!p.unique())
p.reset(new string(*p)); // 我们不是唯一用户;分配新的拷贝
*p += newVal; // 现在我们知道自己是唯一的用户,可以改变对象的值
智能指针和异常
函数的退出有两种可能,正常处理结束或者发生了异常,无论哪种情况,局部对象都会被销毁。使用 shared_ptr
会自动释放内存:
void f()
{
shared_ptr<int> sp(new int(42));
// 这段代码抛出一个异常,且在 f 中未被捕获
} // 在函数结束时 shared_ptr 自动释放内存
与之相对的,当发生异常时,直接管理的内存是不会自动释放的。如果使用内置指针管理内存,且在 new
之后在对应的 delete
之前发生了异常,则内存不会被释放:
void f()
{
int *ip = new int(42);
// 这段代码抛出一个异常,且在 f 中未被捕获
delete ip; // 在退出之前释放内存
}
在资源分配和释放之间发生了异常,程序也会发生资源泄漏。
分配了资源,而又没有定义析构函数来释放这些资源的类,可能会遇到与使用动态内存相同的错误:
struct destination; // 表示我们正在连接什么
struct connection; // 使用连接所需的信息
connection connect(destination*); // 打开连接
void disconnect(connection); // 关闭给定的连接
void f(destination &d /* 其他参数 */)
{
// 获得一个连接;记住使用完后要关闭它
connection c = connect(&d);
// 使用连接
// 如果在 f 退出前忘记调用 disconnect,就无法关闭 c 了
}
定义一个删除器(deleter)来代替 delete
操作:
void end_connection(connection *p) { disconnect(*p); }
void f(destination &d /* 其他参数 */)
{
connection c = connect(&d);
shared_ptr<connection> p(&c, end_connection);
// 使用连接
// 当 f 退出时(即使是由于异常而退出),connection 会被正确关闭
}
unique_ptr
一个 unique_ptr
“拥有”它所指向的对象。与 shared_ptr
不同,某个时刻只能有一个 unique_ptr
指向一个给定对象。当 unique_ptr
被销毁时,它所指向的对象也被销毁。
unique_ptr 特有的操作
unique_ptr<T> u1
/unique_ptr<T, D> u2
- 空
unique_ptr
,可以指向类型为T
的对象 u1
会使用delete
来释放它的指针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.reset(nullptr)
- 如果提供了内置指针
q
,令u
指向这个对象;否则将u
置为空
- 如果提供了内置指针
weak_ptr
-
weak_ptr
是一种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr
管理的对象 -
将一个
weak_ptr
绑定到一个shared_ptr
不会改变shared_ptr
的引用计数 -
一旦最后一个指向对象的
shared_ptr
被销毁,对象就会被释放 -
即使有
weak_ptr
指向对象,对象也还是会被释放 -
weak_ptr<T> w
- 空
weak_ptr
可以指向类型为T
的对象
- 空
-
weak_ptr<T> w(sp)
- 与
shared_ptr sp
指向相同对象的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
- 如果
动态数组
new 和数组
int *pia = new int[get_size()]; // pia 指向第一个 int
当用 new
分配一个数组时,我们并未得到一个数组类型的对象,而是得到一个数组元素类型的指针。
初始化动态分配对象的数组
int *pia = new int[10]; // 10个未初始化的 int
int *pia2 = new int[10](); // 10个值初始化为0的 int
string *psa = new string[10]; // 10个空 string
string *psa2 = new string[10](); // 10个空 string
// 10个 int 分别用列表中对应的初始化器初始化
int *pia3 = new int[10]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
// 10个 string,前4个用给定的初始化器初始化,剩余的进行值初始化
string *psa3 = new string[10]{"a", "an", "the", string(3, 'x')};
动态分配一个空数组是合法的
char arr[0]; // 错误:不能定义长度为0的数组
char *cp = new char[0]; // 正确:但 cp 不能解引用
当用 new
分配一个大小为0的数组时,new
返回一个合法的非空指针。此指针保证与 new
返回的其他任何指针都不相同。对于零长度的数组来说,此指针就像尾后指针一样,可以像使用尾后迭代器一样使用这个指针。但此指针不能解引用——毕竟它不指向任何元素。
释放动态数组
delete [] pa;
智能指针和动态数组
// up 指向一个包含10个未初始化 int 数组
unique_ptr<int[]> up(new int[10]);
up.release();
指向数组的 unique_ptr
- 指向数组的
unique_ptr
不支持成员访问运算符(点和箭头运算符) unique_ptr<T[]> u
u
可以指向一个动态分配的数组,数组元素类型为T
unique_ptr<T[]> u(p)
u
指向内置指针p
所指向的动态分配的数组p
必须能转换为类型T*
u[i]
- 返回
u
拥有的数组中位置i
处的对象 u
必须指向一个数组
- 返回
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*
的指针,指向一块原始内存- arg 被传递给类型为
T
的构造函数,用来在p
指向的内存中构造一个对象
a.destroy(p)
p
为T*
类型的指针,此算法对p
指向的对象执行析构函数
为了使用 allocate 返回的内存,我们必须用 construct 构造对象。使用未构造的内存,其行为是未定义的。
这些函数在给定目的位置创建元素,而不是由系统分配内存给它们:
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
必须指向足够大的未构造的原始内存,能够容纳给定数量的对象
- 从迭代器