声明:仅为个人学习总结,还请批判性查看,如有不同观点,欢迎交流。
摘要
本文主要包括:通过 new/delete、shared_ptr、unique_ptr 进行单个对象的动态创建和销毁,通过 new/delete、allocator 进行对象数组的动态分配和释放,以及通过智能指针进行动态数组的管理。
对象及其生存期
对象类型 | 责任方 | 生存期 | 存储区域 |
---|---|---|---|
全局对象 | 编译器 | 在程序启动时创建,在程序结束时销毁 | 静态内存 |
局部自动对象 | 编译器 | 在进入其定义所在的程序块时被创建,在离开块时销毁 | 栈内存 |
局部 static 对象 | 编译器 | 在第一次使用前创建,在程序结束时销毁 | 静态内存 |
动态分配对象 | 程序 | 在需要时显式创建,在不需要时显式(或由智能指针自动)销毁 | 堆内存/自由空间 |
1、动态内存与智能指针
程序使用动态内存的原因:
- 不知道需要使用多少对象;
- 不知道所需对象的准确类型;
- 需要在多个对象间共享数据。
1.1 智能指针(C++11)
智能指针(smart pointer),行为类似常规指针,但是能够自动释放所指向的对象。
- 智能指针是模板,创建智能指针时,必须提供指针可以指向的类型;
- 默认初始化的智能指针中保存着一个空指针;
- 智能指针的使用方式与普通指针类似:
- 解引用一个智能指针,返回它所指向的对象;
- 在条件判断中使用智能指针,效果就是检测它是否为空。
C++11 标准库提供了两种智能指针类型:
shared_ptr
,允许多个指针指向同一个对象;weak_ptr
,是一种弱引用,指向shared_ptr
所管理的对象。
unique_ptr
,“独占”所指向的对象。
智能指针操作:
序号 | 操作 | 说明 | shared_ptr | unique_ptr |
---|---|---|---|---|
1 | shared_ptr<T> sp unique_ptr<T> up | 定义空智能指针,可以指向类型为 T 的对象 | √ | √ |
2 | p | 将 p 用作一个条件判断,若 p 指向一个对象,则为 true | √ | √ |
3 | *p | 解引用 p ,获得它指向的对象 | √ | √ |
4 | p->mem | 等价于 (*p).mem | √ | √ |
5 | p.get() | 返回 p 中保存的指针。谨慎使用,如果智能指针释放了其对象,那么返回的指针所指向的对象也就消失了 | √ | √ |
6 | swap(p, q) p.swap(q) | 交换 p 和 q 中的指针 | √ | √ |
1 | make_shared<T>(args) | 返回一个 shared_ptr ,指向一个动态分配的类型为 T 的对象。使用 args 初始化该对象 | √ | |
2 | shared_ptr<T> p(q) | p 是 shared_ptr q 的拷贝;此操作会递增 q 的引用计数。q 中的指针必须能转换为 T* | √ | |
3 | p = q | p 和 p 都是 shared_ptr ,所保存的指针必须能相互转换。此操作会递增 q 的引用计数,递减 p 的引用计数;如果 p 的引用计数变为 0,则释放其管理的原内存 | √ | |
4 | p.use_count() | 返回与 p 共享对象的智能指针数量;可能很慢,主要用于调试 | √ | |
5 | p.unique() | 如果 p.use_count() 为 1,返回 true ,否则返回 false | √ | |
6 | shared_ptr<T> p(q) | p 管理内置指针 q 指向的对象;q 必须指向 new 分配的内存,且能够转换为 T* 类型 | √ | |
7 | shared_ptr<T> p(u) | p 从 unique_ptr u 那里接管了对象的所有权;将 u 置为空 | √ | |
8 | shared_ptr<T> p(q,d) | p 接管了内置指针 q 所指向的对象的所有权。q 必须能转换成 T* 类型。p 将使用可调用对象 d 来代替 delete | √ | |
9 | shared_ptr<T> p(p2,d) | p 是 shared_ptr p2 的副本,p 将使用可调用对象 d 来代替 delete | √ | |
10 | p.reset() | 若 p 是唯一指向其对象的 shared_ptr ,reset 会释放此对象 | √ | |
11 | p.reset(q) | 若传递了可选的内置指针 q ,会令 p 指向 q ,否则会将 p 置为空 | √ | |
12 | p.reset(q,d) | 若还传递了参数 d ,将会调用 d 而不是 delete 来释放 q | √ | |
1 | unique_ptr<T> u1 | 定义空 unique_ptr ,可以指向类型为 T 的对象。u1 会使用 delete 来释放它的指针 | √ | |
2 | unique_ptr<T, D> u2 | u2 会使用类型 D 的可调用对象代替 delete | √ | |
3 | unique_ptr<T, D> u(d) | u 会使用类型 D 的对象 d 代替 delete | √ | |
4 | u = nullptr | 释放 u 指向的对象,将 u 置为空 | √ | |
5 | u.release() | u 放弃对指针的控制权,返回指针,并将 u 置为空 | √ | |
6 | u.reset() | 释放 u 指向的对象 | √ | |
7 | u.reset(q) | 如果提供了内置指针 q ,令 u 指向这个对象 | √ | |
8 | u.reset(nullptr) | 否则将 u 置为空 | √ |
1.2 shared_ptr 类
1.2.1 make_shared 函数
标准库函数 make_shared
,在动态内存中分配一个对象并初始化它,返回指向此对象的 shared_ptr
。
make_shared
用其参数来构造给定类型的对象;- 如果不传递任何参数,对象就会进行值初始化:
- 对于内置类型,初始值自动设为 0;
- 对于类类型,进行默认初始化。
shared_ptr<string> p1; // shared_ptr that can point at a string
shared_ptr<list<int>> p2; // shared_ptr that can point at a list of ints
shared_ptr<int> p3 = make_shared<int>(42); // shared_ptr that points to an int with value 42
shared_ptr<string> p4 = make_shared<string>(10, '9'); // p4 points to a string with value 9999999999
shared_ptr<int> p5 = make_shared<int>(); // p5 points to an int that is value initialized to 0
// 通常用 auto 定义一个对象来保存 make_shared 的结果
auto p6 = make_shared<vector<string>>(); // p6 points to a dynamically allocated, empty vector<string>
1.2.2 拷贝、赋值和销毁
可以认为每个 shared_ptr
都有一个关联的计数器(引用计数,reference count)。
- 拷贝一个
shared_ptr
,计数器会递增:- 用一个
shared_ptr
初始化另一个shared_ptr
; - 将
shared_ptr
作为参数传递给一个函数; - 将
shared_ptr
作为函数的返回值。
- 用一个
- 销毁一个
shared_ptr
,计数器会递减,变为 0 时,自动释放所管理的对象:- 一个局部的
shared_ptr
离开其作用域; - 调用
reset
成员。
- 一个局部的
- 给
shared_ptr
赋予一个新值:- 右侧运算对象,计数器会递增;
- 左侧运算对象,计数器会递减,变为 0 时,自动释放所管理的对象。
auto p = make_shared<int>(42); // p 指向的对象只有 p 一个引用者
auto q(p); // p 和 q 指向相同对象,此对象有两个引用者
auto r = make_shared<int>(42);
r = q; // 给 r 赋值,令它指向另一个地址
// 递增 q 指向的对象的引用计数
// 递减 r 原来指向的对象的引用计数
// r 原来指向的对象已经没有引用者,会自动释放
1.3 直接管理内存
在 C++ 中,通过一对运算符来直接管理动态内存:
new
,为对象分配空间,进行初始化(如果需要),返回指向对象的指针;delete
,接受指向对象的指针,销毁对象、释放内存。
1.3.1 分配与初始化对象
动态分配对象:
- 在自由空间分配的内存是未命名的(无名对象);
- 返回一个指向分配的对象的指针。
int *pi = new int; // pi 指向一个动态分配的、未初始化的、无名对象
初始化对象:
- 默认情况下,进行默认初始化:
- 内置类型或组合类型的对象,值将是未定义的;
- 类类型的对象,将用默认构造函数进行初始化。
- 可以进行直接初始化:
- 使用传统的构造方式(使用圆括号);
- 使用列表初始化方式(使用花括号,C++11)。
- 可以进行值初始化(在类型名之后加上一对空的圆括号)。
int *pi = new int(1024); // 直接初始化,*pi 的值为 1024
string *ps = new string(10, '9'); // 直接初始化 *ps 为 "9999999999"
vector<int> *pv = new vector<int>{0, 1, 2}; // 直接初始化 vector 有 3 个元素,值从 0 到 2
string *ps1 = new string; // 默认初始化,值为空 string
string *ps2 = new string(); // 值初始化,值为空 string
int *pi1 = new int; // 默认初始化,*pi1 的值未定义
int *pi2 = new int(); // 值初始化,*pi2 的值为 0
可以使用 auto
从初始化器推断想要分配的对象的类型:
- 初始化器被圆括号包围;
- 在括号中仅有单一初始化器。
auto p1 = new auto(obj); // p1 指向一个与 obj 类型相同的对象,该对象用 obj 进行初始化
auto p2 = new auto(a, b, c); // 错误:括号中只能有单个初始化器
可以用 new
分配 const
对象:
- 必须进行初始化:
- 定义了默认构造函数的类类型,可以隐式初始化;
- 其他类型的对象,必须显示初始化。
- 返回一个指向
const
的指针。
const int *pci = new const int(1024); // 分配并初始化一个 const int
const string *pcs = new const string; // 分配并默认初始化一个 const 的空 string
1.3.2 内存耗尽
一旦一个程序用光了它所有可用的内存,new
表达式就会失败:
- 默认情况下,它会抛出一个类型为
bad_alloc
的异常; - 可以传递
nothrow
给new
,告诉new
不能抛出异常:定位 new
(placement new) 表达式,允许向new
传递额外的实参;nothrow
是由标准库定义的对象。
int *p1 = new int; // 如果分配失败,new 抛出 std::bad_alloc
int *p2 = new (nothrow) int; // 如果分配失败,new 返回空指针
1.3.3 释放动态内存
由内置指针管理的动态对象,需要进行显式释放,才会被销毁。
通过 delete
表达式,销毁给定的指针指向的对象,释放对应的内存。
delete p; // p 必须指向一个动态分配的对象,或是一个空指针
p = nullptr; // p 不再绑定到任何对象
const int *pci = new const int(1024);
delete pci; // 正确:释放一个 const 对象
使用 new
和 delete
管理动态内存的 3 个常见问题:
- 忘记
delete
内存,导致“内存泄漏”; - 使用已经
delete
的对象; - 同一块内存被重复
delete
。
1.4 shared_ptr 和 new 结合
可以用 new
返回的指针来初始化智能指针,接受指针参数的智能指针构造函数是 explicit
的。
shared_ptr<int> p1(new int(42)); // 正确:必须使用直接初始化形式
shared_ptr<int> p2 = new int(42); // 错误:不能将内置指针隐式地转换为智能指针
shared_ptr<string> p;
// 可以用 reset 将一个新的指针赋值给 shared_ptr
// 与赋值类似,reset 会更新引用计数,如果需要的话,会释放 p 指向的对象
// p = new string; // 错误:不能将一个指针赋予 shared_ptr
p.reset(new string); // 正确:p 指向一个新对象
// reset 成员经常与 unique 一起使用,来控制多个 shared_ptr 之间共享的对象
if (!p.unique())
p.reset(new string(*p)); // 不是唯一用户,分配新的拷贝
*p += newVal; // 已经是唯一的用户,可以改变对象的值
1.4.1 不要混合使用普通指针和智能指针
shared_ptr
可以协调对象的析构,但仅限于其自身的拷贝(也是 shared_ptr
)之间。
- 尽量使用
make_shared
而不是new
,能够在分配对象的同时就将shared_ptr
与之绑定,避免无意中将同一块内存绑定到多个独立创建的shared_ptr
上。 - 在将一个
shared_ptr
绑定到普通指针上之后,就不应该再使用内置指针来访问shared_ptr
所指向的内存,因为无法知道对象何时会被销毁。
// ptr is created and initialized when process is called
void process(shared_ptr<int> ptr)
{
// use ptr
} // ptr goes out of scope and is destroyed
int main()
{
shared_ptr<int> p(new int(42)); // reference count is 1
process(p); // copying p increments its count; in process the reference count is 2
int i = *p; // ok: reference count is 1
int *x(new int(1024)); // dangerous: x is a plain pointer, not a smart pointer
process(x); // error: cannot convert int* to shared_ptr<int>
process(shared_ptr<int>(x)); // legal, but the memory will be deleted!
int j = *x; // undefined: x is a dangling pointer!
return 0;
}
1.4.2 不要使用 get 初始化另一个智能指针
智能指针的 get
函数,可以返回一个内置指针,指向智能指针管理的对象:
- 用于将内置指针传递给无法使用智能指针的代码;
- 使用
get
返回的指针的代码不能delete
此指针。- 不要用
get
初始化另一个智能指针或者为另一个智能指针赋值。
- 不要用
shared_ptr<int> p(new int(42)); // reference count is 1
int *q = p.get(); // ok: but don't use q in any way that might delete its pointer
{ // new block
// undefined: two independent shared_ptrs point to the same memory
shared_ptr<int>(q);
} // block ends, q is destroyed, and the memory to which q points is freed
int foo = *p; // undefined; the memory to which p points was freed
1.5 智能指针和异常
使用异常处理的程序,需要确保在异常发生后资源能被正确地释放。一个简单的确保资源被释放的方法是使用智能指针。
// 使用智能指针管理内存,即使程序块过早结束,智能指针类也能确保在内存不再需要时将其释放
void f1()
{
shared_ptr<int> sp(new int(42)); // allocate a new object
// code that throws an exception that is not caught inside f1
} // shared_ptr freed automatically when the function ends
// 使用内置指针管理内存,如果在 new 之后 delete 之前发生了异常,内存不会被释放
void f2()
{
int *ip = new int(42); // dynamically allocate a new object
// code that throws an exception that is not caught inside f2
delete ip; // free the memory before exiting
}
对于不能通过 delete
释放的资源,可以使用自定义的释放操作。
// 假设正在使用 C 和 C++ 都使用的网络库(没有析构函数),使用该库的程序可能包含以下代码:
struct destination; // represents what we are connecting to
struct connection; // information needed to use the connection
connection connect(destination *); // open the connection
void disconnect(connection); // close the given connection
// 为了使用 shared_ptr 来管理 connection,必须首先定义一个函数来代替 delete
// 这个删除器 (deleter) 函数必须能够对 shared_ptr 中保存的指针进行释放
void end_connection(connection *p) { disconnect(*p); }
void f(destination &d /* other parameters */)
{
connection c = connect(&d);
shared_ptr<connection> p(&c, end_connection);
// use the connection
// when f exits, even if by an exception, the connection will be properly closed
}
1.6 unique_ptr
一个 unique_ptr
“独占”它所指向的对象,某个时刻只能有一个 unique_ptr
指向一个给定对象。当 unique_ptr
被销毁时,它所指向的对象也被销毁。
- 定义一个
unique_ptr
时,需要将其绑定到一个new
返回的指针上;- 与
shared_ptr
不同,没有类似make_shared
的标准库函数返回一个unique_ptr
。
- 与
- 与
shared_ptr
一样,初始化unique_ptr
必须采用直接初始化形式; unique_ptr
不支持拷贝构造函数、拷贝赋值运算符(因为“独占”);unique_ptr
支持移动构造函数、移动赋值运算符;- 可以通过调用
release
、reset
将指针的所有权从一个(非const
)unique_ptr
转移给另一个unique_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); // error: no copy for unique_ptr
unique_ptr<string> p3;
p3 = p2; // error: no assign for unique_ptr
// transfers ownership from p1 (which points to the string Stegosaurus) to p2
unique_ptr<string> p2(p1.release()); // release makes p1 null
unique_ptr<string> p3(new string("Trex")); // transfers ownership from p3 to p2
p2.reset(p3.release()); // reset deletes the memory to which p2 had pointed
// 调用 release 会切断 unique_ptr 和它原来管理的对象之间的联系
// 如果不用另一个智能指针来保存 release 返回的指针,程序就要负责资源的释放
p2.release(); // 错误:p2 不会释放内存,并且丢失了指针
auto p = p2.release(); // 正确:但必须记得 delete(p)
// 可以拷贝或赋值一个将要被销毁的 unique_ptr,例如传递 unique_ptr 参数和返回 unique_ptr
// 在此情况下,编译器知道对象将要被销毁,将执行移动操作
unique_ptr<int> clone(int p) {
// ok: explicitly create a unique_ptr<int> from int*
unique_ptr<int> ret(new int(p));
// . . .
return ret;
}
对于不能通过 delete
释放的资源,可以使用自定义的释放操作。
与 shared_ptr
不同的是,重载一个 unique_ptr
中的删除器会影响到 unique_ptr
类型以及如何构造(或 reset
)该类型的对象。
// p 指向一个类型为 objT 的对象,并使用一个类型为 delT 的对象释放 objT 对象
// 它会调用一个名为 fcn 的 delT 类型的对象(删除器)
unique_ptr<objT, delT> p(new objT, fcn);
// 使用 unique_ptr 重写连接程序:
void f(destination &d /* other needed parameters */)
{
connection c = connect(&d); // open the connection
// when p is destroyed, the connection will be closed
unique_ptr<connection, decltype(end_connection) *> p(&c, end_connection);
// use the connection
// when f exits, even if by an exception, the connection will be properly closed
}
1.7 智能指针使用规范
智能指针可以提供对动态分配的内存安全而又方便的管理,但这建立在正确使用的前提下。为了正确使用智能指针,必须坚持一些基本规范。
智能指针使用规范:
- 不使用相同的内置指针初始化(或
reset
)多个智能指针;- 不
delete
get()
返回的指针;- 不使用
get()
初始化或reset
另一个智能指针;- 如果使用
get()
返回的指针,记住当最后一个对应的智能指针销毁后,返回的指针就变为无效了;- 如果使用智能指针管理的资源不是
new
分配的内存,需要传递给它一个删除器。
1.8 weak_ptr
weak_ptr
也是一种智能指针,但它不控制所指向对象的生存期,它指向一个由 shared_ptr
管理的对象。
- 将一个
weak_ptr
绑定到一个shared_ptr
不会改变shared_ptr
的引用计数(是“弱”共享); - 通过调用
weak_ptr
的lock
成员访问其对象,可以防止用户访问一个不再存在的对象。
序号 | 操作 | 说明 |
---|---|---|
1 | weak_ptr<T> w | 定义空 weak_ptr ,指向类型为 T 的对象 |
2 | weak_ptr<T> w(sp) | 指向与 shared_ptr sp 相同的对象,T 必须能够转换为 sp 指向的类型 |
3 | w = p | p 可以是一个 shared_ptr 或 weak_ptr ,赋值后 w 与 p 共享对象 |
4 | w.reset() | 将 w 置为空 |
5 | w.use_count() | 与 w 共享对象的 shared_ptr 的数量 |
6 | w.expired() | 如果 w.use_count() 为 0,返回 true ,否则返回 false |
7 | 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 weakly shares with p; use count in p is unchanged
// 由于对象可能不存在,所以不能使用 weak_ptr 直接访问对象,而必须调用 lock
// 如果对象存在,lock 返回一个指向共享对象的 shared_ptr
if (shared_ptr<int> np = wp.lock())
{ // true if np is not null
// inside the if, np shares its object with p
}
2、动态数组
new
和 delete
运算符一次分配/释放一个对象,有时需要一次分配/释放多个对象。
- C++ 语言定义了另一种
new
表达式语法,可以分配并初始化一个对象数组; - 标准库中的
allocator
类,允许将分配和初始化分离(通常性能更好管理更灵活)。
2.1 new 和数组
2.1.1 分配动态数组
让 new
分配一个对象数组:在类型名之后跟一对方括号,在其中指明要分配的对象的数目(整型,不必是常量),返回指向第一个对象的指针(而不是一个数组类型的对象)。
// call get_size to determine how many ints to allocate
int *pia = new int[get_size()]; // pia points to the first of these ints
typedef int arrT[42]; // arrT names the type array of 42 ints
int *p = new arrT; // allocates an array of 42 ints; p points to the first one
虽然不能创建大小为 0 的数组变量,但当 n
为 0 时,调用 new[n]
是合法的。
对于零长度的数组来说,new
返回一个合法的非空指针,不能解引用,但可以像使用尾后迭代器一样使用这个指针(进行比较操作,加上/减去 0,减去自身得到 0)。
char arr[0]; // error: cannot define a zero-length array
char *cp = new char[0]; // ok: but cp can't be dereferenced
size_t n = get_size(); // get_size returns the number of elements needed
int *p = new int[n]; // allocate an array to hold the elements
// 当 n 为 0 时,循环中的条件会失败(q 等于 p+n),循环体不会被执行
for (int *q = p; q != p + n; ++q)
/* process the array */;
2.1.2 初始化数组
在分配对象数组的同时,可以进行对象的初始化。
- 默认情况下,进行默认初始化:
- 内置类型或组合类型的对象,值将是未定义的;
- 类类型的对象,将用默认构造函数进行初始化。
- 可以进行值初始化(在大小之后加上一对空的圆括号):
- 不能在括号中给出初始化器,也就不能使用
auto
来分配数组。
- 不能在括号中给出初始化器,也就不能使用
- 可以提供一个元素初始化器的花括号列表(C++11,同内置数组对象):
- 如果初始化器数目小于元素数目,剩余元素将进行值初始化;
- 如果初始化器数目大于元素数目,
new
表达式失败。
int *pia = new int[10]; // 默认初始化:block of ten uninitialized ints
int *pia2 = new int[10](); // 值初始化:block of ten ints value initialized to 0
string *psa = new string[10]; // 默认初始化:block of ten empty strings
string *psa2 = new string[10](); // 值初始化:block of ten empty strings
// block of ten ints each initialized from the corresponding initializer
int *pia3 = new int[10]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
// block of ten strings; the first four are initialized from the given initializers
// remaining elements are value initialized
string *psa3 = new string[10]{"a", "an", "the", string(3, 'x')};
2.1.3 释放动态数组
释放动态数组:使用 delete [] 指针;
- 指针指向一个对象数组的第一个元素;
- 销毁数组中的元素,并释放对应的内存;
- 元素按逆序销毁,即,先销毁最后一个元素。
delete p; // p must point to a dynamically allocated object or be null
delete[] pa; // pa must point to a dynamically allocated array or be null
2.1.4 智能指针和动态数组
2.1.4.1 unique_ptr 和动态数组
标准库提供了一个 unique_ptr
版本,可以管理 new
分配的数组。
// 定义一个管理动态数组 unique_ptr 时,必须在对象类型后面跟一对空方括号
// 当 unique_ptr 销毁它所管理的指针时,自动使用 delete[]
unique_ptr<int[]> up(new int[10]); // up points to an array of ten uninitialized ints
up.reset(); // automatically uses delete[] to destroy its pointer
当用 unique_ptr
指向数组时,不能使用成员访问运算符 .
和 ->
,其他操作不变,并新增部分特有操作:
序号 | 操作 | 说明 |
---|---|---|
1 | unique_ptr<T[]> u | u 可以指向一个动态分配的数组,其元素类型为 T |
2 | unique_ptr<T[]> u(p) | u 指向内置指针 p 所指向的动态分配的数组,p 必须能够转换为类型 T* |
3 | u[i] | 使用下标运算符返回 u 拥有的数组中位置 i 处的对象。u 必须指向一个数组 |
2.1.4.2 shared_ptr 和动态数组
shared_ptr
不直接支持管理动态数组。
- 如果希望使用
shared_ptr
管理一个动态数组,必须提供自己定义的删除器。
// to use a shared_ptr we must supply a deleter
shared_ptr<int> sp(new int[10], [](int *p) { delete[] p; });
sp.reset(); // uses the lambda we supplied that uses delete[] to free the array
- 为了访问数组中的元素,必须用
get
获取一个内置指针,然后用它来访问数组元素。
// shared_ptr 未定义下标运算符,而且智能指针类型不支持指针算术运算
for (size_t i = 0; i != 10; ++i)
*(sp.get() + i) = i; // use get to get a built-in pointer
2.2 allocator 类
使用 new
分配动态数组,有一些灵活性上的局限:
- 将内存分配和对象构造组合在了一起,不能按需构造对象(数量、数据);
- 元素可能被赋值两次:第一次是在初始化时,第二次是在赋值时;
- 没有默认构造函数的类不能动态分配数组。
标准库 allocator
类,将内存分配和对象构造分离开来。它提供一种类型感知的内存分配方法,它分配的内存是原始的、未构造的。
allocator
支持的操作:
序号 | 操作 | 说明 |
---|---|---|
1 | allocator<T> a | 定义了一个名为 a 的 allocator 对象,它可以为类型为 T 的对象分配内存。 |
2 | a.allocate(n) | 分配一段原始的、未构造的内存,保存 n 个类型为 T 的对象。 |
3 | a.deallocate(p, n) | 释放从 T* 指针 p 中地址开始的内存,这块内存保存了 n 个类型为 T 的对象;p 必须是先前由 allocate 返回的指针,n 必须是 p 创建时所要求的大小。在调用 deallocate 之前,用户必须对每个在这块内存中创建的对象调用 destroy 。 |
4 | a.construct(p, args) | p 为 T* 类型的指针,指向一块原始内存;args 被传递给类型为 T 的构造函数,用来在 p 指向的内存中构造一个对象。 |
5 | a.destroy(p) | p 为 T* 类型的指针,对 p 指向的对象执行析构函数。 |
2.2.1 分配内存
allocator
是一个模板。为了定义一个 allocator
对象,必须指明这个 allocator
可以分配的对象类型。
当一个 allocator
对象分配内存时,它会根据给定的对象类型来确定恰当的内存大小和对齐位置。
// 这个 allocate 调用为 n 个 string 分配了内存
allocator<string> alloc; // object that can allocate strings
auto const p = alloc.allocate(n); // allocate n unconstructed strings
2.2.2 构造对象
allocator
分配的内存是未构造的(unconstructed):
- 为了使用
allocator
返回的内存,必须用construct
构造对象; - 在用完对象之后,必须对每个构造的对象调用
destroy
来销毁它们。
// 在 C++11 标准库中,construct 成员函数接受一个指针和零个或多个额外参数,
// 指针标识在给定的位置构造一个元素
// 额外参数用来初始化构造的对象,这些参数必须是与构造的对象的类型相匹配的合法的初始化器
auto q = p; // q 指向最后构造的元素之后的位置
alloc.construct(q++); // *q is the empty string
alloc.construct(q++, 10, 'c'); // *q is cccccccccc
alloc.construct(q++, "hi"); // *q is hi
// 还未构造对象的情况下就使用原始内存是错误的
cout << *p << endl; // 正确:使用 string 的输出运算符
cout << *q << endl; // 灾难:q 指向未构造的内存
// 在用完对象之后,必须对每个构造的元素调用 destroy 来销毁它们
// 即,对指向的对象执行析构函数
while (q != p)
alloc.destroy(--q); // 释放真正构造过的 string
// 一旦元素被销毁后,就可以重新使用这部分内存来保存其他 string,
// 也可以释放 allocator 对象分配的内存
alloc.deallocate(p, n);
2.2.3 拷贝和填充未初始化内存
标准库为 allocator
类定义了两个伴随算法,可以在未初始化的内存中创建对象。
序号 | 操作 | 说明 |
---|---|---|
1 | uninitialized_copy(b, e, b2) | 从迭代器 b 和 e 指出的输入范围中拷贝元素,到迭代器 b2 指定的未构造的原始内存中。b2 指向的内存必须足够大,能够容纳输入序列中元素的拷贝。 |
2 | uninitialized_copy_n(b, n, b2) | 从迭代器 b 指向的元素开始,拷贝 n 个元素到 b2 开始的内存中。 |
3 | uninitialized_fill(b, e, t) | 在迭代器 b 和 e 指定的原始内存范围中创建对象,对象的值均为 t 的拷贝。 |
4 | uninitialized_fill_n(b, n, t) | 从迭代器 b 指向的内存地址开始,创建 n 个对象,对象的值均为 t 的拷贝。b 必须指向足够大的未构造的原始内存,能够容纳给定数量的对象。 |
allocator<int> alloc;
vector<int> vi{0, 1, 2, 3, 4};
// allocate twice as many elements as vi holds
auto p = alloc.allocate(vi.size() * 2);
// construct elements starting at p as copies of elements in vi
// 返回(递增后的)目的位置迭代器 q,即指向最后一个构造的元素之后的位置的指针
auto q = uninitialized_copy(vi.begin(), vi.end(), p);
// initialize the remaining elements to 42
uninitialized_fill_n(q, vi.size(), 42);
参考
- [美] Stanley B.Lippman著.C++ Primer 中文版(第5版).电子工业出版社.2013.
- [美] Stephen Prata著.C++ Primer Plus(第6版)中文版.人民邮电出版社.2012.
宁静以致远,感谢 Vico 老师。