auto_ptr与unique_ptr
auto_ptr与unique_ptr都是独占所有权的智能指针类型,前者由C++98引入,而后者则是由C++11新引入并且指明要用unique_ptr代替auto_ptr。理由主要是以下两点:
-
auto_ptr的复制语义直接将所有权移交给新的auto_ptr对象不符合直觉,而这也使得在STL容器中是没法安全存放auto_ptr对象的,因为STL中的实现都是假定元素是可以安全复制的,比如«effective STL»中举的一个例子就是STL中的sort算法的实现可能是快速排序(quicksort),快排中每一次分区都会选出一个主元素(pivot)复制给临时变量,再对所有元素进行分区,这样就会造成每次选出的主元素被意外析构。
而unique_ptr却得益于C++11引入的移动语义,并且实现了无异常的移动语义(STL中容器在编译阶段会判断元素的移动构造或移动赋值函数是否是无异常的,因为只有无异常的移动语义才能保证STL的强异常安全),于是可以安全地作为STL元素使用。 -
auto_ptr只支持单对象的指针,不支持指针数组,因为auto_ptr的析构函数调用的是delete。而unique_ptr可以传入自己的删除器(deleter)来控制析构过程。
优化unique_ptr的内存空间
默认情况下unique_ptr的大小就等于原始指针的大小,比如在64位系统上就是8个字节。
#include <iostream>
#include <memory>
using namespace std;
int main() {
unique_ptr<int> pi(new int);
cout << sizeof(pi) << endl;
return 0;
}
这里会输出8(字节)
但是如果自己传入自定义的删除器(deleter)函数,则会增加一个指针大小,因为unique_ptr对象还要保存删除器函数指针。
#include <iostream>
#include <memory>
using namespace std;
void deleter(int *pi) noexcept {
delete pi;
}
int main() {
unique_ptr<int, decltype(deleter) *> pi(new int, deleter);
cout << sizeof(pi) << endl;
return 0;
}
这里会输出16(字节)
但是也有另外的方法来优化自定义删除器(deleter)占用的空间,比如使用lambda函数,不过需要lambda函数的捕捉列表为空,这是因为lambda的底层实现是仿函数,当捕捉列表为空也就意味着该仿函数对应的类成员变量为空,于是当不包含成员变量的lambda对象作为unique_ptr的成员变量时编译器就会优化掉其占用的空间。
#include <iostream>
#include <memory>
using namespace std;
int main() {
auto deleter = [](int *pi) { delete pi; };
unique_ptr<int, decltype(deleter)> pi(new int, deleter);
cout << sizeof(pi) << endl;
return 0;
}
这里会输出8(字节)
shared_ptr
shared_ptr通过引用计数实现了更强大的资源管理能力,每当新增一个shared_ptr指向资源时,计数加一;每当销毁一个指向资源的shared_ptr时计数减一,当计数归零时自动释放资源。
一般引用计数的实现是通过引入额外的数据结构来保存相关信息(强引用计数,弱引用计数,删除器等),再让shared_ptr保存指向资源和额外数据结构的指针即可:
这里强引用计数(reference count)就是资源的引用计数;而弱引用计数(weak count)和下面的weak_ptr相关,暂时不用管。
使用make_shared创建shared_ptr
在«effective modern c++»中提到,使用make_shared创建shared_ptr比直接通过shared_ptr的构造函数或者reset函数要更高效:
#include <iostream>
#include <memory>
using namespace std;
int main() {
shared_ptr<int> spi1(new int);
auto spi2 = make_shared<int>();
int hold;
cin >> hold;
return 0;
}
这里spi1是要调用两次内存分配的,而make_shared却可以只进行一次内存分配:
就是直接一次性申请出控制块和资源对象内存空间之和,然后再调用就地构造(placement new)构造出资源对象。
其次make_shared在一定程度更支持异常安全,比如在«effective c++»和«effective modern c++»中都提到的,当在传入函数参数的时候构造智能指针时,是可能会造成资源泄漏的:
#include <iostream>
#include <memory>
#include <exception>
using namespace std;
bool priority() {
throw exception();
return true;
}
void func(shared_ptr<int> spi, bool flag) {
cout << *spi << endl;
cout << flag << endl;
}
int main() {
func(shared_ptr<int>(new int), priority());
int hold;
cin >> hold;
return 0;
}
其实还是那个问题,就是C和C++都没有规定函数参数的执行顺序,这里参数传递主要涉及三个步骤:
- new int
- shared_ptr<int>()
- priority
这里能确定的是1发生在2之前,但3却不确定,所以当3出现在1和2的中间,抛出异常就会造成内存泄漏。
而make_shared则没有这个问题。
weak_ptr
weak_ptr算是shared_ptr的一个补丁,可以用于解决shared_ptr的一些缺陷。weak_ptr和shared_ptr不同,并不拥有资源的所有权,只是资源的一个观察者,可以通过shared_ptr和weak_ptr创建新的weak_ptr,同时将控制块中的弱引用计数(weak count)加一。
强引用计数和弱引用计数的却别就是,当强引用计数归零时,将自动释放资源,而当之后弱引用计数也归零时才会释放控制块占用的内存资源。
- 可以用weak_ptr作为资源的智能缓存
shared_ptr<int> factory() {
static weak_ptr<int> wpi;
return wpi.lock();
}
这样可以实现当没有shared_ptr占用资源的时候可以自动释放资源,并且可以通过weak_ptr检测出来资源是否已被释放(expire)。