智能指针
为什么不用裸指针
- 裸指针在声明中并没有指出,裸指针指涉到的是单个对象还是一个数组。
- 裸指针在声明中也没有提示在使用完指涉的对象以后,是否需要析构它。换言之,你从声明中看不出来指针是否拥有(own)其指涉的对象。
- 即使知道需要析构指针所指涉的对象,也不可能知道如何析构才是适当的。是应该使用delete运算符呢,还是别有它途(例如,可能需要把指针传入一个专门的、用千析构的函数)
- 即使知道了应该使用delete运算符,参见理由1’还是会发生到底应该用单个对象形式(“
delete
”)还是数组形式(delete[]
)的疑问。一旦用错,就会导致未定义行为。 - 没有什么正规的方式能检测出指针是否空悬(dangle),也就是说,它指涉的内存是否已经不再持有指针本应该指涉的对象。如果一个对象已经被析构了,而某些指针仍然指涉到它,就会产生空悬指针。
c++ 中的智能指针
C++ll 中共有四种智能指针:
std::auto_ptr
(弃用)std::unique_ptr
std::shared_ptr
std::weak_ptr
所有这些智能指针都是为管理动态分配对象的生命期而设计的,
换言之,通过保证这样的对象在适当的时机以适当的方式析构(包括发生异常的场合),
来防止资源泄漏。
专属所有权 unique_ptr
std::unique_ptr实现的是专属所有权语义。一个非空的std::unique_ptr总是拥有其所指涉到的资源。移动一个std::unique_ptr会将所有权从源指针移至目标指针(源指针被置空)。
std::unique_ptr不允许复制,因为如果复制了一个std::unique_ptr,就会得到两个指涉到同一资源的std::unique_ptr,而这两者都认为且已拥有(因此应当析构)该资源。因而,std::unique_ptr是个只移型别。在执行析构操作时,由非空的std::unique_ptr析构其资源。默认地,资源的析构是通过对std::unique_ptr内部的裸指针实施delete完成的。
- unique_ptr 是可以自定义析构函数的
/// Destructor, invokes the deleter if the stored pointer is not null.
~unique_ptr() noexcept
{
static_assert(__is_invocable<deleter_type&, pointer>::value,
"unique_ptr's deleter must be invocable with a pointer");
auto& __ptr = _M_t._M_ptr();
if (__ptr != nullptr)
get_deleter()(std::move(__ptr));
/* 这里比较奇怪(看起来)
实际上是get_deleter()返回构造时传入的可调用对象
然后对可调用对象进行调用
这里的__ptr的类型是auto将由编译时编译器自行推断 */
__ptr = pointer();
}
在这个析构函数中,首先使用static_assert进行断言检查,确保deleter_type类型的对象可以通过指针调用。如果无法调用,则会触发编译错误。
接下来,通过引用_M_t._M_ptr()获取存储的指针,并将其赋值给__ptr变量。然后检查__ptr是否为nullptr,即指针是否为空。
如果__ptr不为空,则调用get_deleter()函数获取deleter对象,并将__ptr作为参数传递给它。这里使用std::move(__ptr)将__ptr转移所有权,以确保在调用完毕后__ptr被置空。
最后,将__ptr赋值为pointer(),即将其置为空指针。
- 如果由 makelnvestment 创建的对象不应被直接删除,而是应该先写入一条日志,那么 makelnvestment 可以像下面这样实现
这里的传入的是一个可调用对象 lambda OR class() OR func()
在使用默认析构器(即 delete 运算符)的前提下,std::unique_ptr 和裸指针尺寸相同。自定义析构器现身以后,情况便有所不同了。若析构器是函数指针,那么 std::unique_ptr 的尺寸 一 般会增加 一 到两个字长 (word) 。而若析构器是函数对象,则带来的尺寸变化取决千该函数对象中存储了多少状态。无状态的函数对象(例如,无捕获的 lambda 表达式)不会浪费任何存储尺寸。
共享所有权 shared_ptr
通过std::shared_ptr
这种智能指针访问的对象采用共享所有权来管理其生存期。没有哪个特定的std::shared_ptr
拥有该对象。取而代之的是,所有指涉到它的std::shared_ptr
共同协作,确保在不再需要该对象的时刻将其析构。当最后一个指涉到某对象的std::shared_ptr
不再指涉到它时(例如,由于是该std::shared_ptr
被析构,或使其指涉到另一个不同的对象),该std::shared_ptr
会析构其指涉到的对象。
std::shared_ptr 的构造函数会使该计数递增(通常如此),而其析构函数会使该计数递减,而复制赋值运算符同时执行两种操作;
引用计数的存在会带来 一 些性能影响:
- 内存 内存是裸指针的两倍
- 引用技术需要动态分配
- 计数的递增和递减必须是原子操作 (慢于非原子操作 即使计数只有一个字节)
析构
和 unique_ptr
相似 shared_ptr
同样支持自定义析构器
但支持的方式和unique_ptr
并不相同
对于 unique_ptr
而言,析构器的型别是智能指针型别的一部分。
内存
关于 shared_ptr 上述内存何时创建
- std::make_shared 总是创建一个控制块.
- 从具备专属所有权的指针(即std::unique_ptr或std::auto_ptr指针)出发构造一个std::shared_ptr时,会创建—个控制块 。
构造后 专属所有权的指针 的所有权转移 其被置空
- 当 std::shared_ptr 构造函数使用裸指针作为实参来调用时
使用裸指针创建时要注意 以免两个sharePtr指向同一内存
进行错误析构 进而野指针
注意
- 在类内不要使用this去构造shared_ptr
使用
class M : public std::enable_shared_from_this<M>
进行继承操作
并使用 成员函数 shared_from_this() 获取此对象的 shared_ptr
如上所示
上述 仅仅作为演示
2. 关于 weak_ptr
- 关于 weak_ptr 的使用场景
- 校验指针是否悬空
- 弱回调
- 缓存及其观察者
- 避免shared_ptr循环引用
-
非必要 则使用std::make_ptr
优先选用 std::make_unique 和 std::make_shared, 而非直接使用 newvoid processWidget(std::shared_ptr<Widget> sqw,int priority){ // ..... } int computerPriority(){ // .... return 1; } int func(){ processWidget(std::make_shared< Widget>( new Widget), computerPriority()); // 潜在的内存(资源)泄漏 }
原因与编译器从源代码到目标代码的翻译有关。在运行期,传递给函数的实参必须在函数调用被发起之前完成评估求值。下列事件必须在 processWidget 开始执行前发生:
- 表达式 “new Widget" 必须先完成评估求值,即, 一个 Widget 对象必须先在堆上创建。
- 由 new 产生的裸指针的托管对象 std::shared_ptr 的构造函数必须执行。
- computePriority 必须运行。
编译器不必按上述顺序来生成代码。
其有可能生成- 实施 “ new Widget" 。
- 执行 computePriority 。
- 运行 std::shared_ptr 构造函数。
如果生成了这样的代码,并且在运行期 computePriority 产生了异常,那么由第一步动态分配的Widget会被泄漏,因为它将永远不会被存储到在第三步才接管的std::shared_ptr中去。
使用 std::make_shared 可以避免该问题。
产生这种歧义的原因可能是 编译优化时进行评估求知? 咱也不知道QAQ
- BUT
-
相对于直接使用 new 表达式,优先选用 make 函数 但是所有的make函数 不能自定 析构器 需要自定析构器的智能指针 还是得
std::shared_ptr<T>(new T, ...)
-
关于列表构造
std::make_shared<std::vector<int>> (10,20)
std::make_shared<std::vector<int>> {10,20}
其构造的 是 [10,20] 还是 [20,20,20,20,20…]
答案是std::make_shared<std::vector<int>> {10,20}
编译不通过std::make_shared<std::vector<int>> (10,20)
构造的是[20,20,20,20,20…]
为了使用列表构造+make函数
我们可以auto i = { 10,20}; std::make_shared<std::vector<int>> (i);
-
- std::shared_ptr 的 API 仅被设计用来处理指涉到单个对象的指针 但unique_ptr 可以是数组
- 关于 shared_ptr 是 make 构造 还是 new 构造时 关于 weak_ptr 的一些差别
#include <iostream> #include <memory> class Resource { public: Resource() { std::cout << "Resource acquired." << std::endl; } ~Resource() { std::cout << "Resource released." << std::endl; } int value = 10; }; int main() { std::weak_ptr<Resource> weakPtr; Resource * i = nullptr; { auto sharedPtr = std::make_shared<Resource>(); weakPtr = sharedPtr; i = sharedPtr.get(); if (auto lockedPtr = weakPtr.lock()) { std::cout << "Resource is still available." << std::endl; } else { std::cout << "Resource has been released." << std::endl; } } std::cout << i->value << std::endl; if (auto lockedPtr = weakPtr.lock()) { std::cout << "Resource is still available." << std::endl; } else { std::cout << "Resource has been released." << std::endl; } std::cout << i->value << std::endl; return 0; }
运行上面的例子 可知:#include <iostream> #include <memory> class Resource { public: Resource() { std::cout << "Resource acquired." << std::endl; } ~Resource() { std::cout << "Resource released." << std::endl; } int value = 10; }; int main() { std::weak_ptr<Resource> weakPtr; Resource * i = nullptr; { std::shared_ptr<Resource> sharedPtr (new Resource); weakPtr = sharedPtr; i = sharedPtr.get(); if (auto lockedPtr = weakPtr.lock()) { std::cout << "Resource is still available." << std::endl; } else { std::cout << "Resource has been released." << std::endl; } } std::cout << i->value << std::endl; if (auto lockedPtr = weakPtr.lock()) { std::cout << "Resource is still available." << std::endl; } else { std::cout << "Resource has been released." << std::endl; } std::cout << i->value << std::endl; return 0; }
通过 std::make_shared(…)创建shared_ptr真正free内存的时机是 ---- 引用计数和弱引用计数全部为0时
通过 std::shared_ptr(new T(…))创建shared_ptr真正free内存的时机是 ---- 引用计数为0时