静态内存、栈内存和堆
我们前面只提到了静态内存或栈内存:
- 静态内存:用来保存局部
static
、类static
数据成员和定义在任何函数之外的变量 - 栈内存:保存定义在函数内的非
static
对象
分配在静态内存或者栈内存的对象由编译器自动创建和销毁。对于栈对象仅在其定义的程序块运行时才存在,static
对象在使用之前分配,在程序结束时销毁。
每个程序还拥有一个内存池(被称为自由空间free store
或堆heap
)。程序用堆来存储动态分配的对象,当动态对象不再使用时,我们的代码必须显式销毁它们。
动态内存和智能指针
在c++
中,动态内存的管理是通过一对运算符来完成的:
-
new
:在动态内存中为对象分配空间并返回一个指向该对象的指针 -
delete
:接收一个动态对象的指针,销毁该对象并释放与之关联的内存空间
这种管理方式有两个问题:
- 如果我们忘记释放内存,就会造成内存泄漏
- 如果在有指针引用内存的情况下我们就释放它,就会出现“野指针”
新标准库提供了三种智能指针smart pointer
:
-
shared_ptr
:允许多个指针指向同个对象 -
unique_ptr
:“独占”所指向的对象 -
weak_ptr
:弱引用,指向shared_ptr
所管理的对象
1. shared_ptr类
shared_ptr
和unique_ptr
都支持的操作:
-
shared_ptr
和sp unique_ptr
:空智能指针,可以指向类型为up 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
:返回一个(args) shared_ptr
,指向一个动态分配的类型为T
的对象,使用args
初始化该对象 -
shared_ptr
: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
-
p.use_count()
:返回和p
共享对象的智能指针数量,可能很慢,主要用于调试
1.1 make_shared函数
// p4指向一个值为"9999999999"的string
shared_ptr p4 = make_shared(10, '9');
// p5指向一个值初始化的int, 即值为0
shared_ptr p5 = make_shared();
类似于顺序容器的emplace
成员,make_shared
用其参数来构造给定类型的对象。即调用make_shared
传递的参数必须与string
某个构造函数相匹配。如果我们不传递任何参数,那么就进行值初始化。
1.2 shared_ptr的拷贝和赋值
当进行拷贝或者赋值操作时,每个shared_ptr
都会记录有多少个其他shared_ptr
指向相同的对象:
auto p = make_shared(42); // p指向的对象只有p一个引用者
auto q(p); // p和q指向同一对象,此对象有两个引用者
每个shared_ptr
都有一个与之关联的引用计数器reference count
,无论何时我们拷贝一个shared_ptr
,计数器都会递增:
- 用一个
shared_ptr
初始化另一个shared_ptr
- 将
shared_ptr
作为参数传递给一个函数 - 作为函数的返回值
当我们给一个shared_ptr
赋予一个新值,或者shared_ptr
被销毁(比如一个局部的shared_ptr
离开其作用域)时计数器就会递减。
一旦一个shared_ptr
的计数器变成0
,它就会自动释放所管理的对象:
auto r = make_shared(42); // r指向的int只有一个引用者
r = q; // 给r赋值,令它指向另一地址
// 递增q指向的对象的引用计数
// 递减r原来指向对象的引用计数
// r原来指向的对象已经没有引用者, 会自动释放
1.3 shared_ptr自动销毁所管理的对象
当指向一个对象的最后一个shared_ptr
被销毁时,shared_ptr
通过对象的一个特殊成员函数——析构函数destructor
完成销毁工作的。析构函数一般用于释放对象所分配的资源。
shared_ptr use_factory(T arg)
{
shared_ptr p = factory(arg);
// 使用p
return p; // 当我们返回p时,引用计数执行了递增操作
} // p离开了作用域,但它指向的内存不会被释放掉
由于在最后一个shared_ptr
销毁前内存都不会释放,保证shared_ptr
在无用之后不再保留就很有必要。有一种常见的例子是:
你将
shared_ptr
存放在一个容器之中,随后重排了容器不再需要某些元素。在这种情况下,你应该使用erase
删除那些不再需要的shared_ptr
元素。
1.4 使用了动态生存期的资源的类
程序使用动态内存出于以下三种原因:
- 程序不知道自己需要使用多少对象
- 程序不知道所需对象的准确类型
- 程序需要在多个对象之间共享数据
容器类是出于第一种原因使用的动态内存的典型例子,第十五章的面向对象程序设计会介绍出于第二种原因使用动态内存的例子。本节中我们定义一个类
StrBlob
类,它使用动态内存主要是为了让多个对象能共享相同的底层数据。
1.5 定义StrBlob类
我们将使用vector
在StrBlob
中保存元素,但是如果我们在一个StrBlob
对象中直接保存vetor
,那么对象销毁时对应的成员也会销毁。比如b1
和b2
是两个StrBlob
对象,如果此vector
保存在b2
中,那么当b2
离开作用域时此vector
也会被销毁。为了保证此vector
中的元素继续存在,我们将vector
保存在动态内存中。
class StrBlob {
public:
typedef std::vector::size_type size_type;
StrBlob();
StrBlob(std::initializer_list 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> data;
// 如果data[i]不合法,抛出一个异常
void check(size_type i, const std::string &msg) const;
}
// 构造函数:两个都使用初始化列表来初始化data成员, 令它指向一个动态分配的vector
StrBlob::StrBlob() : data(make_shared>()) { }
StrBlob::StrBlob(initializer_list il) :
data(make_shared>(il)) { }
pop_back
、front
和back
在试图访问元素之前都必须确保元素存在,我们为StrBlob
定义了一个名为check
的private
工具函数,用于确定索引是否在合法范围内。
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();
}
string& StrBlob::back()
{
check(0, "back on empty StrBlob");
data->pop_back();
}
1.6 StrBlob的拷贝、赋值和销毁
StrBolb
使用默认版本的拷贝、赋值和销毁成员函数来对此类型的对象进行这些操作。因为StrBolb
只有一个shared_ptr
数据成员,因此当我们拷贝、赋值或销毁一个StrBlob
对象时,它的shared_ptr
成员会被拷贝、赋值或销毁。
拷贝一个
shared_ptr
会递增其引用计数,将一个shared_ptr
赋予另一个shared_ptr
会递增赋值号右侧的shared_ptr
的引用计数,递减左侧sahred_ptr
的引用计数。当一个shared_ptr
的引用计数变为0
之后它指向的对象会被自动销毁。
2. 直接管理内存
2.1 使用new动态分配和初始化对象
需要注意如下几点:
- 动态分配的对象执行默认初始化:内置类型或组合类型的对象的值是未定义的,而类类型对象的值用默认构造函数进行初始化
- 值初始化的内置类型对象有着良好定义的值,但是默认初始化的对象的值是未定义的
- 一个动态分配的
const
对象必须初始化,对于定义了默认构造函数的类类型可以隐式初始化,但是其他类型的对象必须显式初始化;由于分配的对象是const
的,new
返回的指针是一个指向const
对象的指针 - 如果程序用光内存,那么
new
就会抛出一个类型为bad_alloc
的异常,我们可以通过int *pi2 = new (nothrow) int;
来使它在分配失败时返回一个空指针
初始化相关:
// 默认初始化
int *pi = new int; // pi指向一个未初始化的int
string *ps = new string; // 初始化为空string
// 直接初始化
int *pi = new int(1024);
string *ps = new string(10, '9');
vector *pv = new vector{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
// 值初始化
string *ps1 = new string; // 默认初始化为空string
string *ps = new string(); // 值初始化为空string
int *pi1 = new int; // 默认初始化, 值未定义
int *pi2 = new int(); // 值初始化为0
动态分配const
对象:
// 分配并初始化一个const int
const int *pci = new const int(1024);
// 分配并默认初始化一个const的空string
const string *pcs = new const string;
2.2 释放动态内存
我们可以通过delete
将动态内存归还给系统,执行了两个动作:
- 销毁给定的指针指向的对象
- 释放对应的内存
delete p; // p必须指向一个动态分配的对象或者是一个空指针
释放一块非
new
分配的内存或者将相同的指针释放多次,其行为是未定义的。
动态对象的生存期直到被释放为止,对于一个由内置指针管理的动态对象,直到被显式释放之前它都是存在的。返回指向动态内存指针的函数给其调用者增加了一个额外负担——调用者必须记得释放内存:
// factory返回一个指针,指向一个动态分配的对象
Foo* factory(T arg)
{
// 处理arg
return new Foo(arg); // 调用者负责释放此内存
}
void use_factory(T arg)
{
Foo *p = factory(arg);
// 使用p但不delete它
} // p离开了它的作用域,但是它所指向的内存还没有被释放!
两种处理方法,方法一就是在use_factory
函数内记得释放内存:
void use_factory(T arg)
{
Foo *p = factory(arg);
// 使用p
delete p;
}
方法二是系统中其他代码可能要使用use_factory
所分配的对象,我们应该修改此函数让他返回一个指针:
void use_factory(T arg)
{
Foo *p = factory(arg);
// 使用p
return p; // 交给use_factory函数的调用者释放内存
}
使用new
和delete
管理内存存在三个最常见的问题:
- 忘记
delete
内存:这种情况下内存再也不可能归还给自由空间,也就是我们所说的“内存泄漏”问题 - 使用已经释放掉的对象:通过在释放内存后将指针置为空,有时可以检测出这种问题
- 同一块内存释放两次:当有两个指针指向相同的动态分配对象时可能发生这种错误,第一次
delete
时对象的内存就被归还给自由空间了,第二次delete
可能破坏自由空间
坚持只使用智能指针,就可以避免上述的所有问题。
delete
一个指针后指针值就变为无效了,但是很多机器上指针仍然保存着(已经释放了的)动态内存的地址。在delete
之后指针就变成了“空悬指针”dangling pointer
,即指向一块曾经保存数据对象但是现在已经失效的内存的指针。有一种可以避免空悬指针的做法:在指针即将离开其作用域之前释放掉它所关联的内存,这样在指针关联的内存被释放掉之后,就没有任何机会继续使用指针了。另一种做法是在delete
之后将指针赋值为nullptr
,这样可以清楚地指出指针不指向任何对象。然而可能有多个指针指向同一块内存:
int *p(new int(42)); // p指向动态内存
auto p = q; // p和q指向同一块内存
delete p; // p和q均变得无效
p = nullptr; // 指出p不再绑定任何对象
// 这时候q变成“空悬指针”,查找指向相同内存的所有指针是异常困难的
2.3 shared_ptr和new结合使用
如果我们不初始化一个智能指针,它就会被初始化为一个空指针。我们还可以用new
返回的指针来初始化智能指针:
shared_ptr p1; // shared_ptr可以指向一个double
shared_ptr p2(new int(42)); // p2指向一个值为42的int
需要注意的是接收指针参数的智能指针构造函数是explicit
的,因此我们不能将一个内置指针隐式转换为一个智能指针,必须使用直接初始化形式来初始化一个智能指针:
shared_ptr p1 = new int(1024); // 错误:必须使用直接初始化形式
shared_ptr p2(new int(1024)); // 正确:使用了直接初始化形式
一个用于初始化智能指针的普通指针必须指向动态内存,因为智能指针默认使用
delete
释放它所关联的对象。
定义和改变shared_ptr
的其他方法:
-
shared_ptr
:p(q) p
管理内置指针q
所指向的对象,q
必须指向new
分配的内存,且能够转化为T*
类型 -
shared_ptr
:p(u) p
从unique_ptr u
那里接管了对象的所有权,将u
置为空 -
shared_ptr
:p(q, d) p
接管了内置指针q
所指向的对象的所有权,q
必须能转换为T*
类型。p
将使用可调用对象d
来代替delete
-
shared_ptr
: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
的函数,它返回一个内置指针,指向智能指针管理的对象。此函数是为了这样一种情况而设计的:我们需要向不能使用智能指针的代码传递一个内置指针。使用get
返回的指针的代码不能delete
指针。
虽然编译器不会报错,但是将另一个智能指针也绑定到
get
返回的指针上是错误的。首先你只有在确定代码不会delete
指针的情况下才能使用get
;另外不要用get
初始化另一个智能指针或者为另一个智能指针赋值。
shared_ptr p(new int(42)); // 引用计数为1
int *q = p.get(); // 正确:但使用q时要注意不要让它管理的指针被释放
{
// 未定义:两个独立的shared_ptr指向相同的内存
shared_ptr(q);
} // 程序块结束, q被销毁, 它指向的内存被释放
int foo = *p; // 未定义:p指向的内存已经被释放了
我们不能将一个指针赋予shared_ptr
,但是我们可以通过reset
将一个新的指针赋予shared_ptr
:
p = new int(1024); // 错误:不能将一个指针赋予shared_ptr
p.reset(new int(1024)); // 正确:p指向一个新对象
reset
和unique
经常一起使用,来控制多个shared_ptr
共享的对象。在改变底层对象之前,我们得先检查一下自己是不是当前对象仅有的用户。如果不是,在改变之前需要制作一份新的拷贝:
if (!p.unique())
p.reset(new string(*p)); // 我们不是唯一用户;分配新的拷贝
*p += newVal; // 现在我们知道自己是唯一的用户,可以改变用户的值
2.4 智能指针和异常
为了确保使用异常处理的程序能在异常发生后资源能被正确地释放,一个简单的确保资源被释放的方法是使用智能指针。如果使用智能指针,即使程序块过早结束,智能指针类也能确保在内存不再需要时将其释放:
void f()
{
shared_ptr sp(new int(42)); // 分配一个新对象
// 这段代码抛出来一个异常,且在f中未被捕获
} // 在函数结束时shared_ptr自动释放内存
2.5 智能指针和哑类
包括所有标准库在内的很多C++
类都定义了析构函数负责清理对象使用的资源。但是不是所有的类都是这么良好定义的,特别是为C
和C++
两种语言设计的类,通常都要求用户手动释放所用的任何资源。与管理动态内存类似,我们可以使用类似的技术来管理不具有良好定义的析构函数。例如我们正在使用一个C
和C++
都使用的网络库,我们通过disconnect
来显示释放:
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
操作。我们可以使用一个函数来代替delete
:
void end_connection(connection *p) { disconnect(*p); }
void f(destination &d /* 其他参数 */)
{
connection c = connect(&d);
shared_ptr p(&c, end_connection);
// 使用连接
// 当f退出时(即使是异常退出), connection也能被正确关闭
}
2.6 智能指针使用注意事项
为了正确使用智能指针,我们需要遵守一些规范:
- 不使用相同的内置指针值初始化或
reset
多个智能指针 - 不
delete get()
返回的指针 - 不使用
get()
初始化或reset
另一个智能指针 - 如果你使用
get()
返回的指针,记住当最后一个对应的智能指针销毁后,那你的指针就无效了 - 如果你使用智能指针管理的资源不是
new
分配的内存,记住传递给它一个删除器
2.7 unique_ptr
一个unique_ptr
拥有它所指向的对象,某个时刻只能有一个unique_ptr
指向一个给定对象。当unique_ptr
被销毁时,它所指向的对象也被销毁。当我们定义一个unique_ptr
时,需要将其绑定到一个new
返回的指针上:
unique_ptr p2(new int(42));
由于unique_ptr
拥有它指向的对象,因此不接受普通的拷贝和赋值:
unique_ptr p1(new string("Stegisaurus"));
unique_ptr p2(p1); // 不支持拷贝
unique_ptr p3;
p3 = p1; // 不支持赋值
unique_ptr
支持的操作包括:
-
unique_ptr
:空u1 unique_ptr
,可以指向类型为T
的对象,u1
会调用delete
来释放指针 -
unique_ptr
:同上,但是会调用u2 D
的可调用对象来释放它的指针 -
unique_ptr
:空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
置为空
虽然我们不能拷贝或者赋值unique_ptr
,但可以通过调用release
或reset
将指针的所有权从一个(非const
)unique_ptr
转移给另一个unique_ptr
:
// 将所有权从p1转移给p2
unique_ptr p2(p1.release()); // release将p1置为空
unique_ptr p3(new string("Trex");
// 将所有权从p3转移给p2
p2.reset(p3.release()); // reset释放了p2原来指向的内存
release
成员返回unique_ptr
当前保存的指针并将其置为空,因此p2
被初始化为p1
原来保存的指针而p1
被置为空。
reset
接收一个可选的指针参数,令unique_ptr
重新指向给定的指针,如果unique_ptr
不为空,它原来指向的对象被释放。
需要注意的是调用release
会切断unique_ptr
和它元拿来管理的对象之间的联系。release
返回的指针通常被用来初始化另一个智能指针或给另一个智能指针赋值。如果我们不用另一个智能指针来保存release
返回的指针,我们就要负责资源的释放:
p2.release(); // 错误:p2不会释放内存,而且我们丢失了指针
auto p = p2.release(); // 正确,但是我们必须记得delete(p)
前面我们提到不能拷贝或者赋值一个unique_ptr
,但是有一个例外:我们可以拷贝或者赋值一个将要被销毁的unique_ptr
,最常见的就是从函数返回unique_ptr
。
// 下面两段代码编译器都知道要返回的对象即将被销毁,因此会执行特殊的“拷贝”
unique_ptr clone(int p) {
// 正确:从int* 创建一个unique_ptr
return unique_ptr(new int(p));
}
// 返回一个局部对象的拷贝
unique_ptr clone(int p) {
unique_ptr ret(new int (p));
// ...
return ret;
}
类似于shared_ptr
,unique_ptr
默认情况下使用delete
释放它指向的对象,我们重载一个删除器,但是unique_ptr
管理删除器的方式和shared_ptr
不同,我们将在十六章介绍。重载一个unique_ptr
中的删除器会影响到unique_ptr
类型一级如何构造(或reset
)该类型的对象。与重载关联容器的比较操作类似,在创建或reset
一个unique_ptr
对象时必须提供一个指定类型的可调用对象作为删除器:
// p指向一个ObjT类型对象,并使用一个delT类型的对象来释放objT对象
// 它会调用一个名为fcn的delT类型对象
unique_ptr p (new objT, fcn);
重写一下前面的连接程序,用unique_ptr
来代替shared_ptr
:
void f(destination &d /* 其他需要的参数 */)
{
connection c = connect(&d); // 打开连接
// 当p被销毁时,连接将被关闭
unique_ptr
p(&c, end_connection);
// 使用连接
// 当f退出时(即使是异常退出),connection也会被正确关闭
}
2.8 weak_ptr
weak_ptr
是一种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr
管理的对象。将一个weak_ptr
绑定到一个绑定到一个shared_ptr
不会改变shared_ptr
的引用计数。一旦最后一个指向对象的shared_ptr
被销毁,对象就会被释放。即使有weak_ptr
指向对象,对象也还是会被释放。
weak_ptr
的操作包括:
-
weak_ptr
:空w weak_ptr
可以指向类型为T
的对象 -
weak_ptr
:与w(sp) shared_ptr sp
指向相同对象的weak_ptr
,赋值后w
与p
共享对象 -
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
-
w.lock()
:如果expired
为true
,返回一个空shared_ptr
,否则返回指向w
对象的shared_ptr
我们创建一个weak_ptr
是时需要用一个shared_ptr
来初始化它。另外由于对象可能不存在,所以我们不能直接使用weak_ptr
直接访问对象,而必须直接调用lock
:此函数会检查weak_ptr
指向的对象是不是仍存在:
autp p = make_share(42);
weak_ptr wp(p); // wp弱共享p; p的引用计数为0
if (shared_ptr np = wp.lock()) { // 如果np不为空则条件成立
// 在if中, np和p共享对象
}
动态数组
new
和delete
运算符一次分配/释放一个对象,但某些应用需要我们一次为很多对象分配内存。当一个应用需要可变数量的对象时,我们更推荐使用vector
或其他标准库容器。
大多数应用应该使用标准库而不是动态分配的数组。使用容器更为简单,更不容易出现内存管理错误并且可能有更好的性能。
使用容器的类可以使用默认版本的拷贝、赋值和析构操作。分配动态数组的类则必须定义自己版本的操作,在拷贝、复制以及销毁对象时管理所关联的内存。
1. new和数组
new
分配要求数量的对象,并在分配成功后返回指向第一个对象的指针:
// 调用get_size确定分配多少个int
int *pia = new int[get_size()]; // pia指向第一个int
// 另一种写法
typedef int arrT[42]; // arrT表示42个int的数组类型
int *p = new arrT; // 分配一个42个int的数组; p指向第一个int
虽然我们通常称new T[]
分配的内存为“动态数组”,当用new
分配一个数组时,我们并未得到一个数组类型的对象,而是得到一个数组元素类型的指针。
由于分配的内存并不是一个数组类型,因此不能对动态数组调用
begin
或end
,也不能用范围for
语句来处理动态数组中的元素。
默认情况下,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个用给定的初始化器初始化,剩余的进行值初始化
// 如果初始化器数目大于元素数目,则new表达式失败不会分配任何内存
string *psa3 = new string[10]{"a", "an", "the", string(3, 'x')};
动态分配一个空数组是合法的,当我们用new
分配一个大小为0
的数组时,new
返回一个合法的非空指针,此指针保证与new
返回的其他任何指针都不相同。对于零长度的数组来说,此指针就像尾后指针一样。但这个指针不能解引用,因为它不指向任何元素。
char arr[0]; // 错误:不能定义长度为0的数组
char *cp = new char[0]; // 正确:但cp不能解引用
2. 释放动态数组
delete [] pa; // pa必须指向一个动态分配的数组或为空
该语句会销毁pa
指向的数组中的元素并释放对应的内存。数组中的元素按逆序销毁,即最后一个元素首先销毁,以此类推。
如果我们在
delete
一个指向数组的指针时忽略了方括号,或者在delete
一个指向单一对象的指针时使用了方括号,其行为是未定义的。
3. 智能指针和动态数组
标准库提供了一个可以管理new
分配的数组的unique_ptr
版本:
// up指向一个包含10个未初始化int的数组
unique_ptr up(new int[10]);
up.release(); // 自动用delete[]销毁其指针
for (size_t i = 0; i != 10; ++i)
up[i] = i; // 为每个元素赋予一个值
指向数组的unique_ptr
支持的操作:
-
unique_ptr
:u u
可以指向一个动态分配的数组,数组元素类型为T
-
unique_ptr
:u(p) u
指向内置类型p
所指向的动态分配的数组,p
必须能转换成类型T*
-
u[i]
:返回第i
个对象
shared_ptr
不支持直接管理动态数组,必须提供自己定义的删除器:
shared_ptr sp(new int[10], [](int *p) { delete[] p; });
sp.reset(); // 使用我们提供的lambda释放数组,它使用delete[]
// shared_ptr未定义下标运算符,并且不支持指针的算术运算
for (size_t i = 0; i != 10; ++i) {
*(sp.get() + i) = i;
}
allocator类
new
有一些灵活性的限制,其中一方面表现在它将内存分配和对象构造组合在一起,同样delete
将对象析构和内存释放组合在一起。当我们分配单个对象时是有必要的,因为我们几乎肯定知道对象应该有什么值。当分配大块内存时,我们通常计划在这块内存上按需构造对象,因此我们希望将内存分配和对象构造分离。
这意味着我们可以分配大块内存,而只有在真正需要时才真正执行对象创建操作(付出一定开销)。
之所以有这个需求,是因为一般情况下将内存分配和对象构造组合在一起可能会导致不必要的浪费:
string *const p = new string[n]; // 构造n个空string
string s;
string *q = p; // q指向第一个p
while (cin >> s && q != p + n)
*q++ = s; // 赋予*q一个新值
const size_t size = q - p;
// 使用数组
delete []p; // p指向一个数组, 记得用delete[]释放
在上面这个例子中,new
表达式分配并初始化了n
个string
。一方面我们可能不需要n
个string
,因此我们可能创建了一些永远也用不到的对象。另一方面,对于那些确实要使用的对象,我们也在初始化之后立即赋予了它们新值,这样每个使用到的元素都被赋值了两次。
1. allocator类
该类提供一种类型感知的内存分配算法,它分配的内存是原始的、未构造的。当一个allocator
对象分配内存时,它会根据给定的对象类型来确定恰当的内存大小和对齐位置:
allocator alloc; // 可以分配string的allocator对象
auto const p = alloc.allocate(n); // 分配n个未经初始化的string
allocator
支持的操作包括:
-
allocator
:定义了一个名为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.destory(p)
:p
为类型T*
的指针,此算法对p
指向的对象执行析构函数
为了使用allocate
返回的内存,我们必须用construct
构造对象,使用未构造的内存,其行为是未定义的。当我们使用完对象之后,必须对每个构造的元素使用destroy
来销毁它们。
一旦元素被销毁后,就可以重新使用这部分内存来保存其他
string
,也可以将其归还给系统。释放内存通过deallocate
来完成。
2. 拷贝和填充未初始化内存的算法
allocator
算法包括:
-
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
个对象,b
必须指向足够大的未构造的原始内存,能够容纳给定数量的对象
举个例子,我们希望把一个int
的vecotr
中的元素拷贝到一个动态数组中,并且这个动态数组的长度是它的两倍,剩下的元素用一个给定值填充:
// 分配比vi元素所占空间大一倍的动态内存
auto p = alloc.allocate(vi.size() * 2);
// 通过拷贝vi中的元素来构造从p开始的元素
auto q = uninitialized_copy(
vi.begin(), vi.end(), p);
// 将剩余元素初始化为42
uninitialized_fill_n(q, vi.size(), 42);