CC-每日一练-6
C/C++ | 每日一练 (6)
💢欢迎来到张胤尘的技术站 💥技术如江河,汇聚众志成。代码似星辰,照亮行征程。开源精神长,传承永不忘。携手共前行,未来更辉煌💥
C/C++ | 每日一练 (6)
题目
什么是智能指针?智能指针和普通指针的区别是什么?有哪些常用的智能指针?说一下底层如何实现的?
参考答案
普通指针存在的问题?
普通指针(如 C/C++ 中的 int*、char*
等)是编程中非常灵活的工具,但同时也带来了许多潜在问题。这些问题主要源于指针的低级特性和对资源管理的直接依赖。
内存泄漏
内存泄漏是指动态分配的内存没有被正确释放,导致程序占用的内存不断增加,最终可能导致程序性能下降甚至崩溃。比如使用 malloc 或者 new
分配内存后,必须在合适的时候使用 free 或者 delete 释放内存。如果忘记释放内存,就会导致内存泄漏。例如:
void leakMemory() {
int* ptr = new int[1000]; // 分配内存
// 忘记释放内存
}
int main() {
leakMemory(); // 函数调用
return 0;
}
当程序每次调用 leakMemory 函数时,都会分配1000个 int 的内存,但这些内存永远不会被释放。
内存泄漏的对程序的危害:
- 性能下降 :随着程序运行时间的增加,内存泄漏会导致可用内存逐渐减少,程序运行速度变慢。
- 系统崩溃 :如果内存泄漏严重,可能会耗尽系统资源,导致程序崩溃甚至整个系统崩溃。
- 资源浪费 :未释放的内存无法被系统或其他程序使用,造成资源浪费。
悬空指针
另外,还有一种无效的指针——悬空指针,在使用时可能导致未定义行为甚至程序崩溃。 悬空指针是指指针曾经指向一个有效的内存位置,但该内存已被释放或回收,导致指针变得无效。尽管指针仍然保存着原来的地址,但访问该地址会产生未定义行为,因为该地址可能已经被分配给其他对象或成为不可访问的区域。例如: void danglingPointer() { int *ptr = new int(10); delete ptr; // 释放内存 *ptr = 20; // 通过悬空指针访问内存,未定义行为 } int main() { danglingPointer(); // 函数调用 return 0; }
指针被重复释放
重复释放是指对同一块内存调用多次 delete 或 free。这可能导致程序崩溃或内存损坏。例如:
int *doubleFree()
{
int *ptr = new int(10);
delete ptr; // 第一次释放
return ptr;
}
int main()
{
int *p = doubleFree();
// free(): double free detected in tcache 2
// Aborted (core dumped)
delete p; // 第二次释放,未定义行为
return 0;
}
另外,在使用普通指针时如果不注意可能还会有一些其他更为严重的问题产生,这里就不再一一列举。
说到这里有些同学可能会有疑问:当程序员的能力足够强、足够的仔细是不是就可以避免这些问题?这么说也很有道理,确实可以避免许多常见的问题,比如内存泄漏、悬空指针等。然而,我认为
“人非圣贤,孰能无过”,即使是能力最强、最谨慎的程序员也难以完全避免错误,尤其是在特别复杂的项目或团队协作环境中。使用普通指针仍然存在一些难以克服的局限性,这些局限性使得智能指针和其他现代C++
特性成为更好的选择。
那么,接下来就针对 C++ 标准库中的智能指针进行深度讲解。
智能指针
智能指针是 C++ 标准库中的一种类模板,用于自动管理动态分配的内存 。它们可以防止内存泄漏和悬挂指针问题,并且提供了异常安全性。
C++11 标准库引入了三种智能指针:std::unique_ptr、std::shared_ptr 和 std::weak_ptr。
本文章只讨论
C++11标准库中的智能指针,Boost库中提供的智能指针本文章不再讨论。另外std::auto_ptr在C++11中被废弃,在C++17中被移除。本文章也不再讨论。另外,本篇文章的所有底层源码均来源于
libstdc++,其他平台的源码实现可能会有些出入,请注意甄别。
std::unique_ptr
std::unique_ptr 是 C++11
引入的一种智能指针,表示对资源(通常是动态分配的内存)的独占所有权。它通过移动语义(而不是拷贝语义)来转移资源的所有权,确保同一时刻只有一个
unique_ptr 可以管理某个资源。
#include
#include
int main()
{
std::unique_ptr p(new int(10));
std::cout « *p « std::endl; // 10
// cannot be referenced – it is a deleted function
// std::unique_ptr p1 = p;
// p = p1;
}
在源码中,std::unique_ptr 因为禁用了拷贝构造函数和赋值运算符函数,导致 std::unique_ptr 不支持拷贝语义。如下所示:
// Disable copy from lvalue.
unique_ptr(const unique_ptr&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;
虽然 std::unique_ptr 禁用了拷贝构造和赋值操作运算符,但是提供了移动拷贝构造和移动赋值操作运算符,也就是说
std::unique_ptr 支持了移动语义。如下所示:
// Move constructor.
unique_ptr(unique_ptr&&) = default;
unique_ptr& operator=(unique_ptr&&) = default;
注意:
std::unique_ptr模板化的移动拷贝构造和移动赋值操作运算符源码这里不再列举,感兴趣的同学自行查看源代码。 #include #include int main() { std::unique_ptr p(new int(10)); std::unique_ptr p1(new int(21)); std::cout « *p « std::endl; // 10 std::cout « *p1 « std::endl; // 21 std::unique_ptr p3(std::move(p)); // 移动构造 std::unique_ptr p4; p4 = std::move(p1); // 移动赋值操作运算符 std::cout « *p3 « std::endl; // 10 std::cout « *p4 « std::endl; // 21 return 0; }
底层结构
在 std::unique_ptr 的实现中,__uniq_ptr_data 是一个底层数据结构,用于封装和管理 std::unique_ptr
的底层指针和删除器。
为了可以更好的理解下面的源码,这里先解释一下删除器:删除器是一个非常重要的组件,它定义了如何释放 std::unique_ptr
所管理的资源。另外,删除器的作用不仅仅是简单地释放内存,它还可以执行更复杂的清理操作,比如关闭文件句柄、释放网络连接、销毁自定义对象等。实际开发过程中,通过自定义删除器,std::unique_ptr
能够灵活地管理各种类型的资源,而不仅仅是动态分配的内存。
在下面的 std::unique_ptr 模板类中,类型 _Tp 表示底层指针的泛型参数,而 _Dp
表示所使用的删除器,如果未指定删除器则使用默认删除器 default_delete<_Tp>。如下所示:
template >
class unique_ptr
{
// …
__uniq_ptr_data<_Tp, _Dp> _M_t;
// …
};
以上代码只是部分截取,请注意甄别。 在上述结构体中,
_M_t是最为核心的成员属性,该属性是一个包含了两个部分的模板结构体,如下所示: template ::value, bool = is_move_assignable<_Dp>::value> struct __uniq_ptr_data : __uniq_ptr_impl<_Tp, _Dp> { using __uniq_ptr_impl<_Tp, _Dp>::__uniq_ptr_impl; __uniq_ptr_data(__uniq_ptr_data&&) = default; __uniq_ptr_data& operator=(__uniq_ptr_data&&) = default; }; 而__uniq_ptr_data又继承自__uniq_ptr_impl,__uniq_ptr_impl也是一个模板类,用于封装和管理std::unique_ptr的指针和删除器。它是std::unique_ptr的底层实现细节,隐藏了指针和删除器的管理逻辑,使得std::unique_ptr的接口更加简洁和安全。源码如下所示: template class __uniq_ptr_impl { template struct _Ptr { using type = _Up*; }; template struct _Ptr<_Up, _Ep, __void_t::type::pointer» { using type = typename remove_reference<_Ep>::type::pointer; }; public: // … pointer& _M_ptr() { return std::get<0>(_M_t); } pointer _M_ptr() const { return std::get<0>(_M_t); } _Dp& _M_deleter() { return std::get<1>(_M_t); } const _Dp& _M_deleter() const { return std::get<1>(_M_t); } // … private: tuple _M_t; }; 以上代码只是部分截取,请注意甄别。 从上述代码中可以看出,__uniq_ptr_impl又使用tuple存储底层指针和删除器,同时对外也提供了底层指针pointer和删除器_Dp的访问函数。
常用操作
下面针对 std::unique_ptr 中常用函数的底层源码进行分析。
释放所有权
release 函数释放 std::unique_ptr 当前管理的指针。
pointer release() noexcept
{
return _M_t.release();
}
调用 _M_t 的 release 函数,取到底层指针 __p,直接将底层指针置为 nullptr,最终返回当前管理的指针。如下所示:
pointer release() noexcept
{
pointer __p = _M_ptr();
_M_ptr() = nullptr;
return __p;
}
如果将 std::unique_ptr 的底层指针置为 nullptr,表示它不再管理任何资源。
重置
reset 函数用于替换 std::unique_ptr 当前管理的底层指针。
void reset(pointer __p = pointer()) noexcept
{
static_assert(__is_invocable::value,
“unique_ptr’s deleter must be invocable with a pointer”);
_M_t.reset(std::move(__p));
}
调用 _M_t 的 reset 函数,取到底层指针 __old_p,将新的指针 __p 赋值底层指针。给如下所示:
void reset(pointer __p) noexcept
{
const pointer __old_p = _M_ptr();
// 新的指针赋值给 unique_ptr
_M_ptr() = __p;
// 如果当前指针不为空
if (__old_p)
// 调用删除器释放当前指针
_M_deleter()(__old_p);
}
如果当前底层指针 __old_p 不为空,则首先获取到删除器。给如下所示:
// 获取取到删除器
const _Dp& _M_deleter() const { return std::get<1>(_M_t); }
然后,通过调用删除器释放当前指针(下面这个是默认删除器的释放指针的代码)。给如下所示:
template
struct default_delete
{
// …
void operator()(_Tp* __ptr) const
{
static_assert(!is_void<_Tp>::value,
“can’t delete pointer to incomplete type”);
static_assert(sizeof(_Tp)>0,
“can’t delete pointer to incomplete type”);
delete __ptr;
}
};
以上代码只是部分截取,请注意甄别。
获取原始指针
get 函数返回 std::unique_ptr 当前管理的底层指针。它不会转移所有权,只是提供对底层指针的访问。
pointer get() const noexcept
{
return _M_t._M_ptr();
}
调用 _M_t 的 _M_ptr 函数,取到底层指针 pointer,直接返回该指针。给如下所示:
pointer _M_ptr() const { return std::get<0>(_M_t); }
交换
swap 函数用于交换两个 std::unique_ptr 的底层指针和删除器。
void swap(unique_ptr& __u) noexcept
{
static_assert(__is_swappable<_Dp>::value, “deleter must be swappable”);
_M_t.swap(__u._M_t);
}
调用 _M_t 的 swap 函数,底层使用的是标准库的 std::swap 函数,然后交换底层指针和删除器。给如下所示:
void swap(__uniq_ptr_impl& __rhs) noexcept
{
using std::swap;
swap(this->_M_ptr(), __rhs._M_ptr());
swap(this->_M_deleter(), __rhs._M_deleter());
}
std::shared_ptr
std::shared_ptr 也是 C++11 引入的一种智能指针,用于通过引用计数机制共享对象的所有权。它允许多个
std::shared_ptr 实例共享同一个对象,并在最后一个 std::shared_ptr 被销毁时自动销毁对象。
#include
#include
int main()
{
std::shared_ptr p(new int(10));
std::cout « *p « std::endl; // 10
std::shared_ptr p1;
std::shared_ptr p2 = p; // 拷贝构造
p1 = p2; // 赋值运算符构造
std::cout « *p « std::endl; // 10
std::cout « *p1 « std::endl; // 10
std::cout « *p2 « std::endl; // 10
}
std::shared_ptr 和 std::unique_ptr 的其中之一的区别就是允许共享同一个对象的所有权,在源码中
std::shared_ptr 开放了拷贝构造函数和赋值运算符函数,也就是说支持拷贝语义。如下所示:
shared_ptr(const shared_ptr&) noexcept = default;
shared_ptr& operator=(const shared_ptr&) noexcept = default;
注意:
std::shared_ptr模板化的拷贝构造和赋值操作运算符源码这里不再列举,感兴趣的同学自行查看源代码。 同样的std::shared_ptr也开放了移动拷贝构造函数和移动赋值运算符函数,支持移动语义。如下所示: shared_ptr(shared_ptr&& __r) noexcept : __shared_ptr<_Tp>(std::move(__r)) { } shared_ptr& operator=(shared_ptr&& __r) noexcept { this->__shared_ptr<_Tp>::operator=(std::move(__r)); return *this; } 注意:std::shared_ptr模板化的移动拷贝构造和移动赋值操作运算符源码这里不再列举,感兴趣的同学自行查看源代码。 #include #include int main() { std::shared_ptr p(new int(10)); std::cout « *p « std::endl; // 10 std::shared_ptr p1; std::shared_ptr p2 = std::move(p); p1 = std::move(p2); std::cout « *p1 « std::endl; // 10 }
底层结构
std::shared_ptr 是通过继承一个内部模板类 __shared_ptr<_Tp> 来实现的。这种设计允许
std::shared_ptr 继承底层的资源管理和引用计数逻辑,同时提供标准库的接口。如下所示:
template
class shared_ptr : public __shared_ptr<_Tp>
{
// …
}
以上代码只是部分截取,请注意甄别。 在
__shared_ptr<_Tp>类中封装了重要的两个成员属性:_M_ptr和_M_refcount。
_M_ptr:指向被管理底层指针的指针。_M_refcount:引用计数器,用于跟踪资源的引用计数。 template class __shared_ptr : public __shared_ptr_access<_Tp, _Lp> { // … private: element_type* _M_ptr; // Contained pointer. __shared_count<_Lp> _M_refcount; // Reference counter. };
以上代码只是部分截取,请注意甄别。 在引用计数器
_M_refcount中,又封装了引用计数逻辑的类__shared_count。如下所示: template<_Lock_policy _Lp> class __shared_count { // … private: _Sp_counted_base<_Lp>* _M_pi; }; 以上代码只是部分截取,请注意甄别。_Sp_counted_base也是一个模板类,封装了实际的引用计数逻辑。它包含两个原子计数器:
_M_use_count:记录强引用计数(即std::shared_ptr的个数)。_M_weak_count:记录弱引用计数(即std::weak_ptr的个数)。 template<_Lock_policy _Lp = __default_lock_policy> class _Sp_counted_base : public _Mutex_base<_Lp> { // … private: _Atomic_word _M_use_count; // #shared _Atomic_word _M_weak_count; // #weak + (#shared != 0) };
以上代码只是部分截取,请注意甄别。
接下来,从整个 std::shared_ptr 的生命周期,深入了解 std::shared_ptr 是如何管理引用计数器的。
- 当一个新的
std::shared_ptr被创建时,_M_use_count和_M_weak_count默认都是 1。 explicit shared_ptr(_Yp* __p) : __shared_ptr<_Tp>(__p) { } 调用__shared_ptr<_Tp>(__p)构造函数,如下所示: template> explicit __shared_ptr(_Yp* __p) : _M_ptr(__p), _M_refcount(__p, typename is_array<_Tp>::type()) { static_assert( !is_void<_Yp>::value, “incomplete type” ); static_assert( sizeof(_Yp) > 0, “incomplete type” ); _M_enable_shared_from_this_with(__p); } 首先_M_ptr(__p)初始化数据指针,紧接着,调用_M_refcount(__p, typename is_array<_Tp>::type())构造函数,如下所示: template explicit __shared_count(_Ptr __p) : _M_pi(0) { __try { _M_pi = new _Sp_counted_ptr<_Ptr, _Lp>(__p); } __catch(…) { delete __p; __throw_exception_again; } } 调用_M_pi的构造函数,先初始化父类_Sp_counted_base,当父类初始化完毕后再初始化_Sp_counted_ptr,如下所示: _Sp_counted_base() noexcept : _M_use_count(1), _M_weak_count(1) { } - 每当共享一个
std::shared_ptr指针时,_M_use_count会增加。
下面以赋值操作运算符为例。其他情况基本类似,这里不再赘述。 shared_ptr& operator=(const shared_ptr&) noexcept = default;
std::shared_ptr使用了default的方式让编译器自动生成代码,这里可以查看官方文档:Shares ownership of the object managed by r. If r manages no object, *this manages no object too. Equivalent to shared_ptr®.swap(*this). 根据标准库的文档,拷贝赋值运算符的行为等价于以下代码: shared_ptr(r).swap(*this); 首先构造一个临时的
std::shared_ptr对象,它共享r的所有权。然后,通过swap方法交换临时对象和当前对象的内容。 template&» shared_ptr(const shared_ptr<_Yp>& __r) noexcept : __shared_ptr<_Tp>(__r) { } 调用__shared_ptr的带参构造函数,如下所示: template> __shared_ptr(const __shared_ptr<_Yp, _Lp>& __r) noexcept : _M_ptr(__r._M_ptr), _M_refcount(__r._M_refcount) { } 其中,_M_refcount又调用了__shared_count的拷贝构造函数,如下所示: __shared_count(const __shared_count& __r) noexcept : _M_pi(__r._M_pi) { if (_M_pi != nullptr) _M_pi->_M_add_ref_copy(); } 在该函数中当_M_pi != nullptr时,调用_M_add_ref_copy函数,在该函数内采用原子操作增加强引用计数器的值,如下所示: void _M_add_ref_copy() { __gnu_cxx::__atomic_add_dispatch(&_M_use_count, 1); } 最后的交换逻辑参考 常用操作 小结中的内容,这里不再赘述。
- 当一个
std::shared_ptr被销毁时,_M_use_count引用计数会减少。如果引用计数变为零,说明没有std::shared_ptr再持有该对象,此时对象会被销毁。__shared_count对象封装了引用计数的逻辑。当std::shared_ptr被销毁时,触发析构逻辑,如下所示: ~__shared_count() noexcept { if (_M_pi != nullptr) _M_pi->_M_release(); } 调用了_M_release函数,该函数内对引用计数器进行了减一的操作,当引用计数器为0时,触发资源回收的逻辑。如下所示: void _M_release() noexcept { // … if (__gnu_cxx::__exchange_and_add_dispatch(&_M_use_count, -1) == 1) { // … _M_dispose(); // … if (__gnu_cxx::__exchange_and_add_dispatch(&_M_weak_count, -1) == 1) { // … _M_destroy(); } } }
以上代码只是部分截取,请注意甄别。
__exchange_and_add_dispatch函数分别对_M_use_count和_M_weak_count进行了减一操作,当_M_use_count值为 0 时调用_M_dispose函数;当_M_weak_count值为 0 时调用_M_destroy函数。如下所示: virtual void _M_dispose() noexcept { delete _M_ptr; } virtual void _M_destroy() noexcept { delete this; }
到目前为止,我们还未对 弱引用计数器 的作用进行说明。另外,从上面的描述中我们可以知道,强引用计数器的作用是为了跟踪
std::shared_ptr 管理的底层指针被多少个其他的 std::shared_ptr
共享。那么在这里我先抛出一个问题:在下面的代码示例中,class A 和 class B 两个类的对象通过 std::shared_ptr
智能指针相互引用时,在程序结束时是否可以正常的将引用计数减少到0?
#include
class B; // 前置声明
class A {
public:
std::shared_ptr **b_ptr;
};
class B {
public:
std::shared_ptra_ptr;
};
int main() {
std::shared_ptraptr = std::make_shared();
std::shared_ptr bptr = std::make_shared();
aptr->b_ptr = bptr; // A 持有 B 的共享指针
bptr->a_ptr = aptr; // B 持有 A 的共享指针
return 0;
}
答案:在这个例子中,A 和 B 通过 std::shared_ptr 相互引用,形成一个循环依赖。当程序结束时,A 和 B
的引用计数都为1,无法减少到0,因此不会触发删除器,导致内存泄漏。
为什么?如何解决?解决办法是否和弱引用计数相关?这些悬念会在 std::weak_ptr 章节中一一解答。
常用操作
获取引用计数
use_count 函数返回当前 std::shared_ptr 管理的底层指针的引用计数,即有多少个 std::shared_ptr
实例共享同一个底层指针。
long use_count() const noexcept
{
return _M_refcount._M_get_use_count();
}
调用 _M_refcount 的 _M_get_use_count 函数,如果 std::shared_ptr 没有引用任何指针,则
_M_pi 对象为 nullptr 则返回 0,否则调用 _M_pi 对象的 _M_get_use_count 获取强引用计数值。如下所示:
long _M_get_use_count() const noexcept
{
return _M_pi ? _M_pi->_M_get_use_count() : 0;
}
紧接着,在 _M_get_use_count 中使用原子操作读取 _M_use_count 强引用计数器的值。如下所示:
long _M_get_use_count() const noexcept
{
// No memory barrier is used here so there is no synchronization
// with other threads.
return __atomic_load_n(&_M_use_count, __ATOMIC_RELAXED);
}
其中,__ATOMIC_RELAXED 是最弱的内存顺序模型,仅保证操作的原子性,不提供任何内存顺序保证。
重置
reset 函数用于释放当前 std::shared_ptr 管理的指针。
void reset() noexcept
{
__shared_ptr().swap(*this);
}
在 reset 函数中使用默认构造创建了 __shared_ptr 临时对象,这个临时对象不管理任何资源(即它的引用计数为 0)。然后调用了
swap 函数将当前 std::shared_ptr 对象的内容与临时对象交换。交换后,当前 std::shared_ptr
的内容被清空。同时,临时对象接管了当前 std::shared_ptr 的资源,但由于临时对象即将被销毁,它的析构函数就会自动释放这些资源。
获取原始指针
get 函数返回 std::shared_ptr 管理的底层指针,但不会转移所有权。
element_type* get() const noexcept
{
return _M_ptr;
}
交换
swap() 函数用于交换两个 std::shared_ptr 的管理底层指针和引用器。
void swap(__shared_ptr<_Tp, _Lp>& __other) noexcept
{
std::swap(_M_ptr, __other._M_ptr);
_M_refcount._M_swap(__other._M_refcount);
}
使用标准库中的 swap 函数对底层指针进行交换,然后调用了 _M_swap 函数交换引用计数器。如下所示:
void _M_swap(__shared_count& __r) noexcept
{
_Sp_counted_base<_Lp>* __tmp = __r._M_pi;
__r._M_pi = _M_pi;
_M_pi = __tmp;
}
检测是否唯一
unique() 函数用于检查当前 std::shared_ptr 是否是其管理底层指针的唯一所有者。
bool unique() const noexcept
{
return _M_refcount._M_unique();
}
调用 _M_unique 函数,返回一个 bool 值。如果 _M_get_use_count 函数的返回值等于1返回 true 否则返回
false。如下所示:
bool _M_unique() const noexcept
{
return this->_M_get_use_count() == 1;
}
在 _M_get_use_count 函数中调用了 _M_pi 的 _M_get_use_count 方法,返回底层指针的强引用计数值。具体
_M_get_use_count 方法内容,请参考 获取引用计数 函数解析。
判断相等性
下面给出一段代码,该代码中判断了两个 std::shared_ptr 的相等性,如下所示:
#include
#include
int main()
{
std::shared_ptr p(new int(10));
std::shared_ptr p1 = p;
std::shared_ptr p2(new int(10));
std::cout « (p == p1) « std::endl; // 1
std::cout « (p == p2) « std::endl; // 0
}
对象相等性判断的依据是 == 运算符重载函数的判断逻辑,函数如下所示:
template
_GLIBCXX_NODISCARD inline bool
operator==(const shared_ptr<_Tp>& __a, const shared_ptr<_Up>& __b) noexcept
{ return __a.get() == __b.get(); }
在该函数中,分别对两个 std::shared_ptr 调用了 get
函数,判断两个底层管理的指针是否相等,又因为指针的相等性是根据指针的地址是否相等来确定的,所以两个 std::shared_ptr
是否相等就是判断分别引用的底层指针地址是否相等。
std::weak_ptr
std::weak_ptr 是 C++11 标准库中的一种智能指针,用于解决 std::shared_ptr
在使用过程中可能出现的循环引用问题,同时允许对资源进行“弱引用”。与 std::shared_ptr 不同,std::weak_ptr
不会增加资源的强引用计数,因此不会阻止资源的销毁。
我们重新看一下 std::shared_ptr 章节中给出的异常代码,如下所示:
#include
class B; // 前置声明
class A {
public:
std::shared_ptr **b_ptr;
};
class B {
public:
std::shared_ptra_ptr;
};
int main() {
std::shared_ptraptr = std::make_shared();
std::shared_ptr bptr = std::make_shared();
aptr->b_ptr = bptr; // A 持有 B 的共享指针
bptr->a_ptr = aptr; // B 持有 A 的共享指针
return 0;
}
接下来,根据每行代码分析一下,为什么会出现在程序结束时,引用计数无法到 0 的问题。
- 首先创建
aptr,_M_use_count和_M_weak_count都是 1。 - 紧接着创建
bptr,_M_use_count和_M_weak_count也都是 1。 - 当执行到
aptr->b_ptr = bptr时,bptr的_M_use_count引用计数器增长变成了 2。 - 同理当执行到
bptr->a_ptr = aptr时,aptr的_M_use_count引用计数器也增长变成了 2。 - 当程序
main函数结束时,对函数栈中的栈对象依次进行析构: - 首先析构
bptr,将bptr的_M_use_count引用计数器减少变成了 1,因为_M_use_count不是 0 无法触发_M_dispose函数的执行。 - 然后析构
aptr,将aptr的_M_use_count引用计数器也减少变成了 1,因为_M_use_count同样不是 0 也无法触发_M_dispose函数的执行。 - 最终,两个
std::shared_ptr所管理的底层指针都无法得到正确的资源释放,形成内存泄漏问题。 为了解决这个问题,就可以使用std::weak_ptr来打破循环引用。因为std::weak_ptr不会增加强引用计数,所以可以安全地访问资源。如下所示: class B; // 前置声明 class A { public: std::weak_ptr **b_ptr; // 使用 weak_ptr }; class B { public: std::weak_ptra_ptr; }; int main() { std::shared_ptraptr = std::make_shared(); std::shared_ptr bptr = std::make_shared(); aptr->b_ptr = bptr; // A 持有 B 的弱引用 bptr->a_ptr = aptr; // B 持有 A 的弱引用 return 0; // 程序结束时,资源可以正确释放 } - 当执行到
aptr->b_ptr = bptr时,bptr的_M_use_count引用计数器不会增长,但是会对_M_weak_count引用计数器增长,变成了 2。 - 同理当执行到
bptr->a_ptr = aptr时,aptr的_M_use_count引用计数器也不会增长,同样会对_M_weak_count引用计数器增长,变成了 2。 - 当
main函数结束时,触发析构逻辑: - 首先析构
bptr: bptr的_M_use_count减少1,变为 0;因为_M_use_count为 0,触发_M_dispose函数,释放B的资源。bptr的_M_weak_count减少1,变为1。- 然后析构
aptr: aptr的_M_use_count减少1,变为 0;因为_M_use_count为 0,触发_M_dispose函数,释放A的资源。aptr的_M_weak_count减少1,变为1。- 当最后一个
std::weak_ptr被析构时,_M_weak_count减少到 0,触发_M_destroy函数的执行。至此内存都被正常释放,无内存泄漏问题。
接下来,我们一起深入 std::weak_ptr 中探究其背后的原理。
std::weak_ptr 支持拷贝语义和移动语义。如下所示:
weak_ptr(const weak_ptr&) noexcept = default;
weak_ptr& operator=(const weak_ptr& __r) noexcept = default;
weak_ptr(weak_ptr&&) noexcept = default;
weak_ptr& operator=(weak_ptr&& __r) noexcept = default;
注意:
std::weak_ptr模板化的拷贝构造、移动构造函数和移动、赋值操作运算符源码这里不再列举,感兴趣的同学自行查看。
底层结构
首先 std::weak_ptr 继承自 __weak_ptr。如下所示:
template
class weak_ptr : public __weak_ptr<_Tp>
{
// …
}
以上代码只是部分截取,请注意甄别。
__weak_ptr是std::weak_ptr的底层实现类,用于管理对资源的弱引用。在__weak_ptr类中有两个核心的成员属性_M_ptr和_M_refcount。如下所示: template class __weak_ptr { // … private: element_type* _M_ptr; // Contained pointer. __weak_count<_Lp> _M_refcount; // Reference counter. } 以上代码只是部分截取,请注意甄别。 在__weak_count中又封装了对_Sp_counted_base的引用计数器的管理,确保引用计数的生命周期被正确跟踪。如下所示: template<_Lock_policy _Lp> class __weak_count { // … private: _Sp_counted_base<_Lp>* _M_pi; }; 以上代码只是部分截取,请注意甄别。
- 当创建一个
std::weak_ptr时,调用其构造函数,同时也调用__weak_ptr的构造函数。如下所示: weak_ptr(const shared_ptr<_Yp>& __r) noexcept : __weak_ptr<_Tp>(__r) { } __weak_ptr(const __shared_ptr<_Yp, _Lp>& __r) noexcept : _M_ptr(__r._M_ptr), _M_refcount(__r._M_refcount) { } 初始化数据指针_M_ptr,如果当前对象确实管理了一个有效的资源则增加弱引用计数_M_weak_count。如下所示: __weak_count(const __shared_count<_Lp>& __r) noexcept : _M_pi(__r._M_pi) { if (_M_pi != nullptr) _M_pi->_M_weak_add_ref(); } 调用_M_weak_add_ref函数增加弱引用计数_M_weak_count。这个方法是线程安全的,确保在多线程环境中正确地更新弱引用计数。如下所示: void _M_weak_add_ref() noexcept { __gnu_cxx::__atomic_add_dispatch(&_M_weak_count, 1); } - 当销毁或者或重置
std::weak_ptr时,__weak_count的析构函数会被调用。如下所示: ~__weak_count() noexcept { if (_M_pi != nullptr) _M_pi->_M_weak_release(); } 在_M_weak_release函数中会调用__exchange_and_add_dispatch函数将_M_weak_count弱引用计数器减一,如果当弱引用计数为 0 时,则调用_M_destroy方法。如下所示: void _M_weak_release() noexcept { // … if (__gnu_cxx::__exchange_and_add_dispatch(&_M_weak_count, -1) == 1) { // … _M_destroy(); } }
以上代码只是部分截取,请注意甄别。
_M_destroy是_Sp_counted_base类中的一个关键方法,它确保在引用计数为 0 时,资源被正确释放。 virtual void _M_destroy() noexcept { delete this; }
常用操作
检查引用对象是否被销毁
expired 函数用于检查 std::weak_ptr 所引用的对象是否已经被销毁。如果 std::weak_ptr
所引用的对象已经被销毁,则返回 true;否则返回 false。
bool expired() const noexcept
{
return _M_refcount._M_get_use_count() == 0;
}
具体 _M_get_use_count 方法内容, 请参考 获取引用计数 函数解析。
获取 std::shared_ptr
lock函数尝试获取一个std::shared_ptr,指向std::weak_ptr所引用的对象。如果对象仍然存在,返回一个std::shared_ptr,共享对象的所有权;如果对象已经被销毁,返回一个空的std::shared_ptr。- shared_ptr<_Tp> lock() const noexcept
- {
- return shared_ptr<_Tp>(*this, std::nothrow);
- }
- 直接调用了
std::shared_ptr的构造函数,紧接着又调用了__shared_ptr的构造函数。如下所示: - shared_ptr(const weak_ptr<_Tp>& __r, std::nothrow_t) noexcept
- __shared_ptr<_Tp>(__r, std::nothrow) { }
在该函数中,首先从
__r的引用计数中初始化当前std::shared_ptr的引用计数。然后设置数据指针,当_M_get_use_count返回非零值(即资源仍然存在),则_M_ptr被设置为__r._M_ptr;否则就是资源已经被销毁_M_ptr被设置为nullptr。 __shared_ptr(const __weak_ptr<_Tp, _Lp>& __r, std::nothrow_t) noexcept - _M_refcount(__r._M_refcount, std::nothrow) { _M_ptr = _M_refcount._M_get_use_count() ? __r._M_ptr : nullptr; }
重置
reset 函数将 std::weak_ptr 置为空,表示不再引用任何对象,并不会影响引用计数。
void reset() noexcept
{
__weak_ptr().swap(*this);
}
首先调用默认构造函数创建 std::weak_ptr 对象,然后调用 swap 函数进行交换。
交换
swap 函数用于交换两个 std::weak_ptr 的内容,并不会影响引用计数。
void swap(__weak_ptr& __s) noexcept
{
std::swap(_M_ptr, __s._M_ptr);
_M_refcount._M_swap(__s._M_refcount);
}
调用标准库中的 swap 函数交换底层数据指针,然后再调用 _M_swap 函数交换引用计数器。如下所示:
void _M_swap(__weak_count& __r) noexcept
{
_Sp_counted_base<_Lp>* __tmp = __r._M_pi;
__r._M_pi = _M_pi;
_M_pi = __tmp;
}
获取引用计数
use_count 函数返回 std::weak_ptr 所引用的对象的引用计数,表示有多少个 std::shared_ptr
共享该对象,如果对象已经被销毁,返回0。
long use_count() const noexcept
{
return _M_refcount._M_get_use_count();
}
在 _M_get_use_count 函数中判断 _M_pi 是否为 nullptr 如果为空则表示没有管理任何资源直接返回 0;否则调用
_M_get_use_count 函数获取引用计数器的值。如下所示:
long _M_get_use_count() const noexcept
{
return _M_pi != nullptr ? _M_pi->_M_get_use_count() : 0;
}
通过原子操作获取关联的强引用计数器的值。如下所示:
long _M_get_use_count() const noexcept
{
return __atomic_load_n(&_M_use_count, __ATOMIC_RELAXED);
}
其中,__ATOMIC_RELAXED 是最弱的内存顺序模型,仅保证操作的原子性,不提供任何内存顺序保证。
🌺🌺🌺撒花!
如果本文对你有帮助,就点关注或者留个👍
如果您有任何技术问题或者需要更多其他的内容,请随时向我提问。
******************