一、new和delete
C++定义了两个运算符来分配和释放动态内存。运算符new分配内存,delete释放new分配的内存。相对于智能指针,使用这两个运算符管理内存非常容易出错。
在新标准下,使用new分配动态内存时,可使用{}来初始化对象。
出于与变量初始化相同的原因,对动态分配的对象进行初始化通常是个好主意。一个动态分配的const对象必须进行初始化。
虽然不能创建一个大小为0的静态数组对象,但当n等于0时,调用new[n]是合法的,即:
char arr[0]; // 错误 不能定义长度为0的数组
char *cp = new char[0]; // 正确 但cp不能解引用
当我们用new分配一个大小为0的数组时,new返回一个合法的非空指针,此指针就像尾后指针一样。
释放一块并非new分配的内存,或者将相同的指针释放多次,其行为是未定义的。
当我们delete一个指针后,指针值就变为无效。虽然指针已经无效,但在很多机器上指针仍然保存着(已经释放了的)动态内存的地址。在delete之后,指针就变成了空悬指针(dangling pointer),即指向一块曾经保存数据对象但现在已经无效的内存的指针。如果我们需要保留指针,可以在delete之后将nullptr赋予指针,这样就清楚地指出指针不指向任何对象。
二、分配器
new有一些灵活性上的局限,其中一方面表现在它将内存分配和对象构造组合在一起。类似的,delete将对象析构和内存释放组合在一起。我们分配单个对象时,通常希望将内存分配和对象初始化组合在一起。但当分配一大块内存时,我们通常计划在这块内存上按需构造对象。在此情况下,我们希望将内存分配和对象构造分离。这意味着我们可以分配大块内存,但只在真正需要时才真正执行对象创建操作(同时付出一定开销)。
STL的allocator类定义在memory头文件中,它帮助我们将内存分配和对象构造分离开来。它提供一种类型感知的内存分配方法,它分配的内存是原始的、未构造的。
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指向的对象执行析构函数
为了使用allocate返回的内存,我们必须用construct构造对象。使用未构造的内存,其行为是未定义的。
我们只能对真正构造了的元素进行destroy操作。一旦元素被销毁后,就可以重新使用这部分内存来保存其他string,也可以将其归还给系统。
STL还为allocator类定义了两个伴随算法,uninitialized_copy(),uninitialized_fill(),可以在未初始化内存中创建对象,其在给定目的位置创建元素,而不是由系统分配内存给它们。
三、智能指针
为了更容易(同时也更安全)地使用动态内存,新的STL提供了智能指针类型来管理动态对象。智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。智能指针几乎可以做到任何裸指针能做到的事情,但是犯错的机会却大大减小。为了使用智能指针,必须包含memory头文件。
智能指针的使用方式与普通指针类似:解引用一个智能指针返回它指向的对象。如果在一个条件判断中使用智能指针,效果就是检测它是否为空。
默认情况下,一个用来初始化智能指针的普通指针必须指向动态内存,因为智能指针默认使用delete释放它所关联的对象。
程序使用动态内存出于以下3种原因之一:
(1)程序不知道自己需要使用多少对象;
(2)程序不知道所需对象的准确类型;
(3)程序需要在多个对象间共享数据(常见原因)。
C++11中共有4种智能指针:auto_ptr,unique_ptr,shared_ptr,weak_ptr,所有这些智能指针都是为管理动态分配对象的生命期而设计的。
(0)auto_ptr,是从C++98中残留下来的弃用特性,不再使用
(1)unique_ptr
(2)shared_ptr
(3)weak_ptr
此外,在Boost库中,还有(4)scoped_ptr智能指针,一般用作没有unique_ptr情况下的替代品。
- unique_ptr
每当需要使用智能指针时,unique_ptr基本上总是首选。你甚至可以在内存和时钟周期紧张的情况下使用unique_ptr。
一个非空unique_ptr总是拥有其所指向的资源。移动一个unique_ptr会将所有权从源指针移至目标指针(源指针被置空)。即:
unique_ptr<string> p1(new string("Stegosaurus"));
unique_ptr<string> p2(p1); // 错误 不支持复制构造
unique_ptr<string> p3;
p3 = p2; // 错误 不支持赋值
unique_ptr<T> u;
u = nullptr; // 释放u指向的对象 将u置为空
u.release(); // u放弃对指针的控制权 返回指针 并将u置为空
u.reset(); // 释放u指向的对象
u.reset(q); // 令u指向内置指针q 并释放原对象
u.reset(nullptr); // 释放u指向的对象
unique_ptr不支持普通的拷贝或赋值操作。虽然我们不能拷贝或赋值unique_ptr,但可以通过调用release或reset将指针的所有权从一个(非const)unique_ptr转移给另一个unique_ptr,即:
unique_ptr<string> p2(p1.release()); // 相当于拷贝
unique_ptr<string> p3(new string("Trex"));
p2.reset(p3.release()); // 相当于赋值
release返回的指针通常被用来初始化另一个智能指针或给另一个智能指针赋值。但如果我们不用另一个智能指针来保存release返回的指针,程序就要负责资源的释放,而reset才是用于主动释放指针资源的函数,即:
p2.release(); // 错误 p2不会释放内存 且丢失指针
auto p = p2.release(); // 正确 但必须记得delete p
不能拷贝unique_ptr的规则也有一个特例:我们可以拷贝或赋值一个将要被销毁的unique_ptr。最常见的例子是从函数返回一个unique_ptr,即:
unique_ptr<int> clone(int p) {
return unique_ptr<int>(new int(p));
}
如果函数使用new分配内存,并返回指向该内存的指针,将其返回类型声明为unique_ptr是好的选择。当确定传递给函数智能指针后,不再需要该智能指针时,也可将函数的形参定义成unique_ptr来进行传递,即:
int process(unique_ptr<int> p) {
return *p++;
}
unique_ptr<int> p1(new int(1));
int res = process(p1);
unique_ptr可以方便高效的转换为shared_ptr,正是这一特性使得其适合作为工厂函数的返回类型。工厂函数并不知道调用者是对其返回的对象采取专属所有权好,还是共享所有权好。通过返回一个unique_ptr,工厂函数就向调用者提供了最高效的智能指针,但它又不会阻止调用者把返回值转换成更具弹性的shared_ptr。
可以由unique_ptr创建shared_ptr,但不能由shared_ptr创建unique_ptr,即使shared_ptr的引用数为1。
unique_ptr支持对动态数组类型进行管理,但大多数应用应该使用STL中的容器而不是动态分配的数组。使用容器更为简单、更不容易出现内存管理错误并且可能有更好的性能。唯一合理使用unique_ptr<T[]>的情景是使用一个C风格的API,其返回了堆上的裸指针,且指定了其指涉对象的所有权。
当一个unique_ptr指向一个数组时,我们不能使用点和箭头成员运算符。毕竟unique_ptr指向的是一个数组而不是单个对象,因此这些运算符是无意义的。另一方面,当一个unique_ptr指向一个数组时,我们可以使用下标运算符来访问数组中的元素,即:
for (size_t i=0; i != 10; ++i) {
up[i] = i; // 为每个元素赋予一个新值
}
- shared_ptr
如果程序要使用多个指向同一个对象的指针,应选择shared_ptr。这样的情况包括:
(1)有一个指针数组,并使用一些辅助指针来标识特定的元素,如最大元素和最小元素;
(2)两个对象包含都指向第三个对象的指针;
(3)STL容器包含的指针。
当指向一个对象的最后一个shared_ptr被销毁时,shared_ptr类会自动销毁此对象。如果你将shared_ptr存放于一个容器中,而后不再需要全部元素,而只使用其中一部分,要记得用erase删除不再需要的那些元素。
复制一个shared_ptr会递增其引用计数;将一个shared_ptr赋予另一个shared_ptr会递增赋值号右侧shared_ptr的引用计数,而递减左侧shared_ptr的引用计数。如果一个shared_ptr的引用计数变为0,它所指向的对象会自动销毁。由于采用引用计数,对其性能会造成一些影响。
shared_ptr对象只能通过复制其值来共享所有权,如果从同一非共享指针构造两个shared_ptr,则它们都将拥有该指针而不是共享它,此时会引起潜在的获取问题。
我们不能将一个内置指针隐式转换为一个智能指针,必须使用直接初始化形式,即:
shared_ptr<int> p1 = new int(1024); // 错误
shared_ptr<int> p2(new int(1024)); // 正确
定义和改变shared_ptr的方法:
T *q = new T;
shared_ptr<T> p(q); // p管理内置指针q所指向的对象
// 从unique_ptr转换为shared_ptr
auto u = make_unique<T>();
shared_ptr<T> p(u); // 从unique_ptr的u那里接管对象的所有权,并将u置空
size_t num = p.use_count(); // 返回持有相同目标的shared_ptr共有多少个
// 一般只会使用p.unique检查是否是唯一持有者,该函数基本不会用到
p.reset(); // 若p是唯一指向其对象的shared_ptr,reset会释放该对象
p.reset(q); // 当reset中传递有参数q时,会令shared_ptr指向q
p.reset(q, d); // 若还传递了参数d时,会调用d而不是delete来释放q
一般传递给函数的智能指针参数为shared_ptr类型,此时函数调用会复制该智能指针,增加其引用计数。当函数调用结束后,引用计数会减少,但不会变为0,因此,该智能指针不会被释放,依然可以使用。
reset函数通常与unique函数一起使用,来控制多个shared_ptr共享的对象。在改变底层对象之前,我们检查自己是否是当前对象仅有的用户。如果不是,在改变前要制作一份新的拷贝,即:
if (!p.unique()) {
p.reset(new string(*p)); // 我们不是唯一用户 要分配新拷贝
}
*p += newVal; // 现在我们是唯一用户 可以改变其对象的值
通常,存储的指针和拥有的指针说的是同一对象,但是别名(alias)共享的对象(即使用alias构造和复制的对象)可能指向不同的对象。
需注意,空的shared_ptr不一定是null shared_ptr,null shared_ptr也不一定是空的shared_ptr。
对shared_ptr可通过static_pointer_cast,dynamic_pointer_cast,const_pointer_cast,3个函数进行相应的static_cast,dynamic_cast,const_cast类型转换:如果该类型转换可以正常进行,当原sp不为空时,返回新类型的对象,其共享sp资源的所有权,引用计数加一。如果sp为空时,则返回对象也为空shared_ptr。即:
foo = std::make_shared<A>();
bar = std::static_pointer_cast<B>(foo);
只有shared_ptr智能指针可以进行类型转换。
由于shared_ptr构造时会创建一控制块,使用时尽可能避免将裸指针传递给shared_ptr的构造函数,常用的替代方法是使用make_shared。但如果使用自定义析构器时,会无法使用make_shared,这时必须将一个裸指针传递给shared_ptr的构造函数,此时应直接传递new运算符的结果,而不是裸指针变量。即:
shared_ptr<Widget> spw1(new Widget, logginDel);
shared_ptr只能用于处理指向单个对象的指针,没有shared_ptr<T[]>实现。
- weak_ptr
对于需要类似shared_ptr但有可能空悬的指针时使用weak_ptr。weak_ptr是一种不控制所指对象生存期的智能指针,它指向由一个shared_ptr管理的对象。weak_ptr不能解引用,也不能检查是否为空,这是因为weak_ptr并不是一种独立的智能指针,而是shared_ptr的一种扩充。其一般是通过shared_ptr来创建的。将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放。即使有weak_ptr指向对象,对象也还是被销毁。即:
auto sp = make_shared<T>();
weak_ptr<T> w(sp); // 创建于shared_ptr sp指向相同对象的weak_ptr
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的空悬检测
if (w.expired()) ...
由于对象可能不存在,我们不能使用weak_ptr直接访问对象,而必须调用lock。此函数检查weak_ptr指向的对象是否仍存在。如果存在,lock返回一个指向共享对象的shared_ptr。与任何其他shared_ptr类似,只要该shared_ptr存在,它所指向的底层对象也就会一直存在。即:
if (shared_ptr<int> np = wp.lock()) {
// 此时np与p共享对象
}
weak_ptr可能的用武之地包括缓存,观察者列表,以及避免shared_ptr的指针环路。
- scoped_ptr
scoped_ptr是Boost库中一个类似于auto_ptr的智能指针,包装了new操作符在堆上分配的动态对象,能够保证动态创建的对象在任何时候都可以被正确的删除。但是scoped_ptr的所有权更加严格,只能在定义的作用域内使用,不能转让。一旦scoped_pstr获取了对象的管理权,你就无法再从它那里取回来。
scoped_ptr同时把复制构造函数和赋值操作都声明为私有,禁止对智能指针的复制操作,保证了被它管理的指针不能被转让所有权。其有成员函数reset()的功能是重置scoped_ptr;它删除原来持有的指针,再保存新的指针值p。如果p是空指针,那么scoped_ptr将不能持有任何指针。一般情况下reset()不应该被调用,因为它违背了scopd_ptr的本意。
实际上拥有权不可转移不够方便,swap()成员函数可以交换两个scopd_ptr保存的原始指针。即:
scoped_ptr<int> sp (new int(10)); // 构造scoped_ptr
scoped_ptr<int> sp2;
sp2 = sp; // 错误 不允许赋值
swap(sp, sp2); // 对两个scoped_ptr进行交换操作
四、智能指针注意事项
最安全的分配和使用动态内存的方法是调用make系列函数。make系列函数有3个:make_unique,make_shared,allocate_shared,该系列函数会把一个任意实参集合完美转发给动态分配内存的对象构造函数,并返回一个指向该对象的智能指针。主要是使用make_unique(在C++14中才添加的新特征)和make_shared。此函数在动态内存中分配一个对象并初始化它,返回指向此对象的智能指针。通常使用auto类型来表示由make系列函数生成的智能指针。即:
#include <iostream>
#include <memory>
int main () {
std::shared_ptr<int> foo = std::make_shared<int> (10);
// same as:
std::shared_ptr<int> foo2 (new int(10));
auto bar = std::make_shared<int> (20);
auto baz = std::make_shared<std::pair<int,int>> (30,40);
std::cout << "*foo: " << *foo << '\n';
std::cout << "*bar: " << *bar << '\n';
std::cout << "*baz: " << baz->first << ' ' << baz->second << '\n';
return 0;
}
在C++11中,make_unique的实现可用以下代码代替:
template<typename T, typename... Ts>
std::unique_ptr<T> make_unique(Ts&&...params){
return std::unique_ptr<T>(new T(std::forward<Ts>(params)...));
}
优先使用make系列函数有3个好处:
(1)使用make系列函数的代码更简洁;
(2)使用make系列函数是异常安全的;
(3)使用make系列函数编译产生的代码更小,速度更快。
但不是所有情况都能使用make系列函数的,此时只能使用智能指针的构造器,包括:
(1)make系列函数不允许使用自定义析构器;
(2)在使用make系列函数创建对象并初始化时,只能使用()进行初始化,而不能使用{}初始化,如果需要使用{}初始化智能指针时,则无法使用。
当不能使用make系列函数创建智能指针时,保证异常安全的最好做法是直接使用new表达式时,立即将该表达式的结果传递给智能指针的构造函数,并在这条语句里不做其他任何事。
智能指针定义了一个名为get的函数,它返回一个内置指针,指向智能指针管理的对象。此函数是为了这样一种情况设计的:我们需要向不能使用智能指针的代码传递一个内置指针。不能对使用get返回的指针使用delete。使用一个内置指针来访问一个智能指针所负责的对象是很危险的,因为我们无法知道对象何时会被销毁。将另一个智能指针绑定到get返回的指针上也是错误的。
为了正确使用智能指针,我们还必须坚持一些基础规范:
(1)不使用相同的内置指针初始化(或reset)多个智能指针,而是使用make系列函数;
(2)不delete get()返回的指针;
(3)不使用get()初始化或reset另一个智能指针;
(4)如果你使用get()返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变为无效了;
(5)如果使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器;
(6)智能指针不支持指针算术操作。