理解为什么需要智能指针
智能指针是一种预防内存泄漏的方法,智能指针在C++没有垃圾回收器环境下,可以很好的解决异常安全等带来的内存泄漏问题
C语言指针存在的缺陷
1. 裸指针在声明的时候没有指出,裸指针指向的是单个对象还是一个数组
2. 裸指针在声明中没有提示在使用完其指向的对象后,是否需要析构它
3. 如果确定需要析构,应该采用怎样的析构方法仍然是一个问题,是使用delete呢?还是需要特定函数来完成呢?
4. 如果确定应该使用delete的方式进行析构,是使用delete呢,还是使用delete []呢?
5. 如果确定指针拥有其指向的对象,并且也知道如何将对象析构,如何保证析构操作在所有的代码路径执行且只执行一次呢?
6. 没有什么正规的方法可以可测出指针是否空悬(dangle)
理解RAII
RAII是一种利用对象生命周期来控制程序资源的简单技术,在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理资源的责任托管给一个对象,这样有两个好处:1)不需要显式地释放资源;2)采用这种方式,对象所需的资源在其生命期内始终保持有效。
智能指针的原理
1) RAII原理
2) 重载operator*和operator->,具有指针一样的行为
常用的智能指针
C++11中有unique_ptr、shared_ptr以及weak_ptr等智能指针,定义在memory头文件中,可以对资源进行管理,保证任何情况下,已构造的对象最终会被销毁,即他的析构函数最终会被调用
auto_ptr介绍
- 永远不要使用auto_ptr,因为在auto_ptr的年代没有移动语义,作为变通,auto_ptr使用赋值操作完成移动任务,在赋值,参数传递的时候会转移所有权
- auto_ptr不能共享所有权,即不要让两个auto_ptr指向同一个对象
- auto_ptr不能指向数组,因为auto_ptr在析构的时候只是调用delete,而数组应该要调用delete[]
- auto_ptr只是一种简单的智能指针,如有特殊需求,需要使用其他智能指针,比如share_ptr
- auto_ptr不能作为容器对象,STL容器中的元素经常要支持拷贝,赋值等操作,在这过程中auto_ptr会传递所有权
shared_ptr介绍
- shared_ptr实现了任意资源在共享所有权语义下进行生命周期管理的垃圾回收
- 与unique_ptr相比,shared_ptr的尺寸通常是裸指针尺寸的两倍,他还会带来控制块的开销。
- 控制块带来的开销包括:引用计数的内存必须动态分配(使用make_shared可以避免)、引用计数的递增和递减必须是原子操作、使用到了继承以及虚函数
- 默认的资源析构通过delete运算符进行,但同时也支持定制删除器。删除器的型别对shared_ptr的型别没有影响
- 避免使用裸指针变量来创建shared_ptr
- shared_ptr的构造函数并不会一定导致引用计数的增加,比如移动构造函数以及移动赋值函数
- shared_ptr支持自定义析构器,自定义析构器不是shared_ptr类型的一部分,而对于unique_ptr而言,析构器是其类型的一部分
- 自定义析构器不会改变shared_ptr的尺寸,无论析构器是怎样的类型,shared_ptr的大小都是裸指针的两倍
- 控制块内容包括:引用计数、自定义析构器、自定义内存分配器以及弱引用计数等
10.创建控制块的时机:
a. std::make_shared总是构建一个控制块
b. 从具备专属所有权的指针出发构造一个shared_ptr
c. 当shared_ptr构造函数使用裸指针作为实参时 - 在使用默认析构器和内存分配器的前提下,并且shared_ptr是由make_shared创建的前提下,控制块的尺寸只有三个字长,并且分配操作实质上没有任何成本,提领一个shared_ptr并不比提领一个裸指针话费更多,当进行引用计数操作的时候需要一个或者两个原子操作,但这些操作通常会映射到单个机器指令,尽管与非原子化指令相比成本高点,但仍然是单指令。控制块中的虚函数机制通常只被每个托管给shared_ptr的对象使用一次:在该对象被析构的时候。
- shared_ptr不支持对应的数组类型
- shared_ptr支持派生类到基类的转换
int main()
{
shared_ptr<int> sp1(new int(22));
shared_ptr<int> sp2 = sp1;
cout << "count: " << sp2.use_count() << endl; //打印引用计数
cout << *sp1 << endl; // 22
cout << *sp2 << endl; // 22
sp1.reset(); //显式让引用计数减1
cout << "count: " << sp2.use_count() << endl; //打印引用计数
cout << *sp2 << endl; // 22
return 0;
}
unique_ptr介绍
- unique_ptr可以做auto_ptr的任何功能,并且他的执行效率和auto_ptr一样高,最后unique_ptr使用移动操作代替复制操作,因此应该完全使用unique_ptr替换auto_ptr
- std::unique_ptr是小巧、高速、具备只移型别的智能指针,每当你需要使用智能指针的时候,unique_ptr基本上应该是首选,可以认为unique_ptr和裸指针有着一样的大小和速度
- unique_ptr对托管资源实施专属所有权语义,unique_ptr持有对对象的独有权(即不支持复制和赋值,比auto_ptr好,直接赋值会编译出错),同一时刻只能有一个unique_str指向给定对象(通过禁止拷贝语义,而只有移动语义来实现)
- 默认情况下,资源析构采用delete运算符实现,但可以指定自定义删除器。有状态的函数对象和采用函数指针实现的删除器会增加unique_ptr的对象大小,无状态的函数对象(包括无捕获的lambda表达式)不会增加unique_ptr的大小
- 将std::unique_ptr转换为std::shared_ptr可以使用shared_ptr的构造函数,非常方便。即返回unique_ptr的工厂函数既可以提供专属所有权语义,又可以提供共享所有权语义。
- 常用于工厂模式以及Pimpl惯用法
std::unique_ptr<int> p1(new int(5));
std::unique_ptr<int> p2 = p1; // 编译会出错
std::unique_ptr<int> p3 = std::move(p1); // 转移所有权, 现在那块内存归p3所有, p1成为无效的指针.
#include <iostream>
#include <memory>
using namespace std;
int main()
{
unique_ptr<int> up1(new int(11)); // 无法复制的unique_ptr
//unique_ptr<int> up2 = up1; // err, 不能通过编译
cout << *up1 << endl; // 11
unique_ptr<int> up3 = move(up1); // 现在p3是数据的唯一的unique_ptr
cout << *up3 << endl; // 11
//cout << *up1 << endl; // err, 运行时错误
up3.reset(); // 显式释放内存
up1.reset(); // 不会导致运行时错误
//cout << *up3 << endl; // err, 运行时错误
unique_ptr<int> up4(new int(22)); // 无法复制的unique_ptr
up4.reset(new int(44)); //"绑定"动态对象
cout << *up4 << endl;
up4 = nullptr;//显式销毁所指对象,同时智能指针变为空指针。与up4.reset()等价
unique_ptr<int> up5(new int(55));
int *p = up5.release(); //只是释放控制权,不会释放内存
cout << *p << endl;
//cout << *up5 << endl; // err, 运行时错误
delete p; //释放堆区资源
return 0;
}
weak_ptr介绍
- weap_ptr不是一种独立的智能指针而是shared_ptr的辅助工具
- expired方法可以判断是否失效
- 从weak_ptr获取shared_ptr
a. weak_ptr::lock()会返回一个shared_ptr,如果此时weak_ptr已经失效,则shared_ptr为空
b. 以weak_ptr为参数调用shared_ptr的构造方法,如果此时weak_ptr已经失效,则会抛出异常 - weak_ptr和shared_ptr从本质上讲是一致的,他们有相同的尺寸,他们使用同一控制块,weak_ptr的构造、析构以及赋值操作都包含了对引用计数的原子操作
- 控制块中有两个引用计数,其中一个为所有shared_ptr共享,一个为所有weak_ptr共享
weak_ptr应用
1.设计提供缓存功能的简单工厂模式(缓存有过期功能,缓存的有效期取决于用户在获取到智能指针后的作用范围)
2. 观察者列表(观察者被销毁后,主题能够有方法获取此信息)
3. 使用weak_ptr来打破shared_ptr的循环引用(在类似树这种有着严格继承谱式的数据结构中使用unique_ptr和普通指针就好)
4. 总结起来:weak_ptr的使用场景适合为“类似shared_ptr但是可能为空的场景”
weak_ptr
weak_ptr是为配合shared_ptr而引入的一种智能指针,它可以从一个shared_ptr或另一个weak_ptr对象构造,它的构造和析构不会引起引用计数的增加或减少。没有重载 * 和 -> 但可以使用lock获得一个可用的shared_ptr对象
weak_ptr的使用更为复杂一点,它可以指向shared_ptr指针指向的对象内存,却并不拥有该内存,而使用weak_ptr成员lock,则可返回其指向内存的一个share_ptr对象,且在所指对象内存已经无效时,返回指针空值nullptr。
C++11或boost的weak_ptr,弱引用。 引用计数有一个问题就是互相引用形成环,这样两个指针指向的内存都无法释放。需要手动打破循环引用或使用weak_ptr。顾名思义,weak_ptr是一个弱引用,只引用,不计数。如果一块内存被shared_ptr和weak_ptr同时引用,当所有shared_ptr析构了之后,不管还有没有weak_ptr引用该内存,内存也会被释放。所以weak_ptr不保证它指向的内存一定是有效的,在使用之前需要检查weak_ptr是否为空指针。
void check(weak_ptr<int> &wp)
{
shared_ptr<int> sp = wp.lock(); // 转换为shared_ptr<int>
if (sp != nullptr)
{
cout << "still " << *sp << endl;
}
else
{
cout << "pointer is invalid" << endl;
}
}
int main()
{
shared_ptr<int> sp1(new int(22));
shared_ptr<int> sp2 = sp1;
weak_ptr<int> wp = sp1; // 指向shared_ptr<int>所指对象
cout << "count: " << wp.use_count() << endl; //打印计数器
cout << *sp1 << endl; // 22
cout << *sp2 << endl; // 22
check(wp); // still 22
sp1.reset();
cout << "count: " << wp.use_count() << endl;
cout << *sp2 << endl; // 22
check(wp); // still 22
sp2.reset();
cout << "count: " << wp.use_count() << endl;
check(wp); // pointer is invalid
return 0;
}
make_unique以及make_shared的优劣势
优势
- 避免代码重复(避免重复撰写类型)
- 保证异常安全
- make系列函数有性能提升,原因是make函数会让编译器有机会使用更简洁的数据结构产生更小更快的代码
a.使用new会产生两次内存分配,而使用make系列函数只会产生一次内存分配,减小程序的静态尺寸,同时增加可执行代码的运行速度,犹有进者,使用make系列函数能够避免控制块中一些薄记信息的必要性,这样也能潜在地减少程序的内存痕迹总量。
劣势
- 所有make函数不允许使用自定义析构器
- 在make系列函数中对形参进行完美转发的代码使用的是圆括号而不是花括号, 即不会优先使用列表初始化
使用make_shared额外劣势
- 如果类自定义类operator new以及operator delete,那么不适合使用make系列函数
- 内存紧张的系统、非常大的对象并且存在比指向到相同对象的shared_ptr生存期更久的weak_ptr