文章目录
- 动态内存
- 12.1动态内存和智能指针
- 12.1.1shared_ptr类
- make_shared函数
- shared_ptr的拷贝和赋值
- shared_ptr自动销毁所管理的对象
- shared_ptr还自动释放相关联的内存
- 使用了动态生存期的资源的类
- 12.1.2直接管理内存
- 使用new动态分配和初始化对象
- 动态分配的const对象
- 内存耗尽
- 指针值和delete
- 动态对象的生存期直到被释放为止
- delete之后重置指针值
- 这只是提供了有限的保护
- 12.1.3shared_ptr和new结合使用
- 不要混合使用普通指针和智能指针
- 也不要使用get初始化另一个智能指针或为智能指针赋值
- 其他shared_ptr操作
- 12.1.4智能指针和异常
- 智能指针和哑类
- 使用自己的释放操作
- 12.1.5unique_ptr
- 传递unique_ptr参数和返回unique_ptr
- 向unique_ptr传递删除器
- 12.1.6weak_ptr
- 核查指针类
- 12.2动态数组
- 12.2.1new和数组
- 分配一个数组会得到一个元素类型的指针
- 初始化动态分配对象的数组
- 动态分配一个空数组是合法的
- 释放动态数组
- 智能指针和动态数组
- 12.2.2allocator类
- allocator类
- allocator分配未构造的内存
- 拷贝和填充未初始化内存的算法
动态内存
每个程序拥有一个内存池,被称作自由空间(free store)或堆(heap)。程序用堆来存储动态分配的对象,即,那些在程序运行时分配的对象。动态对象的生存期由程序来控制,也就是说,当动态对象不再使用时,代码必须显式地销毁它们。
12.1动态内存和智能指针
为了更容易(同时也更安全)地使用动态内存,新的标准库提供了两种智能指针类型来管理动态对象。智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。新标准库提供的这两种智能指针的区别在于管理底层指针的方式:
shared_ptr
:允许多个指针指向同一个对象。unique_ptr
:独占所指向的对象。weak_ptr
:伴随类,它是一种弱引用,指向shared_ptr
所管理的对象。都定义在
memory
头文件中。
12.1.1shared_ptr类
// 智能指针也是模板
shared_ptr<string> p1; // 可以指向string
shared_ptr<list<int>> p2; // 可以指向int的list
// 智能指针的使用方式与普通指针类似
if (p1 && p1->empty()) {
// 如果p1不为空,检查它是否指向一个空string
*p1 = "hi";
}
默认初始化的智能指针中保存着一个空指针。
make_shared函数
最安全的分配和使用动态内存的方法是调用一个名为
make_shared
的标准库函数。此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr
。
// 指向一个值为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的结果,这种方式较为简单:
auto p6 = make_shared<vector<string>>();
make_shared
使用其参数来构造给定类型的对象。如果不传递任何参数,对象就会进行值初始化。
shared_ptr的拷贝和赋值
当进行拷贝或赋值操作时,每个
shared_ptr
都会记录有多少个其他shared_ptr
指向相同的对象。可以认为每个shared_ptr
都有一个关联的计数器,通常称其为引用计数。
无论何时,拷贝一个shared_ptr
都会递增计数器。例如,用一个shared_ptr
初始化另一个shared_ptr
,或将它作为参数传递给一个函数以及作为函数的返回值。当给shared_ptr
赋予一个新值或是shared_ptr
被销毁(例如一个局部的shared_ptr
离开其作用域)时,计数器就会递减。
一旦一个shared_ptr
的计数器变为0,它就会自动释放自己所管理的对象:
// r指向的int只有一个引用者
auto r = make_shared<int>(42);
// 给r赋值,令它指向另一个地址。
// 递增q指向的对象的引用计数,递减r原来指向的对象的引用计数,
// r原来指向的对象已没有引用者,会自动释放。
r = q;
shared_ptr自动销毁所管理的对象
通过特殊的成员函数,即析构函数完成销毁工作。
shared_ptr还自动释放相关联的内存
当动态对象不再被使用时,
shared_ptr
类会自动释放动态对象,这一特性使得动态内存的使用变得非常容易。
// factory返回一个shared_ptr,指向一个动态分配的对象。
shared_ptr<Foo> factory(T arg) {
// 恰当地处理arg
// shared_ptr负责释放内存
return make_shared<Foo>(arg);
}
// 由于factory返回一个shared_ptr,所以可以确保它分配的对象会在恰当的时刻被释放。
void use_factory01(T arg) {
shared_ptr<Foo> p = factory(arg);
// 使用p
} // p离开了作用域,它指向的内存会被自动释放
// 但如果有其他shared_ptr也指向这块内存,它就不会被释放掉:
void use_factory02(T arg) {
shared_ptr<Foo> p = factory(arg);
// 使用p
return p; // 当返回p时,引用计数进行了递增操作。
} // p离开了作用域,被销毁,但它指向的内存不会被释放掉。
由于在最后一个
shared_ptr
销毁前内存都不会释放,保证shared_ptr
在无用之后不再保留就非常重要了。如果忘记了销毁程序不再需要的shared_ptr
,程序仍会正确执行,但会浪费内存。shared_ptr
在无用之后仍然保留的一种可能情况是:将shared_ptr
存放在一个容器中,随后重排了容器,从而不再需要某些元素。在这种情况下,应该确保用erase
删除那些不再需要的shared_ptr
元素。
使用了动态生存期的资源的类
程序使用动态内存出于以下三种原因之一:
- 程序不知道自己需要使用多少对象。例如,容器类。
- 程序不知道所需对象的准确类型。第15章中会介绍。
- 程序需要在多个对象间共享数据。
// 使用动态内存的一个常见原因是允许多个对象共享相同的状态
#include <string>
#include <vector>
#include <memory>
#include <stdexcept>
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:
// 唯一的数据成员。当拷贝、赋值或销毁一个StrBlob对象时,该成员会被拷贝、赋值或销毁。
// 对于由StrBlob构造函数分配的vector,当最后一个指向它的StrBlob对象被销毁时,它会
// 随之被自动销毁。不能直接保存vector,因为一个对象的成员在对象销毁时也会被销毁。
std::shared_ptr<std::vector<std::string>> data;
// 如果data[i]不合法,抛出一个异常。
void check(size_type i, const std::string &msg) const;
};
// 默认构造函数分配一个空vector
StrBlob::StrBlob()
: data(std::make_shared<std::vector<std::string>>()) {
}
StrBlob::StrBlob(std::initializer_list<std::string> il)
: data(std::make_shared<std::vector<std::string>>(il)) {
}
void StrBlob::check(StrBlob::size_type i, const std::string &msg) const {
if (i >= data->size()) {
throw std::out_of_range(msg);
}
}
std::string &StrBlob::front() {
// 如果vector为空,check会抛出一个异常。
check(0, "front on empty StrBlob");
return data->front();
}
std::string &StrBlob::back() {
check(0, "back on empty StrBlob");
return data->back();
}
void StrBlob::pop_back() {
check(0, "pop_back on empty StrBlob");
data->pop_back();
}
12.1.2直接管理内存
在c++中,动态内存的管理是通过一对运算符来完成的:
new
:在动态内存中为对象分配空间并返回一个指向该对象的指针,可以选择对对象进行初始化。delete
:接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。
使用new动态分配和初始化对象
默认情况下,动态分配的对象是默认初始化的,这意味着内置类型或组合类型的对象的值将是未定义的,而类类型对象将用默认构造函数进行初始化:
int *pi = new int; // pi指向一个动态分配的、未初始化的无名对象。
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, 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
:
auto p1 = new auto(obj); // p指向一个与obj类型相同的对象,该对象用obj进行初始化。
auto p2 = new auto{a, b, c}; // 错误:括号中只能有单个初始化器。
动态分配的const对象
用
new
分配const
对象是合法的:
// 分配并初始化一个const int
const int *pci = new const int(1024);
// 分配并默认初始化一个const的空string
const string *pcs = new const string;
一个动态分配的
const
对象必须进行初始化。对于一个定义了默认构造函数的类类型,其const
动态对象可以隐式初始化,而其他类型的对象就必须显式初始化。由于分配的对象是const
的,new
返回的指针是一个指向const
的指针。
内存耗尽
默认情况下,如果
new
不能分配所要求的内存空间,它会抛出一个类型为bad_alloc
的异常。可以改变使用new
的方式来阻止它抛出异常:
int *p1 = new int; // 如果分配失败,抛出bad_alloc。
// 定位new
int *p2 = new (nothrow) int; // 如果分配失败,返回一个空指针。
指针值和delete
传递给
delete
的指针必须指向动态分配的内存,或者是一个空指针。释放一块并非new
分配的内存,或者将相同的指针值释放多次,其行为是未定义的:
int i, *pi1 = &i, *pi2 = nullptr;
double *pd = new double(33), *pd2 = pd;
delete i; // 错误:i不是一个指针。
delete pi1; // 未定义:pi1不指向动态分配的内存,而是一个局部变量。
delete pd; // 正确
delete pd2; // 未定义:pd2指向的内存之前已经被释放了。
delete pi2; // 正确:释放一个空指针总是没有错误的。
// 虽然一个const对象的值不能被改变,但它本身是可以被销毁的。
const int *pci = new const int(1024);
delete pci;
动态对象的生存期直到被释放为止
Foo *factory(T arg) {
// 视情况处理arg
return new Foo(arg); // 调用者负责释放此内存
}
void use_factory01(T arg) {
Foo *p = factory(arg);
// 使用p但是不delete它
} // p离开了它的作用域,但它所指向的内存没有被释放。
// 正确方法是记得释放内存
void use_factory02(T arg) {
Foo *p = factory(arg);
// 使用p
delete p; //释放内存
}
使用
new
和delete
管理动态内存存在三个常见问题:
- 忘记
delete
内存。会导致内存泄漏问题。- 使用已经释放掉的对象。通过在释放内存后将指针置为空,有时可以监测出这种错误。
- 同一块内存释放两次。如果对一个指针进行了
delete
操作,对象的内存就被归还给自由空间了。如果随后又delete
第二个指针,自由空间就可能被破坏。坚持只使用智能指针,就可以避免所有这些问题。对于一块内存,只有在没有任何智能指针指向它的情况下,智能指针才会自动释放它。
delete之后重置指针值
当
delete
一个指针后,指针值就变为无效了。虽然指针已经无效,但在很多机器上指针仍然保存着(已经释放了的)动态内存的地址。在delete
之后,指针就变成了空悬指针,即,指向一块曾经保存数据对象但现在已经无效的内存的指针。
有一种方法可以避免空悬指针的问题:在指针即将要离开其作用域之前释放掉它所关联的内存。这样,在指针关联的内存被释放掉之后,就没有机会继续使用指针了。如果需要保留指针,可以在delete
之后将nullptr
赋予指针,这样就清楚地指出指针不指向任何对象。
这只是提供了有限的保护
动态内存的一个基本问题是可能有多个指针指向相同的内存。在
delete
内存之后重置指针的方法只对这个指针有效,对其他任何仍指向(已释放的)内存的指针是没有作用的:
int *p(new int(42)); // p指向动态内存
auto q = p; // p和q指向相同的内存
delete p; // p和q均变为无效
p = nullptr; // 指出p不再绑定到任何对象,但是对q没有任何作用。
12.1.3shared_ptr和new结合使用
接受指针参数的智能指针构造函数是
explicit
的。因此,不能将一个内置指针隐式转换为一个智能指针,必须使用直接初始化形式:
shared_ptr<int> p1 = new int(1024); // 错误:必须使用直接初始化形式。
shared_ptr<int> p2(new int(1024)); // 正确
出于相同的原因,一个返回
shared_ptr
的函数不能在其返回语句中隐式转换一个普通指针:
shared_ptr<int> clone(int p) {
return new int(p); // 错误:隐式转换为shared_ptr<int>。
}
// 必须将shared_ptr显式绑定到一个想要返回的指针上:
shared_ptr<int> clone(int p) {
// 正确:显式地用int*创建shared_ptr<int>。
return shared_ptr<int>(new int(p));
}
默认情况下,一个用来初始化智能指针的普通指针必须指向动态内存,因为智能指针默认使用
delete
释放它所关联的对象。可以将智能指针绑定到一个指向其他类型的资源的指针上,但是为了这样做,必须提供自己的操作来替代delete
。
不要混合使用普通指针和智能指针
void process(shared_ptr<int> ptr) {
// 使用ptr
} // ptr离开作用域,被销毁。
int *x(new int(1024)); // 危险:x是一个普通指针,不是一个智能指针。
process(x); // 错误:不能将int*转换为一个shared_ptr<int>。
process(shared_ptr<int>(x)); // 合法的,但内存会被释放。
int j = *x; // 未定义的:x是一个空悬指针。
当将一个
shared_ptr
绑定到一个普通指针时,就将内存的管理责任交给了这个shared_ptr
。一旦这样做了,就不应该再使用内置指针来访问shared_ptr
所指向的内存了。
也不要使用get初始化另一个智能指针或为智能指针赋值
shared_ptr<int> p(new int(42)); // 引用计数为1
int *q = p.get(); // 正确:但使用q时要注意,不要让它管理的指针被释放。
{ // 新程序块
// 未定义:两个独立的shared_ptr指向相同的内存。
shared_ptr<int>(q);
} // 程序块结束,q被销毁,它指向的内存被释放。
int foo = *p; // 未定义:p指向的内存已经被释放了。
其他shared_ptr操作
可以用
reset
将一个新的指针赋予一个shared_ptr
。reset
会更新引用计数,如果需要的话,会释放其指向的对象。
p = new int(1024); // 错误:不能将一个指针赋予shared_ptr。
p.reset(new int(1024)); // 正确:p指向一个新对象。
经常与
unique
一起使用,来控制多个shared_ptr
共享的对象。在改变底层对象之前,检查自己是否是当前对象仅有的用户。如果不是,在改变之前要制作一份新的拷贝:
if (!p.unique()) {
// 拷贝当前对象的值并分配到新的内存区域,这里的reset只是
// 令p指向新的地址,对其他指向当前对象的智能指针没有影响。
p.reset(new string(*p)); // 不是唯一用户,分配新的拷贝。
} else {
*p += newVal; // 现在知道自己是唯一的用户,可以改变对象的值。
}
12.1.4智能指针和异常
一个简单的确保异常发生后资源被释放的方法是使用智能指针,即使程序块过早结束,智能指针类也能确保在内存不再需要时将其释放:
void f() {
shared_ptr<int> sp(new int(42)); // 分配一个新对象
// 这段代码抛出一个异常,且在f中未被捕获。
}
// 函数的退出有两种可能,正常处理结束或者发生了异常,无论哪种情况,局部对象都会被销毁。
// 因此在函数结束时,shared_ptr自动释放内存。
与之相对的,当发生异常时,直接管理的内存是不会自动释放的。如果使用内置指针管理内存,且在
new
之后在对应的delete
之前发生了异常,则内存不会被释放:
void f() {
int *ip = new int(42); // 动态分配一个新对象
// 这段代码抛出一个异常,且在f中未被捕获。
delete ip; // 在退出之前释放内存
}
智能指针和哑类
并不是所有的类都定义了析构函数,从而清理对象使用的资源,特别是那些为c和c++两种语言设计的类,通常都要求用户显式地释放所使用的任何资源。
那些分配了资源,而又没有定义析构函数来释放这些资源的类,可能会遇到与使用动态内存相同的错误,即忘记释放资源。类似的,如果在资源分配和释放之间发生了异常,程序也会发生资源泄漏。
与管理动态内存类似,通常可以使用类似的技术来管理不具有良好定义的析构函数的类:
// 假定正在使用一个c和c++都使用的网络库:
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。这个删除器函数必须能够完成对shared_ptr中保存的指针进行释放的操作。
void end_connection(connection *p) {
disconnect(*p);
}
void f(destination &d /* 其他参数 */) {
connection c = connect(&d);
shared_ptr<connection> p(&c, end_connection);
// 使用连接
// 当f退出时(即使是由于异常而退出),connection会被正确关闭。
}
智能指针可以提供对动态分配的内存安全而又方便的管理,但这建立在正确使用的前提下。为了正确使用智能指针,必须坚持一些基本规范:
- 不使用相同的内置指针初始化(或
reset
)多个智能指针。- 不
delete get()
返回的指针。- 不使用
get()
初始化或reset
另一个智能指针。- 如果使用
get()
返回的指针,记住当最后一个对应的智能指针销毁后,指针就变为无效了。- 如果使用智能指针管理的资源不是
new
分配的内存,记住传递给它一个删除器。
12.1.5unique_ptr
某个时刻只能有一个
unique_ptr
指向一个给定对象。当unique_ptr
被销毁时,它所指向的对象也被销毁。
没有类似
make_shared
的标准库函数返回一个unique_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
不支持普通的拷贝或赋值操作:
unique_ptr<string> p1(new string("Stegosaurus"));
unique_ptr<string> p2(p1); // 错误:unique_ptr不支持拷贝。
unique_ptr<string> p3;
p3 = p2; // 错误:unique_ptr不支持赋值。
但是可以通过调用
release
或reset
将指针的所有权从一个(非const
)unique_ptr
转移给另一个:
// 将所有权从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原来指向的内存
调用
release
会切断unique_ptr
和它原来管理的对象间的联系。release
返回的指针通常被用来初始化另一个智能指针或给另一个智能指针赋值。但是,如果不用另一个智能指针来保存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;
}
向unique_ptr传递删除器
unique_ptr
默认情况下使用delete
释放它指向的对象。可以重载一个unique_ptr
中默认的删除器。
重载一个unique_ptr
中的删除器会影响到unique_ptr
类型以及如何构造(或reset
)该类型的对象。因此,必须在尖括号中提供删除器的类型:
// p指向一个类型为objT的对象,并使用一个类型为delT的对象释放objT对象,
// 它会调用一个名为fcn的delT类型对象。
unique_ptr<objT, delT> p(new objT, fcn);
重写之前的连接程序:
void f(destination &d /* 其他需要的参数 */) {
connection c = connect(&d); // 打开连接
// 当p被销毁时,连接将会关闭。
unique_ptr<connection, decltype(end_connection) *> p(&c, end_connection);
// 使用连接
// 当f退出时(即使是由于异常而退出),connection会被正确关闭。
}
12.1.6weak_ptr
weak_ptr
是一种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr
管理的对象。将一个weak_ptr
绑定到一个shared_ptr
不会改变shared_ptr
的引用计数。一旦最后一个指向对象的shared_ptr
被销毁,对象就会被释放。即使有weak_ptr
指向对象,对象也还是会被释放。
由于对象可能不存在,不能使用
weak_ptr
直接访问对象,而必须调用lock
:
if (shared_ptr<int> np = wp.lock()) { // 如果np不为空则条件成立
// 在if中,np与p共享对象。
}
核查指针类
// 作为weak_ptr用途的一个展示,将为StrBlob类定义一个伴随指针类。
#include <memory>
#include <vector>
#include <string>
#include <stdexcept>
#include "StrBlob.h"
class StrBlobPtr {
public:
StrBlobPtr() : curr(0) {
}
// 不能将StrBlobPtr绑定到一个const StrBlob对象。这个限制是由于构造函数接受
// 一个非const StrBlob对象的引用而导致的。需要注意的是,为了访问data成员,
// 指针类必须声明为StrBlob的friend。
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;
};
std::shared_ptr<std::vector<std::string>> StrBlobPtr::check(std::size_t i, const std::string &msg) const {
// vector还存在吗?
auto ret = wptr.lock();
if (!ret) {
throw std::runtime_error("unbound StrBlobPtr");
}
if (i >= ret->size()) {
throw std::out_of_range(msg);
}
return ret;
}
std::string &StrBlobPtr::deref() const {
auto p = check(curr, "dereference past end");
return (*p)[curr];
}
StrBlobPtr &StrBlobPtr::incr() {
// 如果curr已经指向容器的尾后位置,就不能递增它。
check(curr, "increment past end of StrBlobPtr");
// 推进当前位置
++curr;
return *this;
}
12.2动态数组
C++和标准库提供了两种一次分配一个对象数组的方法:
new
表达式语法,可以分配并初始化一个对象数组。- 使用名为
allocator
的类,允许将分配和初始化分离。通常会提供更好的性能和更灵活的内存管理能力。
很多(可能是大多数)应用都没有直接访问动态数组的需求。当一个应用需要可变数量的对象时,使用
vector
(或其他标准库容器)几乎总是更简单、更快速并且更安全的。
使用容器的类可以使用默认版本的拷贝、赋值和析构操作。分配动态数组的类则必须定义自己版本的操作,在拷贝、复制以及销毁对象时管理所关联的内存。
12.2.1new和数组
// 调用get_size确定分配多少个int,方括号中的大小必须是整型,但不必是常量。
int *pia = new int[get_size()]; // pia指向第一个int
// 也可以用一个表示数组类型的类型别名来分配一个数组:
typedef int arrT[42];
int *p = new arrT; // 等价于:int *p = new int[42]。
分配一个数组会得到一个元素类型的指针
当用
new
分配一个数组时,并未得到一个数组类型的对象,而是得到一个数组元素类型的指针。即使使用类型别名定义了一个数组类型,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
在新标准中,还可以提供一个元素初始化器的花括号列表:
int *pia3 = new int[10]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
string *psa3 = new string[10]{"a", "an", "the", string(3, 'x')};
如果初始化器数目大于元素数目,则
new
表达式失败,不会分配任何内存。
虽然用空括号对数组中元素进行值初始化,但不能在括号中给出初始化器,这意味着不能用auto
分配数组。
动态分配一个空数组是合法的
size_t n = get_size();
int *p = new int[n];
for (int *q = p; q != p + n; ++q) {
/* 处理数组 */
}
当用
new
分配一个大小为0的数组时,new
返回一个合法的非空指针。此指针保证与new
返回的其他任何指针都不相同。对于零长度的数组来说,此指针就像尾后指针一样,可以像使用尾后迭代器一样使用这个指针。可以用此指针进行比较操作,例如循环。可以向此指针加上(或从此指针减去)0,也可以从此指针减去自身从而得到0。但此指针不能解引用,毕竟它不指向任何元素。
释放动态数组
delete p; // p必须指向一个动态分配的对象或为空
delete[] pa; // pa必须指向一个动态分配的数组或为空
动态数组中的元素按逆序销毁,即,最后一个元素首先被销毁,然后是倒数第二个。
当释放一个指向数组的指针时,空方括号对是必需的:它指示编译器此指针指向一个对象数组的第一个元素。
typedef int arrT[42];
int *p = new arrT;
delete[] p; // 方括号是必须的,即使是使用类型别名。
智能指针和动态数组
标准库提供了一个可以管理
new
分配的数组的unique_ptr
版本:
// up指向一个包含10个未初始化int的数组
unique_ptr<int[]> up(new int[10]);
up.release(); // 自动用delete[]销毁其指针
另一方面,当一个
unique_ptr
指向一个数组时,可以使用下标运算符来访问数组中的元素:
for (size_t i = 0; i != 10; ++i) {
up[i] = i; // 为每个元素赋予一个新值
}
如果希望使用
shared_ptr
管理一个动态数组,必须提供自己定义的删除器:
shared_ptr<int> sp(new int[10], [](int *p) { delete[] p; });
sp.reset(); // 使用提供的lambda释放数组,它使用delete[]。
由于默认情况下,
shared_ptr
使用delete
销毁它指向的对象。如果此对象是一个动态数组,对其使用delete
所产生的问题与释放一个动态数组指针时忘记[]
产生的问题一样。
shared_ptr
不直接支持动态数组管理这一特性会影响如何访问数组中的元素:
// shared_ptr未定义下标运算符,并且不支持指针的算术运算。
for (size_t i = 0; i != 10; ++i) {
*(sp.get() + i) = i; // 使用get获取一个内置指针
}
12.2.2allocator类
new
将内存分配和对象构造组合在了一起。类似的,delete
将对象析构和内存释放组合在了一起。分配单个对象时,通常希望将内存分配和对象初始化组合在一起。因为在这种情况下,几乎肯定知道对象应有什么值。
当分配一大块内存时,通常计划在这块内存上按需构造对象。在此情况下,希望将内存分配和对象构造分离。这意味着可以分配大块内存,但只在真正需要时才真正执行对象创建操作(同时付出一定开销)。
一般情况下,将内存分配和对象构造组合在一起可能会导致不必要的浪费。另一方面,没有默认构造函数的类就不能动态分配数组了:
// 可能不需要n个string,少量string可能就足够了。这样,就可能创建了一些
// 永远也用不到的对象。而且,对于那些确实要使用的对象,也在初始化之后立即
// 赋予了它们新值。每个使用到的元素都被赋值了两次:第一次是在默认初始化时,
// 随后是在赋值时。
string *const p = new string[n];
string s;
string *q = p;
while (cin >> s && q != p + n) {
*q++ = s;
}
const size_t size = q - p; // 记住读取了多少个string
// 使用数组
delete[] p;
allocator类
allocator
类将内存分配和对象构造分离开来。它提供一种类型感知的内存分配方法,它分配的内存是原始的、未构造的。当一个allocator
对象分配内存时,它会根据给定的对象类型来确定恰当的内存大小和对齐位置:
allocator<string> alloc; // 可以分配string的allocator对象
auto const p = alloc.allocate(n); // 分配n个未初始化的string
allocator分配未构造的内存
// 额外参数必须是与构造的对象的类型相匹配的合法的初始化器
auto q = p; // q指向最后构造的元素之后的位置,目前是第一个。
alloc.construct(q++); // *q为空字符串
alloc.construct(q++, 10, 'c'); // *q为cccccccccc
alloc.construct(q++, "hi"); // *q为hi
还未构造对象的情况下就使用原始内存是错误的:
cout << *p << endl; // 正确:使用string的输出运算符。
cout << *q << endl; // 灾难:q指向未构造的内存!
为了使用
allocate
返回的内存,必须用construct
构造对象。使用未构造的内存,其行为是未定义的。
当用完对象后,必须对每个构造的元素调用
destroy
来销毁它们。需要注意的是,只能对真正构造了的元素进行destroy
操作:
// 开始时,q指向最后构造的元素之后的位置。
while (q != p) {
alloc.destroy(--q);
}
一旦元素被销毁后,就可以重新使用这部分内存来保存其他元素,也可以将其归还给系统。释放内存通过调用
deallocate
来完成。
拷贝和填充未初始化内存的算法
// 假定有一个int的vector,希望将其内容拷贝到动态内存中。
// 分配比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);