动态内存
1 动态内存和智能指针
在C++中,动态内存的管理是通过一对运算符来完成的:new,在动态内存中为对象分配空间并返回一个指向该对象的指针。delete则是接受一个动态对象的指针,销毁该对象,并释放与之相关联的内存。
动态内存的使用很容易出问题。若忘记释放内存,则会产生内存泄露;若在尚有指针引用内存的情况下就释放内存,则会产生引用非法内存的指针。
因此新标准库提供了两种智能指针类型来管理动态对象:一是shared_ptr
允许多个指针指向一个对象;二是unique_ptr
则“独占”所实现的对象;标准库还定义一个名为weak_ptr
的伴随类,该类是一种弱引用,指向shared_ptr
所管理的对象。这三种类型都定义在memory头文件中。
1.1 shared_ptr类
类似vector,智能指针也是模板。智能指针的使用方式和普通指针类似,解引用一个智能指针返回它指向的对象。
操作 | 描述 |
---|---|
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中的指针必须能转换为T* |
p = q | p和q都是shared_ptr ,所保存的指针必须能相互转换,此操作会递减p的引用计数,递增q的引用计数 |
p.unique() | 若p.use_count()为1,返回true;否则返回false |
p.use_count() | 返回与p共享对象的智能指针数量 |
make_shared
最安全的分配和使用动态内存的方法是调用一个名为make_shared的标准库函数,此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr
。其使用示例如下:
//指向一个值为42的int的shared_ptr
shared_ptr<int> p3 = make_shared<int>(42);
//利用auto来定义 p6指向一个动态分配的空vector<string>
auto p6 = make_shared<vector<string>>();
shared_ptr的拷贝和赋值
当进行拷贝或赋值操作时,每个shared_ptr
都会记录有多少个其他shared_ptr
指向相同的对象,示例如下:
auto p = make_shared<int>(42); //p指向的对象只有p一个引用这
auto q(p); //p和q指向相同的对象,此对象有两个引用者
理解:可以认为每个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类会自动释放动态对象。如以下例子所示:
//factory返回一个shared_ptr,指向一个动态分配的对象
shared_ptr<Foo> factory(T arg)
{
//恰当处理arg
//shared_ptr负责释放内存
//return make_shared<Foo>(arg);
}
//局部变量p在use_factory使用结束时,将被自动销毁
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离开了作用域,其指向的内存会不会被释放掉
因为在最后一个use_factory中的return语句向函数的调用者返回一个p的拷贝,拷贝一个shared_ptr
会增加所管理对象的引用计数值。对于一块内存,shared_ptr
类保证只要有任何shared_ptr
对象引用它,就不会被释放掉。
使用动态生存期的资源的类
程序使用动态内存处于以下三种原因之一:
- 程序不知道自己需要使用多少对象
- 程序不知道所需对象的准确类型
- 程序需要在多个对象间共享数据
1.2 直接管理内存——new和delete
使用new动态分配和初始化对象,可使用直接初始化和列表初始化,来初始化一个动态分配的对象。示例如下:
int *pi = new int(1024); //值初始化
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
动态分配的const对象
用new分配const对象是合法的:
//分配并初始化一个const int
const int *pci = new const int(1024);
//分配并默认初始化一个const的空string
const string *pcs = new const string;
释放动态内存
通过delete表达式来将动态内存归还给系统。传递给delete的指针必须指向动态分配的内存或者是一个空指针,示例如下:
double *pd = new double(33),
delete pd;
使用new和delete管理动态内存存在的问题:
- 忘记delete内存
- 使用已经释放掉的对象
- 同一块内存释放两次
若只使用智能指针,可以避免所有这些问题
delete之后重置指针值
在delete之后,指针就变成了空悬指针,即指向一块曾经保存数据对象但现在已经无效的内存的指针。避免上述问题:在指针即将要离开作用域之前释放掉其所关联的内存
动态内存的一个基本问题是:可能有多个指针指向相同的内存。如下例子:
int *p(new int(42)); //p指向动态内存
auto q = p; //p和q均指向相同的内存
delete p; //p和q均变为无效
p = nullptr; //指出p不再绑定到任何对象
1.3 shared_ptr
和new
结合使用
用new返回的指针来初始化智能指针,同时必须使用直接初始化形式来初始化一个智能指针:
shared_ptr<int> p1(new int(42)); //正确:使用了直接初始化形式
shared_ptr<int> p2 = new int(1024); //错误:必须使用直接初始化形式
操作 | 描述 |
---|---|
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所指向的对象的所有权。p使用可调用对象d来代替delete |
shared_ptr<T> p(p2, d) | p是shared_ptr p2 的拷贝,p将用可调用对象d来代替delete |
p.reset() | 若p是唯一指向其对象的shared_ptr ,reset会释放此对象 |
p.reset(q) | 若传递了可选的参数内置指针q,会令p指向q,否则会将p置为空 |
p.reset(q, d) | 若传递了参数d,将会调用d而不是delete来释放q |
不要混合使用普通指针和智能指针
考虑下面对shared_ptr
进行操作的函数:
void process(shared_ptr<int> ptr)
{
//使用ptr
} //ptr离开作用域,被销毁
由于process的参数是传值方式传递,因此实参会被拷贝到ptr中,而拷贝一个shared_ptr
会递增其引用计数。当局部变量ptr被销毁时,ptr指向的内存不会被释放。使用此函数的正确方法是传递一个shared_ptr:
shared_ptr<int> p(new int(42)); //引用计数为1
process(p); //拷贝p会递增其引用计数;在process中引用计数值为2
int i = *p; //引用计数只为1
不要使用get初始化另一个智能指针或为智能指针赋值
智能指针类型定义的get函数的功能:需要向不能使用智能指针的代码传递一个内置指针,使用get返回的指针的代码不能delete此指针,注意:永远不要用get初始化另一个智能指针或者为另一个智能指针赋值
1.4 智能指针和异常
使用智能指针的一些规范:
- 不使用相同的内置指针值初始化(或reset)多个智能指针
- 不使用get()初始化或reset另一个智能指针
- 若使用智能指针管理的资源部署new分配的内存,一定要传递给它应该删除器
- 使用get()返回的指针,当最后一个对应的智能指针销毁后,指针就变为无效了。
1.5 unique_ptr
与shared不同,某个时刻只能有一个unique_ptr
指向一个给定对象。当unique_ptr
被销毁时,他所实现的对象也被销毁,与shared_ptr
不同,定义unique_ptr
时,需要将其绑定到一个new返回的指针上。初始化unique_ptr
必须采用直接初始化形式,示例如下:
unique_ptr<double> p1; //可以指向一个double的unique_ptr
unique_ptr<int> p2(new int(42)); //p2指向一个值为42的int
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(nullptr)u.reset(q) | 如果提供了内置指针q,令u指向这个对象;否则将u置为空 |
通过上述表格中的操作,虽然不能拷贝或赋值unique_ptr
,但可以调用release或reset将指针的所有从一个(非const)unique_ptr
转移给另外一个unique,示例如下:
//将所有权从p1(指向string stegosaurus)转移给p2
unique_ptr<string> p2(p1.release()); //release将p1置为空
unique_ptr<string> p3(new string("Trex")); //release将p1置为空
//将所有权从p3转移给p2
p2.reset(p3.release()); //reset释放了p2原来指向的内存
在上述程序中,如果不用另外一个智能指针来保存release返回的指针,则就需要程序来释放资源:
p2.release(); //错误,p2不会释放内存,而且会丢失了指针
auto p = p2.release(); //正确,但必须记得delete(p)
传递unique_ptr参数和返回unique_ptr
不能拷贝unique_ptr
的规则有一个例外:可以拷贝或赋值一个将要被销毁的unique_ptr
。最常见的例子是从函数返回一个unique_ptr
:
unique_ptr<int> clone(int p)
{
//正确:从int*创建一个unique_ptr<int>
return unique_ptr<int>(new int(p));
}
//返回一个局部对象的拷贝
unique_ptr<int> clone(int p)
{
unique_ptr<int> ret(new int (p));
//...
return ret;
}
1.6 weak_ptr
是一种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的对象,将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。
操作 | 描述 |
---|---|
weak_ptr<T> w | 空weak_ptr 可以指向类型为T的对象 |
weak_ptr<T> w(sp) | 与shared 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 |
当创建一个weak_ptr
时,要用一个shared_ptr
来初始化它:
auto p = make_shared<int>(42);
weak_ptr<int> wp(p); //wp弱共享p;p的引用计数为改变
wp和p指向相同的对象,由于是弱共享,创建wq不会改变p的引用计数;wp指向的对象可能被释放掉。==由于对象可能不存在,因此不能直接使用weak_ptr
直接访问对象,而必须调用lock,此函数检查weak_ptr
指向的对象是否仍然存在。==调用示例如下:
if (shared_ptr<int> np = wp.lock()) //若np不为空则条件成立
{
//此处np与p共享对象
}
2 动态数组
2.1 new和数组
new分配一个对象数组,在其中指明要分配的对象的数目,示例如下:
int *pia = new int[get_size()];
//pia指向第一个int
方括号中的大小必须是整数,但不必是常量
初始化动态分配对象的数组,示例如下:
int *pia = new int[10]; //10个未初始化的int
int *pia2 = new int[10](); //10个值初始化为0的int
释放动态数组
特殊形式的delete——在指针前加上一个空方括号对:
delete p; //p必须指向一个动态分配的对象或为空
delete [] pa; //pa必须指向一个动态分配的数组或为空
在第二条语句中,销毁pa指向的数组中的元素,是按逆序销毁的,即,最后一个元素首先被销毁,然后是倒数第二个。方括号是必须的
智能指针和动态数组
使用unique_ptr管理动态数组,必须在对象类型后面跟一对空方括号:
//up指向一个包含10个未初始化int的数组
unique_ptr<int[]> up(new int[]);
up.release(); //自动用delete[] 销毁其指针
操作 | 描述 |
---|---|
unique_ptr<T[]> u | u可以指向一个动态分配的数组,数组元素类型为T |
unique_ptr<T[]> u(p) | u指向内置指针p所指向的动态分配的数组,p必须能转换为类型T* |
u[i] | 返回u拥有的数组中位置i处的对象,u必须指向一个数组 |
指向数组的unique_ptr
不支持成员访问运算符(点和箭头运算符)
2.2 allocator类
allocator类定义在头文件memory中,下表介绍了allocator的操作
操作 | 描述 |
---|---|
allocator<T> a | 定义了一个名为a的allocator对象,可以为类型为T的对象分配内存 |
a.allocate(n) | 分配一段原始的,为构造的内存,保存n个类型为T的对象 |
a.deallocate(p, n) | 释放从T*指针中p中地址开始的内存,这块内存保存了n个类型为T的对象;在调用deallocate之前,必须对每个这块内存中创建的对象调用的destroy |
a.construct(p, args) | p必须是一个类型T*的指针,指向一块原始内存;arg 被传递给类型为T的构造函数,用来在p指向的内存中构造一个对象 |
a.destroy(p) | p为T*类型的指针,对p指向的对象执行析构函数 |
拷贝进而填充未初始化内存的算法
算法 | 描述 |
---|---|
uninitialized_copy(b, e, b2) | 从迭代器b和e指出的输入范围拷贝元素到迭代器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个对象 |