std 智能指针
智能指针的选择标准
在类中使用智能指针作为成员变量。需要根据实际情况(主要是看所有权)
原始指针:
-
所有权:该资源对象不属于我,
-
使用前提:资源对象被释放前,我理应必然已经被释放。否则会存在风险。
unique_ptr:
-
所有权:该对象仅仅属于我。
-
被管理的资源对象的生命周期,取决于他所属的唯一一个引用的寿命。
shared_ptr:
-
所有权:
-
该资源由多个对象共享
-
或者 虽然该对象仅仅属于我,但有使用 weak_ptr 的需要。
可用于函数传参
-
-
被管理的资源对象的生命周期,取决于所有引用中,最长寿的那一个。
weak_ptr:
-
所有权:该资源对象不属于我
-
该资源对象释放后,我仍可能不被释放时。
智能指针全家桶
相较于裸指针,智能指针对裸指针封装的初衷,是无需手动释放内存。
auto_ptr
auto_ptr 是c++ 98定义的智能指针模板,已经被C++11抛弃------支持复制运算符重载。
用于管理指针的对象,当对象过期时其析构函数将使用delete 来释放内存。
- 复制或者赋值都会改变资源的所有权
- 在STL容器中使用auto_ptr存在着重大风险,因为容器内的元素必须支持可复制和可赋值
- 不支持对象数组的内存管理
unique_ptr
使用 unique_ptr 最主要的特点:对资源是独占的,防止多个智能指针指向同一个对象。
-
更安全
- 不支持复制运算符重载,不允许复制。
- 支持移动语义 move,完美转发 forward
- 因为独占,所以压根就不存在shared_ptr循环引用的问题
-
更好的支持数组。
std::unique_ptr<T[]>
:为数组提供自动内存管理的智能指针,可用于管理动态分配的数组的内存释放。 -
通过在析构函数中释放资源来管理对象的生命周期,来自动管理资源。
shared_ptr
资源&控制块的生命周期
被管理的资源对象的生命周期:取决于所有 shared_ptr(强引用)中最长寿的那一个。
控制块的生命周期:
- 所有的强引用和弱引用全都析构后,控制块才会删除。
- 因为哪怕是资源析构掉,也要满足wp查询是否资源还存在的需求。
make_shared
make_shared内存布局更加紧凑,避免内存碎片化。
若使用shared_ptr<Person> (new X)
:控制块的地址与被控制资源的地址 分离。
耗费资源高
耗费资源高,不能代替 unique_ptr
-
需要维护一个 atomic 的引用计数器,效率低,需要额外的一块管理内存,访问实际对象需要二级指针
-
而且 deleter 使用了类型擦除技术。
线程安全
对于这个问题,通常的回答我觉得答非所问:即 sharedptr 安全又不安全:
-
引用计数(控制块中的计数)线程安全
资源的生命周期是线程安全的(这就足够啦)。
-
访问被指向的资源不是线程安全的(请用
atomic_shared_ptr
)答非所问,人家的本职工作就是解决生命周期的问题。
这个附加要求是在此之前的所有指针的通病。要解决它就必须引入新的复杂度。
如果想要支持这个需求2
-
在设计资源本身时下功夫
-
新写一个智能指针 支持指针控制资源的多线程修改,解决这个访问资源的竞争问题
atomic_shared_ptr
反正不要影响sharedptr的复杂度和运行时效率,重温一下 cpp语言的准则:你不需要为不需要的特性付出代价!!
The zero-overhead principle is a C++ design principle that states:
- You don't pay for what you don't use.
- What you do use is just as efficient as what you could reasonably write by hand.
循环引用
内存泄漏
- 全部用 shared_ptr,可能出现循环引用之类的问题,导致内存泄露,
- 依然需要使用不影响计数的原始指针或者 weak_ptr 来避免。
cpp
#include <iostream>
#include <string>
#include <vector>
#include <memory>
using namespace std;
class Person {
public:
string m_sName;
shared_ptr<Person> m_pMother;
shared_ptr<Person> m_pFather;
vector<shared_ptr<Person>> m_oKids;
// vector<weak_ptr<Person>> m_oKids; //弱指针
Person (const string& sName,
shared_ptr<Person> pMother = nullptr,
shared_ptr<Person> pFather = nullptr)
: m_sName(sName), m_pMother(pMother), m_pFather(pFather) {
}
~Person() {
// 由于循环引用,shared_ptr<Person> pKid pMom pDad都不会调用析构函数
cout << "删除 " << m_sName << endl;
}
};
shared_ptr<Person> initFamily (const string& sName)
{
shared_ptr<Person> pMom(new Person(sName + "的母亲"));
shared_ptr<Person> pDad(new Person(sName + "的父亲"));
shared_ptr<Person> pKid(new Person(sName, pMom, pDad));
pMom->m_oKids.push_back(pKid);
pDad->m_oKids.push_back(pKid);
return pKid;
}
int main()
{
string sName = "张三";
shared_ptr<Person> pPerson = initFamily(sName);
cout << sName << "家存在" << endl;
cout << "- " << sName << "被分享" << pPerson.use_count() << "次" << endl;
cout << "- " << sName << "母亲第一个孩子的名字是:"
<< pPerson->m_pMother->m_oKids[0]->m_sName << endl;
sName = "李四";
pPerson = initFamily(sName);
cout << sName << "家已存在" << endl;
}
运行结果
sh
张三家存在
- 张三被分享3次
- 张三母亲第一个孩子的名字是:张三
李四家已存在
weak_ptr
意义
解决循环引用
共享指针shared_ptr指针 ------ 循环引用
如果两个对象使用shared_ptr指针相互引用,并且不存在对这些对象的其他引用,若要释放这些对象及其关联的资源,则共享指针shared_ptr不会释放数据,因为每个对象的引用计数仍为1。
解决:
-
使用普通指针,需要手动管理相关资源的释放。
-
使用 weak_ptr
-
unique_ptr
-
对资源是独占的,防止多个智能指针指向同一个对象。
-
从使用场景上就不存在循环引用的可能。但是,这个需求又确确实实存在。
-
生命周期
弱指针weak_ptr需要共享指针shared_ptr才能创建,是共享指针shared_ptr的辅助类。
不影响指向对象的生命周期
- 每当拥有该对象的最后一个共享指针失去其所有权时,任何弱指针weak_ptr指向的资源都会自动变为空
- (但影响shared_ptr的控制块的生命周期)
共享但不拥有对象 的意义
-
若使用共享指针shared_ptr指针,则其将永远不会释放对象。
-
若使用普通指针,则可能出现指针所引用的对象不再有效,这会带来访问已释放数据的风险。
-
weak_ptr
不影响引用对象的生命周期------引用的生存期可以超过了所引用的对象的生命周期。
更小的开销
你不必为不需要的特性付出代价
The zero-overhead principle is a C++ design principle that states:
- You don't pay for what you don't use.
- What you do use is just as efficient as what you could reasonably write by hand.
使用
弱指针weak_ptr仅提供少量操作:创建,复制和赋值一个弱指针,将弱指针转换为共享指针,检查它是否指向对象。
创建
弱指针weak_ptr指针仅提供3个构造函数
- default构造函数
- copy构造函数
- 共享指针shared_ptr的构造函数。
访问
lock()会从所包含的弱指针weak_ptr中产生一个共享指针shared_ptr。其shared_ptr对象的使用计数在共享指针的生命周期内会增加。
注意,要使用弱指针,需要在表达式中插入lock()函数:
cpp
pPerson->mother->kids[0].lock()->name
而不是调用
cpp
pPerson->mother->kids[0]->name
如果无法进行此修改(例如,由于该对象的最后所有者同时释放了该对象),lock()函数会生成一个空的shared_ptr,而直接调用运算符*或->将导致未定义的行为。
检查存在
检查弱指针指向的对象是否仍然存在,则可以使用以下几种方法:
- 接口列表
expired()
如果弱指针weak_ptr不再共享对象(空的弱指针),则返回true。
此选项等效于检查use_count()是否等于0,但可能更快。
- 调用
use_count()
返回关联对象的所有者(共享指针shared_ptr)的所有者数量,如果返回值为0,则不再有有效的对象。
但是请注意,通常只应出于调试目的调用use_count(),因为C++标准库明确指出:"use_count()不一定有效。"
- 通过使用相应的共享指针shared_ptr构造函数将弱指针weak_ptr显式转换为共享指针shared_ptr。
如果没有有效的引用对象,则此构造方法将引发bad_weak_ptr异常。
这个异常是从std::exception派生的类的异常,其中what()会返回" bad_weak_ptr"。
操作字典
下表为弱指针提供的所有操作。
操作 | 结果 |
---|---|
weak_ptr<T> wp |
默认构造函数;创建一个空的弱指针 |
weak_ptr<T> wp(sp) |
创建一个弱指针,共享由sp拥有的指针的所有权 |
weak_ptr<T> wp(wp2) |
创建一个弱指针,共享由wp2拥有的指针的所有权 |
wp.~weak_ptr() |
析构函数;销毁弱指针,但对拥有的对象无效 |
wp = wp2 |
赋值(wp之后共享wp2的所有权,放弃先前拥有的对象的所有权) |
wp = sp |
用共享指针sp进行赋值(wp之后共享sp的所有权,放弃先前拥有的对象的所有权) |
wp.swap(wp2) |
交换wp和wp2的指针 |
swap(wp1,wp2) |
交换wp1和wp2的指针 |
wp.reset() |
放弃拥有对象的所有权(如果有的话),并重新初始化为空的弱指针 |
wp.use_count() |
返回共享所有者的数量(拥有对象的shared_ptr数目);如果弱指针为空,则返回0 |
wp.expired() |
返回wp是否为空(等同于wp.use_count() == 0,但可能更快) |
wp.lock() |
返回共享指针,该共享指针共享弱指针拥有的指针的所有权(如果没有共享指针,则为空共享指针) |
wp.owner_before(wp2) |
提供严格的弱排序和另一个弱指针 |
wp.owner_before(sp) |
通过共享指针提供严格的弱排序 |
原先使用shared_ptr
cpp
shared_ptr<Person> m_pMother;
shared_ptr<Person> m_pFather;
vector<shared_ptr<Person>> m_oKids;
使用weak_ptr
cpp
shared_ptr<Person> m_pMother;
shared_ptr<Person> m_pFather;
vector<weak_ptr<Person>> m_oKids; //弱指针
shared_ptr<Person> pMom(new Person(sName + "的母亲"));
shared_ptr<Person> pKid(new Person(sName, pMom, pDad));
// 尽管是vector<weak_ptr<Person>>但可以push_back分享指针
pMom->m_oKids.push_back(pKid);
std 主类型和弱引用
延伸一下:shared_ptr 是主类型,weak_ptr是其弱引用。
cpp
// 主类型 std::shared_ptr<T>
// 弱引用类型 std::weak_ptr<T>、T *
除此之外,std库中还有其它已经实现且比较常用的 主类型 与 弱引用类型
cpp
// 主类型 std::string
// 弱引用类型 std::string_view、const char *
// 主类型 std::vector<T>
// 弱引用类型 std::span<T>、(T *, size_t)
// 主类型 std::unique_ptr<T>
// 弱引用类型 T *