C++ 智能指针浅析

为了解决 C++ 中内存管理这个老大难问题,C++ 11 中提供了三种可用的智能指针。(早期标准库中还存在一种 auto_ptr,但由于设计上的缺陷,已经被 unique_ptr 取代了)

智能指针不仅能用来管理动态内存,还能用来管理其他类型的资源,比如互斥锁、数据库连接等,这种用资源管理对象来管理资源的思想被称为 RAII。

原始指针的缺陷

  • 看不出来指向的是对象还是数组,也就不知道该用 delete 还是 delete[]
  • 不知道用完后是否应该销毁,也就是不包含所有权的信息;
  • 不知道该用 delete 还是其他方式释放该对象;
  • double free;
  • dangling pointers(悬空指针),即对象已经被释放了,但指针还指向它。

shared_ptr

shared_ptr 体现的是共享的所有权(shared ownership),通俗地讲,就是这个被管理的对象可以被多个用户使用,只有所有用户都不再需要该资源时,它才可以被释放。

为了实现共享的所有权,shared_ptr 会维护一个引用计数,表示有多少 shared_ptr 指向这个被管理的对象。

简单地来说,当 shared_ptr 被拷贝时(不管是我们主动调用,还是将它作为传值参数或返回值),引用计数就会增加;当 shared_ptr 被析构时(比如离开它的作用域),引用计数就会减少。当引用计数归零时,这个被管理的对象就会被释放。

shared_ptr 中引用计数的操作是原子的,这点和 shared_ptr 的线程安全性有关。

shared_ptr 使用规范

要正确地使用 shared_ptr,需要注意一些问题:

  • 不使用相同的内置指针初始化或 reset 多个 shared_ptr,原因请看后文的 shared_ptr 实现,这样操作会产生多个控制块对象,从而造成 double free;
  • get() 返回的原始指针只应该有一种用途,用来作为参数传递给遗留接口;
    • 不应该 delete get() 返回的指针;
    • 不使用 get() 的返回值去初始化或 reset 另一个智能指针;
    • 当最后一个对应的智能指针被销毁时,get() 返回的指针就无效了;
  • 如果使用智能指针管理资源而不是内存,应当定义自定义删除器;
  • 如果将 shared_ptr 存放到容器中,之后不再需要其中的部分元素,应当调用 erase 将它们删除,不然这些元素的生命周期会被意外地延长。

在单条语句里将 new 来的指针放到智能指针里

  1. int priority();
  2. void processWidget(std::shared_ptr<Widget> pw, int priority);
  3. processWidget(std::shared_ptr<Widget>(new Widget), priority());

该调用事实上由三部分组成:

  1. new Widget
  2. 调用 shared_ptr 构造函数
  3. 调用 priority()

1 应当发生在 2 之前,但标准并没有规定 3 要在何时发生;如果编译器以 1、3、2 这样的顺序来执行,那么如果调用 priority() 时抛出异常,那么就会出现内存泄漏。

如果将程序改为:

  1. std::shared_ptr<Widget> pw(new Widget);
  2. processWidget(pw, priority());

可以强制上面的过程以 1、2、3 的顺序发生,消除了内存泄漏的隐患。

此外,将这里改成 std::move() 效率更高,因为移动不涉及引用计数的增减。

  1. std::shared_ptr<Widget> pw(new Widget);
  2. processWidget(std::move(pw), priority());

避免该内存泄露问题的另一种方法是改用 make_shared()

  1. processWidget(std::make_shared<Widget>(), priority());

make_shared

使用 make_shared() 的优势:

  • 避免上面提到的内存泄露问题;
  • 效率更高:先 new 出内存再传递给 shared_ptr,涉及到两次内存分配,一次分配被管理的对象,一次分配控制块对象;但如果改用 make_shared(),只需要分配一次内存就足够容纳这两个对象。

使用 make_shared() 的缺陷:

  • 不允许指定自定义删除器。
  • 没法用 {} 初始化,make_shared() 将参数完美转发给构造函数时,默认使用 ()
  • 如果类重载了 operator newoperator delete,它的内存分配行为可能表现出特殊的行为;
    • 比如 Widget 类的 operator newoperator delete 可能只处理大小为 sizeof(Widget) 的内存分配;
    • make_shared() 不只是分配对象的动态内存,还会同时分配控制块的内存,这种情况下,就会造成麻烦。
  • 由于控制块和对象放在同一块内存里,引用计数为 0 的时候,对象并不会被释放,只有等到控制块也不再需要的时候,这块内存才会被释放;
    • 只要还有 weak_ptr 指向这个对象,控制块就不会被释放;
    • 对于特别大的对象,这可能是问题。

unique_ptr

unique_ptr 体现的是独占/排他的所有权(exclusive ownership),通俗地讲,就是这个被管理的对象为单个用户所有,当该用户不再需要这个资源时,它就会被释放掉;同时,该用户还可以将资源的所有权移交给其他用户。

unique_ptr 的拷贝构造和赋值是删除的,提供了移动构造和赋值,因此要想将持有的独占资源交给其他 unique_ptr,往往需要显式地调用 std::move() 来触发移动操作。

UML 中的组合(或称复合,Composition)关系,就可以使用 unique_ptr 来建模,当整体被析构时,部分也会跟着析构。

unique_ptr 可以转换为 shared_ptr,因此 unique_ptr 很适合作为工厂函数的返回值,因为你不知道使用者需要的所有权语义究竟是独占的还是共享的;相反,一旦所有权交给了 shared_ptr,就没法再转换为独占的所有权了。

C++11 中没有类似于 make_shared() 的标准库函数,应当将 new 返回的指针传递给 unique_ptr 的构造函数;C++14 中加入了 make_unique()

unique_ptr 的大小

  • 使用默认删除器的情况下,unique_ptr 内部只维护了一个指针,和原始指针大小相同

由于 unique_ptr 直接将自定义删除器存储在对象内部

  • 使用函数指针作为删除器,大小为变为两倍的原始指针
  • 使用函数对象作为删除器
    • 无状态的函数对象,比如不捕获变量的 lambda 表达式,不影响 uniqur_ptr 的大小
    • 否则,随着函数对象的大小增加而增加

为什么不捕获变量的 lambda 表达式在 unique_ptr 里不占空间?

实现上,lambda 表达式其实是个匿名类对象,如果它不捕获任何变量,就是个空的类,C++ 规定任何对象和对象成员的大小都至少是 1,即使该对象的类型是一个空的类,这就能保证相同类型的不同对象的地址总是不同的。

  1. class Empty {};
  2. class HoldsAnInt {
  3. private:
  4. int x;
  5. Empty e;
  6. };

由于上述规定的存在,导致 sizeof(HoldsAnInt) > sizeof(int)

但该规定存在例外,那就是这个空的类作为基类的时候,是不需要占据空间的。这种优化被称为空基类优化(empty base optimization,EBO)。

  1. class HoldsAnInt: private Empty {
  2. private:
  3. int x;
  4. };

这种情况下,sizeof(HoldsAnInt) == sizeof(int)

我们查看 unique_ptr 的源码,会发现它是这样存储指针和删除器的:

  1. tuple<pointer, _Dp> _M_t;

gcc 的 tuple 的实现上应用 EBO 做空间压缩,所以不捕获变量的 lambda 表达式作为删除器时,unique_ptr 的空间不会变大。

对于程序员来说,用不捕获变量的 lambda 表达式做删除器是非常常见的情况,所以这是一个很有价值的优化。

auto_ptr 的缺陷

auto_ptr 是早期标准库中实现的一种智能指针,具有 unique_ptr 的部分特性,它用拷贝来模拟资源所有权的移交,拷贝 auto_ptr 会导致源 auto_ptr 被设为 null,这种行为是比较匪夷所思的。

  1. std::auto_ptr<Invesetment> pInv2(pInv1); // pInv1 被设为 null
  2. pInv1 = pInv2; // pInv2 被设为 null

假设将 auto_ptr 存储到容器里,并使用泛型算法对容器做操作,由于泛型算法可能将元素拷贝到局部临时对象中,会导致操作后的容器里存在大量空的 auto_ptr,因此 auto_ptr 不能和容器一起使用。

和 unique_ptr 作比较,unique_ptr 仅允许移动操作,而禁止了拷贝操作,语义更加清晰且安全。

自定义删除器

shared_ptr 和 unique_ptr 都允许自定义删除器,区别在于 unique_ptr 的自定义删除器是模板参数的一部分,而 shared_ptr 仅仅是作为构造函数的一个参数。

  1. auto loggingDel = [](Widget *pw) // 自定义删除器
  2. {
  3. makeLogEntry(pw);
  4. delete pw;
  5. };
  6. // 删除器类型是指针类型的一部分
  7. std::unique_ptr<Widget, decltype(loggingDel)> upw(new Widget, loggingDel);
  8. // 删除器类型不是指针类型的一部分
  9. std::shared_ptr<Widget> spw(new Widget, loggingDel);

不包含删除器类型模板参数给了 shared_ptr 更大的灵活性,比如 std::shared_ptr<Widget> 的容器中可以放删除器不同的各种 Widget 对象。

shared_ptr 的大小不会随着自定义删除器的大小而增加,因为这部分是放在它的控制块对象里;但 uniqur_ptr 是直接将删除器存到指针对象内了,因此它的大小是会随着自定义删除器的大小变化的。

智能指针和动态数组

shared_ptr 和动态数组

shared_ptr 不支持动态数组的管理,将动态数组传递给它后,它仍会使用 delete,而不是 delete[] 去释放这块内存,因此必须提供自己的删除器。

可以直接使用 default_delete 类型作为自定义删除器,这个类提供了针对数组类型的偏特化,会调用 delete[] 来释放内存。

  1. std::shared_ptr<int> sp3(new int[10](), std::default_delete<int[]>());

同时,由于 shared_ptr 没有提供下标运算符,要访问数组中的元素,必须通过 get() 获取内置指针,用它来访问数组元素。

作为替代,可以使用 boost 提供的 boost::scoped_arrayboost::shared_array 来管理动态数组。

C++17 的改进

支持直接用数组作为模板参数,它会正确地调用 delete[],且增加了下标运算符:

  1. std::shared_ptr<int[]> sp3(new int[10]());
  2. int i = sp3[2];

使用 unique_ptr 管理动态数组

在类型名后面加 [],它会自动调用 delete[] 来释放内存,且可以使用下标运算符访问其中的元素。

  1. unique_ptr<int[]> int_array(new int[10]{ 1, 2, 3 });
  2. cout << int_array[1] << endl;

但比起使用 unique_ptr,更应该优先考虑 std::arraystd::vectorstd::string 等数据容器。

RAII

RAII 简介

Resource Acquisition Is Initialization (RAII),即获取到资源后立刻移交给资源管理对象。资源管理对象使用它们的析构函数确保资源被释放。这里说的资源可以是内存、数据库连接、互斥锁等各种形式的资源。

假设有一个 C 风格 API 的 Mutex 类:

  1. void lock(Mutex *pm);
  2. void unlock(Mutex *pm);

Mutex 的资源管理类(C++11 中类似的类叫 std::lock_guard)可以定义为:

  1. class LockGuard
  2. {
  3. public:
  4. explicit LockGuard(Mutex* pm) :mutexPtr(pm) { lock(mutexPtr); }
  5. ~LockGuard() { unlock(mutexPtr); }
  6. private:
  7. Mutex* mutexPtr;
  8. };

对想要写出异常安全代码的程序员来说,RAII 非常重要,因为如果在代码中显式地释放资源,就可能出现因为异常抛出而资源没能正确释放地情况,比如下面这段代码,如果 new 抛异常,mutex 就不会被解锁。

  1. void PrettyMenu::changeBackground(std::istream& imgSrc)
  2. {
  3. lock(&mutex);
  4. delete bgImage;
  5. ++imageChanges;
  6. bgImage = new Image(imgSrc);
  7. unlock(&mutex);
  8. }

但如果改为用 RAII 的方式来管理资源,如果有异常被抛出,栈展开的过程中,资源管理对象就会被析构掉,从而释放掉被管理的资源。

  1. void PrettyMenu::changeBackground(std::istream& imgSrc)
  2. {
  3. Lock ml(&mutex);
  4. delete bgImage;
  5. ++imageChanges;
  6. bgImage = new Image(imgSrc);
  7. }

用智能指针实现 RAII

C++11 后,可以通过智能指针来实现 RAII,对于共享资源,将它交给 shared_ptr,而对于独占资源,将它交给 unique_ptr,同时通过使用自定义删除器,来决定资源该怎样被释放。

shared_ptr 的实现

shared_ptr 实现上包含两个指针:一个指向被管理的对象,一个指向动态分配的控制块对象。控制块对象里包含:

  • 引用计数(use_count),用来判断是否可以释放被管理的对象;
  • 弱引用计数(weak_count),和 weak_ptr 的实现有关;
  • 其他数据,比如自定义删除器。

模拟隐式转换行为

智能指针可以模拟指针的隐式转换行为,自动将派生类指针转换为基类指针,为了做到这点,需要为智能指针增加一个泛化拷贝构造函数:

  1. template<typename T>
  2. class SmartPtr {
  3. public:
  4. template<typename U>
  5. SmartPtr(const SmartPtr<U>& other): heldPtr(other.get())
  6. {
  7. //...
  8. }
  9. T* get() const { return heldPtr; }
  10. //...
  11. private:
  12. T *heldPtr;
  13. };

这个泛化拷贝构造函数不是 explicit 的,以支持隐式转换;同时它的初始化列表告诉我们只有能隐式转换为 T* 类型的 U* 才能通过编译。

源码分析

下面包含一些 gcc 的源码分析,不想看代码的读者可以直接跳过本章。

gcc 中控制块对象的类叫 _Sp_counted_base,包含两个引用计数,它们都是原子类型:

  1. template<_Lock_policy _Lp = __default_lock_policy>
  2. class _Sp_counted_base : public _Mutex_base<_Lp>
  3. {
  4. // ...
  5. _Atomic_word _M_use_count; // #shared
  6. _Atomic_word _M_weak_count; // #weak + (#shared != 0)
  7. // ...
  8. };

shared_ptr 中会保存指向 _Sp_counted_base 的指针。

  1. _Sp_counted_base<_Lp>* _M_pi;

之前我们说过 shared_ptr 的模板参数中是不包含自定义删除器类型的,这就导致 deleter 没法保存在 shared_ptr 内部。

  1. template< class T > class shared_ptr; // shared_ptr 的声明
  2. template< class Y, class Deleter >
  3. shared_ptr( Y* ptr, Deleter d ); // 带自定义删除器的构造函数

很显然,deleter 只能存放在控制块对象中。为此,_Sp_counted_base 有一个带删除器模板参数的派生类 _Sp_counted_deleter

  1. template<typename _Ptr, typename _Deleter, typename _Alloc, _Lock_policy _Lp>
  2. class _Sp_counted_deleter final : public _Sp_counted_base<_Lp>

这些类之间的关系如下图所示:

当创建 shared_ptr 时,会动态分配 _Sp_counted_deleter 对象,并将自定义删除器存到它里面。

  1. template<typename _Ptr, typename _Deleter, typename _Alloc,
  2. typename = typename __not_alloc_shared_tag<_Deleter>::type>
  3. __shared_count(_Ptr __p, _Deleter __d, _Alloc __a) : _M_pi(0)
  4. {
  5. typedef _Sp_counted_deleter<_Ptr, _Deleter, _Alloc, _Lp> _Sp_cd_type;
  6. __try
  7. {
  8. typename _Sp_cd_type::__allocator_type __a2(__a);
  9. auto __guard = std::__allocate_guarded(__a2);
  10. _Sp_cd_type* __mem = __guard.get();
  11. ::new (__mem) _Sp_cd_type(__p, std::move(__d), std::move(__a));
  12. _M_pi = __mem;
  13. __guard = nullptr;
  14. }
  15. __catch(...)
  16. {
  17. __d(__p); // Call _Deleter on __p.
  18. __throw_exception_again;
  19. }
  20. }

shared_ptr 析构的时候,_M_pi->_M_release() 会被调用,(原子地)减少引用计数:

  1. void _M_release() // nothrow
  2. {
  3. _GLIBCXX_SYNCHRONIZATION_HAPPENS_BEFORE(&_M_use_count);
  4. if (__gnu_cxx::__exchange_and_add_dispatch(&_M_use_count, -1) == 1) {
  5. _GLIBCXX_SYNCHRONIZATION_HAPPENS_AFTER(&_M_use_count);
  6. _M_dispose();
  7. if (_Mutex_base<_Lp>::_S_need_barriers) {
  8. __atomic_thread_fence(__ATOMIC_ACQ_REL);
  9. }
  10. _GLIBCXX_SYNCHRONIZATION_HAPPENS_BEFORE(&_M_weak_count);
  11. if (__gnu_cxx::__exchange_and_add_dispatch(&_M_weak_count, -1) == 1) {
  12. _GLIBCXX_SYNCHRONIZATION_HAPPENS_AFTER(&_M_weak_count);
  13. _M_destroy();
  14. }
  15. }
  16. }

如果 use_count 减少到 0,调用虚函数 _M_dispose(),其实调用到的是_Sp_counted_deleter 的实现,由此,自定义删除器被用来销毁被管理的对象。

如果 weak_count 减少到 0,调用 _M_destroy(),销毁控制块对象。

  1. virtual void _M_dispose() noexcept
  2. {
  3. _M_impl._M_del()(_M_impl._M_ptr);
  4. }
  5. virtual void _M_destroy() noexcept
  6. {
  7. __allocator_type __a(_M_impl._M_alloc());
  8. __allocated_ptr<__allocator_type> __guard_ptr{ __a, this };
  9. this->~_Sp_counted_deleter();
  10. }

在不定义自定义删除器的情况下,如果被管理对象是是非数组类型,_M_pi 指向_Sp_counted_deleter 的默认实现 _Sp_counted_ptr,它直接调 delete 析构被管理对象。

  1. virtual void _M_dispose() noexcept
  2. {
  3. delete _M_ptr;
  4. }
  5. virtual void _M_destroy() noexcept
  6. {
  7. delete this;
  8. }

但如果被管理对象是是数组类型,自定义删除器将被设置为 __sp_array_delete,它调用 delete[] 来析构对象。

  1. struct __sp_array_delete
  2. {
  3. template<typename _Yp> void operator() (_Yp* __p) const { delete[] __p; }
  4. };

shared_ptr 的性能分析

  • 大小是原始指针的两倍,因为它要同时持有指向被管理对象的指针和控制块对象的指针;
  • 引用计数的内存必须是动态分配的,可以用 make_shared 来对这点做优化;
  • 引用计数操作是原子的,原子操作比非原子操作要慢;
  • 控制块对象包含继承关系,为了正确的销毁对象使用了虚函数,因此对象销毁的时候要承担虚函数带来的开销。

移动和引用计数

用一个 shared_ptr 移动构造另一个 shared_ptr 不会导致引用计数的变化,新的 shared_ptr 会直接接管老的 shared_ptr 的控制块对象,因此 shared_ptr 的移动操作要比拷贝操作高效。

weak_ptr

weak_ptr 不负责对象的生命周期管理,它指向一个由 shared_ptr 管理的对象,但不改变它的引用计数(use_count)。通过 weak_ptr 可以判它所指向的对象是否已经被销毁。有两种方法可以做这种判断:

  • 尝试用 lock() 提升它为 shared_ptr,如果过期,返回的结果为空;
  • 用它构造一个新的 shared_ptr,如果过期,会抛 std::bad_weak_ptr 异常。
  1. auto spw2 = wpw.lock();
  2. std::shared_ptr<Widget> spw3(wpw);

weak_ptr 的实现

weak_ptr 的实现和 shared_ptr 的控制块对象相关。

由于 weak_ptr 想要知道 shared_ptr 管理的对象是否已经被析构了,那么最直接的手段就是让它访问 shared_ptr 的控制块对象,查看 use_count 是否已经是 0。

控制块对象的生命周期必须延续到所有 shared_ptr 和 weak_ptr 都已经析构掉,它才可以析构。所以需要一个控制块对象的引用计数来管理它的生命周期,即 weak_count。

weak_ptr 的使用场景

shared_ptr 可能悬空的情况下,都可以使用 weak_ptr 来做 shared_ptr 是否悬空的判断。比如 UML 中的聚合(aggregation)关系,一般由整体持有部分的 shared_ptr,部分持有整体的 weak_ptr,并使用升级操作,来判断整体是否还存活。

带缓存的工厂函数

假设说有一个开销很大的工厂函数,它针对不同的 ID 产生不同的产品,但该工厂函数的开销很大,我们希望能缓存下来它的产品:

  1. std::shared_ptr<const Widget> loadWidget(WidgetID id);

我们希望当工厂函数客户端不再需要该缓存对象时,该对象可以被析构,因此我们用哈希表存储对象的 weak_ptr 来做缓存,获取对象时,先尝试升级缓存中的 weak_ptr,如果能成功,说明对象仍未被释放,可以节省调用工厂函数的开销:

  1. std::shared_ptr<const Widget> fastLoadWidget(WidgetID id)
  2. {
  3. static std::unordered_map<WidgetID,
  4. std::weak_ptr<const Widget>> cache;
  5. auto objPtr = cache[id].lock();
  6. if (!objPtr) {
  7. objPtr = loadWidget(id);
  8. cache[id] = objPtr;
  9. }
  10. return objPtr;
  11. }

观察者模式

观察者模式允许我们实现一种“发布-订阅”机制,当对象的状态发生变化时,通知正在“观察”该对象的其他对象。

观察者模式中有两种角色:subject 或 publisher 是状态会发生变化的对象;observer 或 subscriber 是在 subject 状态发生变化时要(通过调用 observer 的 update 接口)通知的对象。

如果使用裸指针来保存 observer,有可能发生的是 observer 已经被析构,但 subject 还不知道,又调用了 observer 的 update 方法,导致未定义行为。

解决方法是用 weak_ptr 保存 observer,在调用 update 前先升级为 shared_ptr,如果 observer 仍存活就调用 update,否则将它从 observer 列表中删除。

解决循环引用

在上图中,如果 B 也有指向 A 的引用,这时就不能选择 shared_ptr,因为如果有多个 shared_ptr 的引用关系形成环状,会导致这些指针的引用计数永远都不会归零,结果就是这些对象永远都不会被释放,即使程序中的其他部分已经不持有指向它们的智能指针。

如果我们使用裸指针,就可能出现 A 已经被析构了,B 还尝试调用它的方法,导致未定义行为。

解决方法:将引用链中的一个指针改为使用 weak_ptr 从而打破环状关系。

shared_ptr 的线程安全性

智能指针本身的线程安全

如果多个线程在不同的 shared_ptr 对象(即使这些对象是拷贝出来的,共享同一个对象的所有权)上不加同步地调用任何成员函数(包括拷贝和赋值),都是线程安全的。

如果多个线程不加同步地访问同一个 shared_ptr 对象,并且有线程使用了非 const 的成员函数(拷贝和赋值也是非 const 的),那么会发生数据竞争。虽然智能指针针对引用计数的操作是原子的,但是非 const 的成员函数往往涉及多个操作,这些操作组合在一起不是原子的。

被管理对象的线程安全

使用多个不同的 shared_ptr 对象访问同一个被管理对象,如果对该对象的操作本身不是线程安全的,那么即使通过智能指针访问,也会发生数据竞争,智能指针并不对被管理对象的线程安全性负责。

enable_shared_from_this

之前我们说过,当你持有一个 shared_ptr ,绝不应该再从它的原始指针(不管是 new 出来的还是通过 get() 得到的)中创建出一个新的 shared_ptr ,因为它们的引用计数是不共享的,会带来 double free 的问题。对于这点,即使是 this 指针也不例外。

  1. struct S
  2. {
  3. shared_ptr<S> dangerous()
  4. {
  5. return shared_ptr<S>(this); // don't do this!
  6. }
  7. };
  8. int main()
  9. {
  10. shared_ptr<S> sp1(new S);
  11. shared_ptr<S> sp2 = sp1->dangerous();
  12. return 0;
  13. }

在这个例子中,智能指针 sp1 和 sp2 指向的是同一个底层对象,但 sp1 和 sp2 对此一无所知,它们各自管理自己的引用计数,因此这块内存会被释放两次。

要避免此问题,可以使用类模板 enable_shared_from_this,以它的派生类作为模板实参,这种继承的模式被称为 curiously recurring template pattern(CRTP)。

  1. struct S : enable_shared_from_this<S>
  2. {
  3. shared_ptr<S> not_dangerous()
  4. {
  5. return shared_from_this();
  6. }
  7. };
  8. int main()
  9. {
  10. shared_ptr<S> sp1(new S);
  11. shared_ptr<S> sp2 = sp1->not_dangerous();
  12. return 0;
  13. }

使用场景

有时候对象可能需要获得一个指向自己的 shared_ptr,以传递给 lambda 表达式或消息队列的时候,就需要调用 shared_from_this()

  1. boost::asio::async_write(socket_, boost::asio::buffer(message_),
  2. boost::bind(&tcp_connection::handle_write, shared_from_this()));

上面这个例子来自 asio 的教程,asio 服务器会为每个客户端连接动态创建一个 tcp_connection 对象(包含连接的上下文信息),并在上面执行异步操作,所以必须保证 tcp_connection 对象存活到回调函数被调用的时候。

但由于异步操作的特点,你不知道回调函数 handle_write 什么时候会被调用,所以很难直接去管理 tcp_connection 的生命周期,通常的做法是用 shared_ptr 来管理它,并且让异步操作捕获一份它的 shared_ptr(不管是通过 std::bind 还是 lambda 表达式的捕获列表),这样只要异步操作还未执行,tcp_connection 对象就不会被析构。

如果在 tcp_connection 的成员函数中要执行异步操作,就必须捕获一个指向自己的 shared_ptr,这时候就需要调用 shared_from_this(),因此 tcp_connection 必须继承 enable_shared_from_this。

使用限制

必须通过 shared_ptr 来调用 shared_from_this(),下面的代码是错误的:

  1. int main()
  2. {
  3. S *p = new S;
  4. shared_ptr<S> sp2 = p->not_dangerous(); // don't do this
  5. }

此外,shared_from_this() 不能在构造函数里被调用。

参考材料

  • [1] C++ Primer
  • [2] Effective Modern C++
  • [3] Effective C++
  • [4] Linux多线程服务端编程 使用muduo C++网络库

C++ 智能指针浅析的更多相关文章

  1. c++学习笔记—动态内存与智能指针浅析

    我们的程序使用内存包含以下几种: 静态内存用来保存局部static对象.类static数据成员以及定义在任何函数之外的变量,在使用之前分配,在程序结束时销毁. 栈内存用来保存定义在函数内部的非stat ...

  2. enote笔记法使用范例(2)——指针(1)智能指针

    要知道什么是智能指针,首先了解什么称为 “资源分配即初始化” what RAII:RAII—Resource Acquisition Is Initialization,即“资源分配即初始化” 在&l ...

  3. C++11 shared_ptr 智能指针 的使用,避免内存泄露

    多线程程序经常会遇到在某个线程A创建了一个对象,这个对象需要在线程B使用, 在没有shared_ptr时,因为线程A,B结束时间不确定,即在A或B线程先释放这个对象都有可能造成另一个线程崩溃, 所以为 ...

  4. C++智能指针

    引用计数技术及智能指针的简单实现 基础对象类 class Point { public: Point(int xVal = 0, int yVal = 0) : x(xVal), y(yVal) { ...

  5. EC笔记:第三部分:17、使用独立的语句将newed对象放入智能指针

    一般的智能指针都是通过一个普通指针来初始化,所以很容易写出以下的代码: #include <iostream> using namespace std; int func1(){ //返回 ...

  6. 智能指针shared_ptr的用法

    为了解决C++内存泄漏的问题,C++11引入了智能指针(Smart Pointer). 智能指针的原理是,接受一个申请好的内存地址,构造一个保存在栈上的智能指针对象,当程序退出栈的作用域范围后,由于栈 ...

  7. 智能指针unique_ptr的用法

    unique_ptr是独占型的智能指针,它不允许其他的智能指针共享其内部的指针,不允许通过赋值将一个unique_ptr赋值给另一个unique_ptr,如下面错误用法: std::unique_pt ...

  8. 基于C/S架构的3D对战网络游戏C++框架 _05搭建系统开发环境与Boost智能指针、内存池初步了解

    本系列博客主要是以对战游戏为背景介绍3D对战网络游戏常用的开发技术以及C++高级编程技巧,有了这些知识,就可以开发出中小型游戏项目或3D工业仿真项目. 笔者将分为以下三个部分向大家介绍(每日更新): ...

  9. C++ 引用计数技术及智能指针的简单实现

    一直以来都对智能指针一知半解,看C++Primer中也讲的不够清晰明白(大概是我功力不够吧).最近花了点时间认真看了智能指针,特地来写这篇文章. 1.智能指针是什么 简单来说,智能指针是一个类,它对普 ...

随机推荐

  1. dotnet new 命令笔记

    让dotnet new使用平台特定的目标,例如net6.0-windows10.0.19041.0 dotnet new console --name CallWinRTConsole --frame ...

  2. 《头号玩家》AI电影调研报告(二)

    四. 涉及前沿技术及与现实的交互 1.VR技术 在影片中,斯皮尔伯格用他认为未来的VR虚拟技术为我们创造了众多精彩的画面,令人佩服其对科技的预见性.其中好多的装备特别引人注目,部分也在现实中存在:VR ...

  3. iOS开发 将html 富文本文字 转换成oc 的富文本

    - (NSMutableAttributedString *)mf_htmlAttribute:(NSString *)htmlString{ htmlString = [NSString strin ...

  4. CPU乱序执行问题

    CPU为了提高执行效率,会在一条指令执行的过程中(比如去内存读数据,读数据的过程相较于CPU的执行速度慢100倍以上,cpu处于等待状态),这个时候cpu会分析接下来的指令是否正在执行的指令相关联,如 ...

  5. RESTFUL风格的接口命名规范

    1.首先restfulf风格的api是基于资源的,url命名用来定位资源,而不是表示动作,动作通过请求方式进行表示. 2.URL中应该单复数区分,推荐的实践是永远只用复数.比如GET /api/use ...

  6. 2021.12.21 eleveni的刷题记录

    2021.12.21 eleveni的刷题记录 0. 有意思的题 P6701 [POI1997] Genotype https://www.luogu.com.cn/problem/P6701 状压优 ...

  7. Homomorphic Evaluation of the AES Circuit:解读

    之前看过一次,根本看不懂,现在隔这么久,再次阅读,希望有所收获! 论文版本:Homomorphic Evaluation of the AES Circuit(Updated Implementati ...

  8. 攻防世界-MISC:pdf

    这是攻防世界新手练习区的第二题,题目如下: 点击附件1下载,打开后发现是一个pdf文件,里面只有一张图片 用WPS打开,没发现有什么不对的地方,参考一下WP,说是要转为word格式.随便找一个在线转换 ...

  9. [STL] deque 双端队列

  10. ubuntu 16.04,ros kinetic 使用husy_gazebo

    我当前使用的是ubuntu 16.04,ros kinetic ,Gazebo版本为7.0.protoc需要确保版本为2.6.1,而我当前的为3.4.0,因此需要将系统中的protoc替换为2.6.1 ...