第十二章 动态内存
动态内存是通过一对运算符来完成的: new,在动态内存中为对象分配空间并返回一个指向该对象的指针;delete, 接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。
为了更容易(同时也更安全)地使用动态内存,新的标准库提供了两种 智能指针(smart pointer
) 类型对象。智能指针的行为类似常规指针,重要的区别时它负责自动释放所指向的对象。新标准库提供的这两种智能指针的却别在于管理底层指针的方式: shared_ptr 允许多个指针指向同一个对象;unique_ptr 则“独占”所指向的对象。标准库还定义了一个名为 weak_ptr 的伴随类,它是一种弱引用,指向 shared_ptr
所管理的对象。这三种类型都定义在 memory
头文件中。
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 共享对象的智能指针数量;可能很慢,主要用于调试 |
make_shared 函数
// 指向一个值为 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 一个引用者
auto q(p); // p 和 q 指向相同对象,此对象有两个引用者
一旦一个 shared_ptr
计数器变为 0,它就会自动示范给自己所管理的对象
auto r = make_shared<int>(42); // r 指向的 int 只有一个引用者;
r = q; // 给 r 赋值,令它指向另一个地址
// 递增 q 指向的对象的引用计数
// 递减 r 原来指向对象的引用计数
// r 原来指向的对象已经没有引用者,会自动释放
如果将
shared_ptr
存放于一个容器中,而后不再需要全部元素,而只是用其中一部分,要记得用erase
删除不再需要的那些元素
使用动态生存期的资源的类
程序使用动态内存出于以下三种原因之一:
- 程序不知道自己需要使用多少对象
- 程序不知道所需对象的准确类型
- 程序需要在多个对象间共享数据
定义 StrBlob 类
class StrBlob{
public:
typedef std::vector<std::string>::size_type size_type;
StrBlob();
StrBlob(std::initializer_list<std::string> il);
size_type size() const { return data->size(); }
bool empty() const { return data->empty(); }
// 添加和删除元素
void push_back(const std::string& t){ data->push_back(t); }
void pop_back();
// 元素访问
std::string& front();
std::string& back();
private:
std::shared_ptr<std::vector<std::string>> data;
// 如果 data[i] 不合法,抛出一个异常
void check(size_type i, const std::string& msg) const;
};
在此类中实现了 size、empty 和 push_back
成员。这些成员通过指向底层 vector
的 data
成员来完成它们的工作。例如,对一个 StrBlob
对象调用 size()
会调用 data->size()
,依次类推
StrBlob 构造函数
StrBlob::StrBlob():data(make_shared<vector<string>>()) {}
StrBlob::StrBlob(initializer_list<string> il):data(make_shared<vector<string>>(il)){}
元素访问成员函数
void strBlob::check(size_type i, const string& msg) const
{
if(i >= data->size())
throw out_of_range(msg);
}
string& StrBlob::front(){
// 如果 vector 为空,check 会抛出一个异常
check(0, "front on empty StrBlob");
return data->front();
}
// const string& StrBlob::front(){} const 版本重载
string& StrBlob::back(){
// 如果 vector 为空,check 会抛出一个异常
check(0, "back on empty StrBlob");
return data->back();
}
void StrBlob::pop_back(){
check(0, "pop_back on empty StrBlob";
data->pop_back();
}
StrBlob 的拷贝、赋值和销毁
类似 Sales_data
类,StrBlob
使用默认版本的拷贝、赋值和销毁成员函数来对此类型的对象进行这些操作。默认情况下,这些操作拷贝、赋值和销毁类的数据成员。StrBlob
类只有一个数据成员,它是 Shared_ptr
类型。因此,当我们拷贝、赋值或销毁一个 StrBlob
对象时,它的 shared_ptr
成员会被拷贝、赋值或销毁。
拷贝一个 shared_ptr
会递增其引用计数;将一个 shared_ptr
赋予另一个 shared_ptr
会递增复制好右侧 shared_ptr
的引用计数,递减左侧。如果一个 shared_ptr
的引用计数变为 0,它所指的对象会被自动销毁。因此,对于由 StrBlob
构造函数分配的 vector
,当最后一个指向它的 StrBlob
对象被销毁时,它会随之被自动销毁。
使用 new 动态分配和初始化对象
string* ps1 = new string; // 默认初始化为空 string
string* ps = new string(); // 值初始化为空 string
int* pi1 = new int; // 默认初始化; *pi1 的值未定义
int* pi2 = new int(); // 值初始化为 0; *pi2 wei 0
指针值和 delete
int i, *pi1 = &i, *pi2 = nullptr;
double* pd = new double(33), *pd2 = pd;
delete i; // 错误, i 不是一个指针
delete pi1; // 未定义: pi1 指向一个局部变量
delete pid; // 正确
delete pd2; // 未定义, pd2 指向的内存已经被释放了
delete pi2; // 正确: 释放一个空指针总是没有错误的
同一块内存释放两次。当有两个指针指向相同的动态分配对象时,可能发生这种错误。如果对其中一个指针进行了
delete
操作,对象的内存就被归还给自由空间了。如果我们随后又delete
第二个指针,自由空间就可能被破坏
shared_ptr 和 new 结合使用
如果我们不初始化一个智能指针,它就会被初始化为一个空指针。我们还可以用 new
返回的指针来初始化智能指针:
shared_ptr<double> pl; // shared_ptr 可以指向一个 double
shared_ptr<int> p2(new int(42)); // p2 指向一个值为 42 的 int
接受指针参数的智能指针构造函数是 explicit
的。因此,我们不能将一个内置指针隐式地转换为一个智能指针,必须使用直接初始化形式来初始化一个智能指针:
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){
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 会释放此对象。若传递了可选的参数内置指针 p , 会令 p 指向 q ,否则会将 p 置为空。若还传递了参数 d ,将会调用 d 而不是 delete 来释放 q |
不要混合使用普通指针和智能指针
// 在函数被调用时 ptr 被创建并初始化
void process(shared_ptr<int> ptr){
// 使用 ptr
}// ptr 离开作用域,被销毁
使用此函数的正确方法是传递给它一个 shared_ptr
shared_ptr<int> p(new int(42)); // 引用计数为 1
process(p); // 拷贝 p 会递增它的引用计数;在 process 中引用计数为 2
int i = *p; // 正确: 引用计数为 1
虽然不能传递给 process
一个内置指针,但可以传递给它一个( 临时的 ) shared_ptr
,这个 shared_ptr
是用一个内置指针显式构造的。但是,这样做很可能会导致错误
int* x(new int(1024)); // 危险:x 是一个普通指针,不是一个智能指针
process(x); // 错误: 不能将 int* 转换为一个 shared_ptr<int>
process(shared_ptr<int>(x)); // 合法的,但内存会被释放!
int j = *x; // 未定义的: x 是一个空悬指针
在上面的调用中,将一个临时的 shared_ptr
传递给 process
。当这个调用所在的表达式结束时,这个临时对象就被销毁了。销毁这个临时变量会递减引用计数,此时引用计数就变成 0 了。因此,当临时对象被销毁时,它所指向的内存会被释放
但 x
继续指向这个内存,从而变成一个空悬指针。如果试图使用 x 的值,其行为是未定义的。
当将一个 shared_ptr
绑定到一个普通指针时,我们就酱内存的管理责任交给了这个 shared_ptr
。一旦这样做了,就不应该再使用内置指针来访问 shared_ptr
所指向的内存了。
不要使用 get 初始化另一个智能指针或为智能指针赋值
智能指针类型定义了一个名为 get
的函数,它返回一个内置指针,指向智能指针管理的对象。此函数是为了这样一种情况设计的:我们需要想不能使用智能指针的代码传递一个内置指针。使用 get
返回的指针的代码不能 delete
此指针。
编译器不会给出错误信息,但将另一个智能指针也绑定到 get
返回的指针上是错误的:
shared_ptr<int> p(new int(42)); // 引用计数为 1
int* q = p.get(); // 正确: 但使用 q 时要注意,不要让他管理的指针被释放
{// 新程序块
// 未定义: 两个独立的 shared_ptr 指向相同的内存
shared_ptr<int>q(q);
}// 程序块结束,q 被销毁,它指向的内存被释放
int foo = *p; // 未定义:p 指向的内存已经被释放了
本例中, p
和 q
指向相同的内存。由于它们时相互独立创建的,因此各自的引用计数都是 1.当 q
所在的程序块结束时,q
被销毁,这会导致 q
指向的内存被释放。从而 p
变成一个空宣子还真,意味着当我们试图使用 p
时,将会发生未定义行为。而且当 p
被销毁时,这块内存会被第二次 delete
;
其他 shared_ptr 操作
p = new int(1024); // 错误: 不能将一个指针赋予 shared_ptr
p.reset(new int(1024)); // 正确: p 指向一个新对象
与赋值类似,reset
会更新引用计数,如果需要的话,会释放 p
指向的对象。reset
成员常常与 unique
一起使用,来控制多个 shared_ptr
共享的对象。再改变底层对象之前,我们检查自己是否时当前对象仅有的用户。如果不是,在改变之前要制作一份新的拷贝
if( !p.unique() )
p.reset(new string(*p)); // w偶们不是唯一用户;分配新的拷贝
*p += newVal; // 现在我们知道自己时唯一的用户,可以改变对象的值
智能指针和哑类
struct destination; // 表示正在连接什么
struct connection; // 使用连接所需的信息
connection connect(destination*); // 打开连接
void disconnect(connection); // 关闭给定的连接
void f(destination& d /* 其他参数 */){
// 获得一个连接;记住使用后要关闭
connection c = connect(&d);
// 使用连接
// 如果在 f 退出前忘记调用 disconnect, 就无法关闭 c 了
}
如果 connection
有一个析构函数, 就可以在 f
结束时由析构函数自动关闭丽娜姐。但是如果没有,就要使用我们自己的释放操作。
默认情况下,shared_ptr
假定它们指向的是动态内存。因此,当一个 shared_ptr
被销毁时,它默认地对它管理地指针进行 delete
操作。为了用 shared_ptr
来管理一个 connection
,我们必须首先定义一个函数来替代 delete
。这个**删除器(deleter)**函数必须能够完成对 shared_ptr
中保存地指针进行释放地操作。在本例中,我们的删除器必须接受单个类型为 connection*
的参数
void end_connection(connection* p) { disconnection(*p)}
当我们创建一个 shared_ptr
时,可以传递一个( 可选的 )指向删除器函数的参数
void f(destination& d /* 其他参数 */){
connection c = connect(&d);
shared_ptr<connection> p(&c, end_connection);
// 使用连接
// 当 f 退出(即使是由于异常退出), connection 会被正确关闭
}
当 p
被销毁时,他不会对自己保存的指针执行 delete
, 而是调用 end_connection
。接下来 end_connection
会调用 disconnect
,从而确保连接被关闭。如果 f
正常退出,那么 p
的销毁会作为结束处理的一部分。如果发生了异常, p
童谣会被销毁,从而连接被关闭。
unique_ptr
与 shared_ptr
不同,没有类似 make_shared
的标准库函数返回一个 unique_ptr
。当我们定义一个 unique_ptr
时,需要将其绑定到一个 new
返回的指针上。类似 shared_ptr
, 初始化 unique_ptr
必须采用直接初始化形式
unique_ptr<double> p1; // 可以指向一个 double 的 unique_ptr
unique_ptr<int> p2(new int(42)); // p2 指向一个值为 42 的 int
由于一个 unique_ptr
拥有它指向的对象,因此 unique_ptr
不支持普通的拷贝或赋值操作
unique_ptr<string> p1(new string("Stegosaurus"));
unique_ptr<string> p2(p1); // 错误: unique_ptr 不支持拷贝
unique_ptr<string> p3;
p3 = p2; // 错误: 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.reset(q) u.reset(nullptr) | 释放 u 指向的对象; 如果提供了内置指针 q ,令 u 指向这个对象; 否则将 u 置空 |
虽然不可以赋值或拷贝,但是可以通过 release
或 reset
将指针所有权从一个 ( 非 const
) unique_ptr
转移给另一个 unique
:
// 将所有权从 p1 转移给 p2
unique_ptr<string> p2(p1.release()); // release 将 p1 置为空
unique_ptr<string> p3(new string("Trex"));
// 将所有权从 p3 转移到 P2
p2.reset(p3.release()); // reset 释放了 p2 原来指向的内存
传递 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;
}
向后兼容:auto_ptr
标准库较早版本包含了一个名为auto_ptr
的类,它具有unique_ptr
的部分特性,但不是全部。特别是,我们不能在容器中保存auto_ptr
,也不能从函数中返回auto_ptr
虽然auto_ptr
仍然是标准库的一部分,但编写程序时应该使用unique_ptr
向 unique_ptr 传递删除器
unique_ptr
管理删除器的方式和 shared_ptr
不同(16章介绍)
重载一个 unique_ptr
中的删除器会影响到 unique_ptr
类型以及如何构造( 或 reset
)该类型的对象。与重载关联容器的比较操作类似,我们必须在尖括号中 unique_ptr
指向类型之后提供删除器类型。在创建或者 reset
一个这种 unique_ptr
类型的对象时,必须提供一个指定类型的可调用对象( 删除器 )
// p 指向一个类型为 objT 的对象,并使用一个类型为 delT 的对象释放 objT 对象
// 它会调用一个名为 fcn 的 delT 类型对象
unique_ptr<objT, delT> p (new objT, fcn);
用 shared_ptr
的例子
void f(destination& d /* 其他需要的参数 */){
connection c = connect(&d); // 打开连接
// 当 p 被销毁时,连接将会关闭
unique_ptr<connection, decltype(end_connection)*>
p(&c, end_connection);
// 使用连接
// 当 f 退出时( 即使时因为异常退出 ),connection 会被正确关闭
}
本例中使用了 decltype
来指明函数指针类型。由于 decltype(end_connection)
返回一个函数类型,所以我们必须添加一个 *
来指出我们正在使用该类型的一个指针
weak_ptr
weak_ptr
是一种不控制所指向对象生存期的智能指针,它指向由一个 shared_ptr
管理的对象。将一个 weak_ptr
绑定到一个 shared_ptr
不会改变 shared_ptr
的引用计数。一旦最后一个指向对象的 shared_ptr
被销毁,对象就会被释放。即使有 weak_ptr
指向对象,对象还是会被释放,因此,weak_ptr
的名字抓住了这种智能指针“弱”共享对象的特点
写法 | 作用 |
---|---|
weak_ptr<T> w weak_ptr<T> w(sp) | 空 weak_ptr 可以指向类型为 T 的对象;与 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 |
当我们创建一个 weak_ptr
时,要用一个 shared_ptr
来初始化它
auto p = make_shared<int>(42);
weak_ptr<int> wp(p); // wp 弱共享 p; p 的引用计数未改变
由于对象可能不存在,我们不能使用 weak_ptr
直接问对象,而必须调用 lock
。此函数检查 weak_ptr
指向的对象是否存在。若存在,lock
返回一个指向共享对象的 shared_ptr
。与任何其他 shared_ptr
类似,只要此 shared_ptr
存在,它所指的底层对象也就会一直存在
if(shared_ptr<int> np = wp.lock()){ // 如果 np 不为空则条件成立
// 注意这时引用计数器会 + 1
// 在 if 中,np 与 p 共享对象
}
核查指针类
作为 weak_ptr
用途的一个展示,为 StrBlob
类定义一个伴随指针类。命名为 StrBlobPtr
, 会保存一个 weak_ptr
,指向 StrBlob
的 data
成员,这是初始化时提供给它的。通过使用 weak_ptr
,不会影响一个给定的 StrBlob
所指向的 vector
的生存期。但是,可以阻止用户访问一个不再存在的 vector
的企图
// 对于访问一个不存在元素的尝试,StrBlobPtr 抛出一个异常
class StrBlobPtr{
public:
StrBlobPtr():curr(0){}
StrBlobPtr(StrBlob& a, size_t sz = 0):wptr(a.data), curr(sz){}
std::string& deref() const;
StrBlobPtr& incr(); // 前缀递增
private:
// 若检查成功,check 返回一个指向 vector 的 shared_ptr
std::shared_ptr<std::vector<std::string>>
check(std::size_t, const std::string&) const;
// 保存一个 weak_ptr, 意味着底层 vector 可能会被销毁
std::weak_ptr<std::vector<std::string>> wptr;
std::size_t curr; // 在数组中的当前位置
};
默认构造函数生成一个空的 StrBlobPtr
。其构造函数初始化列表将 curr
显示初始化为 0,并将 wptr
隐式初始化为一个空 weak_ptr
。
第二个构造函数接受一个 StrBlob
对象的 shared_ptr
中的 vector
, 并将 curr
初始化为 sz
的值,
值得注意的时,我们不能将 StrBlobPtr
绑定到一个 const StrBlob
对象。这个限制是由于构造函数接受一个非 const StrBlob
对象的引用而导致的
StrBlobPtr
的 check
成员与 StrBlob
中的同名成员不同,它还要检查指针指向的 vector
是否还存在
std::shared_ptr<std::vector<std::string>>
StrBlobPtr::check(std::size_t i, const std::string& msg) const{
auto ret = wptr.lock(); // vector 还存在嘛
if( !ret )
throw std::runtime_error("unbound StrBlobPtr");
if(i >= ret->size())
throw std::out_of_range(msg);
return ret;
}
指针操作
deref
成员调用 check
, 检查使用 vector
是否安全以及 curr
是否在合法范围内
std::string& StrBlobPtr::deref() const{
auto p = check(curr, "dereference past end");
return (*p)[curr]; // (*p) 是对象所指的 vector
}
如果 check
成功,p
就是一个 shared_ptr
,指向 StrBlobPtr
所指向的 vector
。
incr
成员也调用 check
// 前缀递增:返回递增后的对象的引用
StrBlobPtr& StrBlobPtr::incr(){
// 如果 curr 已经指向容器的尾后位置,就不能递增它
check(curr, "increment past end of StrBlobPtr");
++cur; // 推进当前位置
return *this;
}
当然,为了访问 data
成员,指针类必须声明为 StrBlob
的 friend
。还要为 StrBlob
类定义 begin
和 end
操作,返回一个指向它自身的 StrBlobPtr
// 对于 StrBlobPtr 中的友元声明来说,此前置声明式必要的
class StrBlobPtr;
class StrBlob{
friend class StrBlobPtr;
// ...
// 返回指向首元素和尾后元素的 StrBlobPtr
StrBlobPtr begin() { return StrBlobPtr(*this) };
StrBlobPtr end(){
auto ret = StrBlobPtr(*this, data->size());
return ret;
}
};
分配一个数组会得到一个元素类型的指针
虽然通常称 new T[]
分配的内存为 “动态数组”,但实际上用 new
分配一个数组时,我们并未得到一个数组类型的对象,而是得到一个数组元素类型的指针。
由于分配的内存并不是一个数组类型,因此不能对动态数组调用 begin
或 end
。这些函数使用数组维度。由于相同的原因,也不能用范围 for
语句来处理所谓的动态数组中的元素
// 初始化动态分配对象的数组
int* pia = new int[10]; // 10 个未初始化的 int
int* pia2 = new int[10](); // 10 个值初始化为 0 的 int
string* psa = new string[10]; // 10 个空 string
string* psa = new string[](); // 10 个空 string
int* pia3 = new int[10]{0,1,2,3,4,5,6,7,8,9};
string* psa2 = new string[10]{"a", "an", "the", string(3, 'x')};
动态分配一个空数组合法
char arr[0]; // 错误: 不能定义长度为 0 的数组
char* cp = new char[0]; // 正确: 但 cp 不能解引用
当我们用 new
分配一个大小为 0 的数组时,new 返回一个合法的非空指针。此指针保证与 new
返回的其他任何指针都不相同。对于零长度的数组来说,此指针就像尾后指针一样,可以像使用尾后迭代器一样使用这个指针。
智能指针和动态数组
标准库提供了一个可以管理 new
分配的数组的 unique_ptr
版本。为了用一个 unique_ptr
管理动态数组,我们必须在对象类型后面跟一对空方括号:
// up 指向一个包含 10 个未初始化 int 的数组
unique_ptr<int[]> up(new int[10]);
up.release(); // 自动用 delete[] 销毁其指针
当一个 unique_ptr
指向一个数组时,我们不能使用点和箭头成员运算符。毕竟 unique_ptr
指向的是一个数组而不是单个对象,因此这些运算符时无意义的。另一方面,当一个 unique_ptr
指向一个数组时,我们可以用下标运算符来访问数组中的元素
for(size_t i = 0; i != 10; ++i)
up[i] = i; // 为每个元素赋予一个新值
指向数组的
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 需要删除器
// 为了使用 shared_ptr ,必须提供一个删除器
shared_ptr<int> sp(new int[10], [](int* p){delete[] p;});
sp.reset(); // 使用提供的 lambda 释放数组,它使用 delete[]
allocator 类
allocator
是一个模板。
allocator<string> alloc; // 可以分配 string 的 allocator 对象
auto const p = alloc.allocate(n); // 分配 n 个未初始化的 string
算法 | 作用 |
---|---|
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 指向的对象执行析构函数 |
// auto q = p; // q 指向最后构造的元素之后的位置
alloc.construct(q++); // *q 为空字符串
alloc.construct(q++, 10, 'c'); // *q 为 cccccccccc
alloc.construct(q++, "hi"); // *q 为 hi
cout << *q << endl; // 正确:使用 string 的输出运算符
cout << *p << endl; // 灾难:q 指向为构造的内存
当使用完对象后,必须对每个构造的元素调用 destroy
来销毁。destroy
接受一个指针,对指向的对象执行析构函数
while(q != p)
alloc.destroy(--q);
// 不再使用就将内存归还给操作系统
alloc.deallocate(p, n);