智能指针
为了使管理动态内存更容易、更安全,新标准库提供了两种管理动态对象的智能指针类型。智能指针的作用类似于常规指针,但格外重要的是它会自动删除它指向的对象。新标准库定义了两种智能指针,它们在管理底层指针方面有所不同:shared_ptr
(允许多个指针引用同一个对象)和unique_ptr
(“拥有”它指向的对象)。该库还定义了一个名为weak_ptr
的伴随类,它是对shared_ptr
管理的对象的弱引用。这三个类都在头文件<memory>
中定义。
关于头文件<memory>
的更多信息请参考header <memory>。
shared_ptr类
与vector
一样,智能指针也是模板。因此,当我们创建智能指针时,我们必须提供附加信息,即在这种情况下,指针可以指向的类型。与vector
一样,我们在尖括号内提供该类型,它遵循我们定义的智能指针类型的名称:
Code:
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
默认初始化智能指针为空指针。我们也可以使用其他初始化摸板类的方法对其进行初始化。
我们使用智能指针的方式类似于使用指针。解引用智能指针会返回指针指向的对象。当我们在条件中使用智能指针时,效果是测试指针是否为空:
Code:
// if p1 is not null, check whether it's the empty string
if (p1 && p1->empty())
*p1 = "hi"; // if so, dereference p1 to assign a new value to that string
下表中列出了shared_ptr
和unique_ptr
共有的操作:
操作 | 功能 |
---|---|
shared_ptr<T> sp unique_ptr<T> up
|
生成一个可以指向T类型对象的空指针。 |
p |
用p作为条件,如果p指向一个对象则返回true。 |
*p |
解引用p来获得p指向的对象 |
p->mem |
等效于(*p).mem 。 |
p.get() |
返回p中的指针。请谨慎使用;当智能指针要删除它时,返回的指针指向的对象将消失,这样返回的指针就是一个空指针。 |
swap(p, q) p.swp(q)
|
交换p和q中的指针。 |
下表中列出了shared_ptr
独有的操作:
操作 | 功能 |
---|---|
make_shared<T> (args) |
返回指向类型为T的动态分配对象的shared_ptr 。使用args 初始化该对象。 |
shared_ptr<T> p(q) |
p 是shared_ptr 对象q 的副本; 增加q 中的计数。q 中的指针必须可转换为T * 。 |
p = q |
p 和q 是shared_ptr ,它们包含可以相互转换的指针。减少p 的引用计数并增加q 的计数; 如果p 的计数变为0,则删除p 的现有内存。 |
p.unique() |
如果p.use_count() 是1则返回true,否则返回false。 |
p.use_count() |
返回与p 共享的对象数;这可能是一个慢速的操作,主要用于调试目的。 |
关于shared_ptr
更多信息请参考header <shared_ptr>。
动态分配对象的列表初始化
我们可以使用直接初始化的方式来初始化动态分配的对象。我们也可以使用传统构造的构造方式(使用括号)进行初始化。在新标准下,我们也可以使用列表初始化的方式(带花括号)进行初始化:
Code:
int *pi = new int(1024); // object to which pi points has value 1024
string *ps = new string(10, '9'); // *ps is "9999999999"
// vector with ten elements with values from 0 to 9
vector<int> *pv = new vector<int>{0,1,2,3,4,5,6,7,8,9};
我们还可以通过使用类型名称后跟一对空括号来初始化动态分配的对象:
Code:
string *ps1 = new string; // default initialized to the empty string
string *ps = new string(); // value initialized to the empty string
int *pi1 = new int; // default initialized; *pi1 is undefined
int *pi2 = new int(); // value initialized to 0; *pi2 is 0
对于定义了自己的构造函数的类类型(如string
),类型名称后面是否后跟一对空括号无关紧要;无论形式如何,对象都由默认构造函数进行初始化。在内置类型的情况下,类型名称后面是否后跟一对空括号的差异是显着的;内置类型的值初始化对象具有明确定义的值,但默认初始化对象不具有。同样,对于那些依赖于编译器默认生成的构造函数的类,类中的内置类型的成员没有在类体中初始化,那么这些成员也将未初始化。
与我们通常初始化变量的原因相同,初始化动态分配的对象也是一个好主意。
auto和动态分配
当我们在括号内提供初始化器时,我们可以使用auto
推导出我们想要从该初始化器中生成的对象的类型。但是,因为编译器使用初始化器的类型来推断要分配的类型,所以我们只能在括号内使用auto
和一个初始化器:
Code:
auto p1 = new auto(obj); // p points to an object of the type of obj
// that object is initialized from obj
auto p2 = new auto{a,b,c}; // error: must use parentheses for the initializer
p1
是一个指向由obj
通过auto
推导出的类型的指针。如果obj
是int
,则p1
是int *
;如果obj
是string
,则p1
是string *
;依此类推。新分配的对象由obj的值进行初始化。
unique_ptr类
unique_ptr
“拥有”它指向的对象。与shared_ptr
不同,一次只能有一个unique_ptr
指向给定对象。当unique_ptr
被销毁时,unique_ptr
指向的对象将被销毁。下表列出了unique_ptr
特有的操作。
操作 | 功能 |
---|---|
unique_ptr<T> u1 unique_ptr<T, D> u2
|
生成一个指向类型T 的空unique_ptr 。.u1 将使用delete 来释放它的指针; u2 将使用类型为D 的可调用对象来释放其指针。 |
unique_ptr<T, D> u(d) |
生成一个指向类型T 且使用d 的空unique_ptr 。d 必须是类型D 的对象而不是delete 。 |
u = nullptr u.release()
|
删除u 指向的对象,并将u 置位空。 |
u.reset() u.reset(q) u.reset(nullptr)
|
删除u 指向的对象。如果提供了内置指针q ,则指向该对象。 否则使u 为空。 |
与shared_ptr
不同,没有与make_shared
相类似的库函数返回unique_ptr
。相反,当我们定义unique_ptr
时,我们将它绑定到new
返回的指针,与shared_ptr
一样,我们必须使用直接初始化形式对其进行初始化:
Code:
unique_ptr<double> p1; // unique_ptr that can point at a double
unique_ptr<int> p2(new int(42)); // p2 points to int with value 42
因为unique_ptr
拥有它指向的对象,所以unique_ptr
不支持普通拷贝或赋值:
Code:
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
虽然我们无法拷贝或对unique_ptr
进行赋值,但我们可以通过调用release
或reset
将所有权从一个(nonconst
)unique_ptr
转移到另一个(nonconst
)unique_ptr
:
Code:
// 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
中的指针,并使unique_ptr
为null
。因此,p2
由存储在p1
中的指针值初始化,p1
变为空。
reset
成员接收一个可选指针,并重新定位unique_ptr
以指向给定指针。如果unique_ptr
不为null
,则删除unique_ptr
指向的对象。因此,对p2
进行的reset
调用释放了从“Stegosaurus”初始化的字符串所使用的内存,将p3
的指针传递给p2
,并使p3
为空。
调用release
会破坏unique_ptr
与其管理的对象之间的连接。通常由release
返回的指针用于初始化或分配另一个智能指针。在这种情况下,管理内存的责任只是从一个智能指针转移到另一个智能指针。但是,如果我们不使用另一个智能指针来保存从release
返回的指针,我们的程序将负责释放该资源:
Code:
p2.release(); // WRONG: p2 won't free the memory and we've lost the pointer
auto p = p2.release(); // ok, but we must remember to delete(p)
关于unique_str
更多信息请参考header <unique_str>。
weak_ptr类
weak_ptr
是一个智能指针,它不控制它指向的对象的生命周期。相反,weak_ptr
指向由shared_ptr
管理的对象。将weak_ptr
绑定到shared_ptr
不会更改该shared_ptr
的引用计数。一旦指向该对象的最后一个shared_ptr
消失,该对象本身将被删除。即使有指向它的weak_ptr
,该对象也将被删除。因此名称为weak_ptr
,它强调了weak_ptr
“弱”共享其对象的想法。
下表列出了weak_ptr
的相关操作:
操作 | 功能 |
---|---|
weak_ptr<T> w |
生成一个指向类型T对象的空weak_ptr 。 |
weak_ptr<T> w(sp) |
weak_ptr 指向与shared_ptr 对象sp 相同的对象。类型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() |
如果w.expired 为true,则返回一个空shared_ptr ;否则将返回一个指向w 指向的对象的shared_ptr 。 |
当我们创建一个weak_ptr
时,我们用shared_ptr
初始化它:
Code:
auto p = make_shared<int>(42);
weak_ptr<int> wp(p); // wp weakly shares with p; use count in p is unchanged
这里wp
和p
都指向同一个对象。由于共享较弱,创建wp
不会改变p
的引用计数;wp
点指向的对象可能会被删除。
因为对象可能不再存在,所以我们不能使用weak_ptr
直接访问其对象。要访问该对象,我们必须调用lock
。lock
函数检查weak_ptr
指向的仍然存在的对象。如果存在,lock
会将shared_ptr
返回给共享对象。与任何其他shared_ptr
一样,我们保证shared_ptr
指向的底层对象至少在shared_ptr
存在的情况下仍然存在。例如:
Code:
if (shared_ptr<int> np = wp.lock()) { // true if np is not null
// inside the if, np shares its object with p
}
这里,我们只在调用lock
成功时才进入if
的主体。在if
中,使用np
访问该对象是安全的。
关于weak_str
更多信息请参考header <weak_str>。
for范围声明不适用动态分配的数组
(array type)!所以不能对其使用for
范围声明!
动态数组的列表初始化
新标准下,我们可以使用列表初始化器来初始化一个动态数组:
Code:
// 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')};
当我们列出初始化内置数组类型的对象时,初始化器用于初始化数组中的第一个元素。如果初始值设定项的个数少于元素个数,则其余元素是值初始化的。如果初始值设定项多于给定的大小,那么new
表达式将失败,并且不会分配任何内存。在这种情况下,new
会抛出bad_array_new_length
类型的异常。与bad_alloc
一样,此类型在new
头中定义。关于头文件new
更多的信息可参考header <new>。
虽然我们可以使用空括号来值初始化数组的元素,但是我们不能在括号内提供元素初始值设定项。事实上,我们不能在括号内提供初始值,这意味着我们不能使用auto
来申请分配数组。
动态分配空数组是合法的
我们可以使用任意表达式来确定要分配的对象数量:
Code:
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
for (int* q = p; q != p + n; ++q)
/* process the array*/ ;
一个有趣的问题出现了:如果get_size
返回0会发生什么?答案是我们的代码正常工作。即使我们不能创建大小为0的数组变量,使用n
等于0调用new [n]
仍然是合法的:
Code:
char arr[0]; // error: cannot define a zero-length array
char *cp = new char[0]; // ok: but cp can't be dereferenced
当我们使用new
来分配一个大小为零的数组时,new
返回一个有效的非零指针。该指针与new
返回的任何其他指针不同。此指针用作零元素数组的非结束指针(off-the-end pointer)。我们可以通过一个非结束迭代器(off-the-end iterator)来使用这个指针。可以像在上述代码中的for
循环那样比较指针。我们可以在这样的指针上加零(或从中减去零),也可以减去其自身,得到零。 这种指针不能被解引用。毕竟,它指向的数组中没有元素。
在上述代码中,如果get_size
返回0,则n
也为0。对new
的调用将分配一个大小为零的数组。for
中的判定条件将失败(p
等于q + n
,因为n
为0)。因此,不执行for
循环体。
allocator::construct可以使用任何构造函数
allocator
分配的内存是未构造的。我们通过在该内存中构造对象来使用此内存。在新标准库中,construct
成员接收一个指针和零个或多个附加参数,它在给定位置构造一个元素,附加参数用于初始化正在构造的对象。与make_shared
的参数一样,这些附加参数必须是正在构造的类型的对象的有效初始值设定项。特别是,如果对象是类类型,则这些参数必须与该类的构造函数匹配:
Code:
auto q = p; // q will point to one past the last constructed element
alloc.construct(q++); // *q is the empty string
alloc.construct(q++, 10, 'c'); // *q is cccccccccc
alloc.construct(q++, "hi"); // *q is hi!
在早期版本的标准库中,construct
只接收两个参数:构造对象的指针和元素类型的值。因此,我们只能将一个元素复制到未构造的空间中,我们不能对元素类型使用任何其他构造函数。
使用尚未构造对象的原始内存是错误的:
Code:
cout << *p << endl; // ok: uses the string output operator
cout << *q << endl; // disaster: q points to unconstructed memory!
注:我们必须construct
对象以便使用allocate
返回的内存。以其他方式使用未构造的内存是未定义的。
当我们完成对象的使用时,我们必须销毁我们构造的元素,我们通过在每个构造的元素上调用destroy
来实现。destroy
函数接受一个指针并在指向的对象上运行析构函数:
Code:
while (q != p)
alloc.destroy(--q); // free the strings we actually allocated
在循环开始时,q
指向最后一个构造元素的后一位置。我们在调用destroy
之前递减q
。因此,在第一次调用调用destroy
时,q
指向最后构造的元素。我们在最后一次迭代中destroy第一个元素,之后q
将等于p
并且循环结束。
一旦元素被destroy,我们可以重用内存来保存其他字符串或将内存返回给系统。我们通过调用deallocate
来释放内存:
alloc.deallocate(p, n);
我们传递给deallocate
的指针不能为null
;它必须指向由allocate
分配的内存。而且,传递给deallocate
的size
参数必须与调用allocate
中使用的大小相同才能获得指针所指向的内存。
关于allocator::construct
的更多信息可参考std::allocator::construct。
参考文献
[1] Lippman S B , Josée Lajoie, Moo B E . C++ Primer (5th Edition)[J]. 2013.
网友评论