做者 Eatonios
导语 在 C++ 中,内存管理是十分重要的问题,一不当心就会形成程序内存泄露,那么怎么避免呢?经过智能指针能够优雅地管理内存,让开发者只须要关注内存的申请,内存的释放则会被自动管理。在文章 开源微服务框架 TARS 之 基础组件 中已经简要介绍过,TARS 框架组件中没有直接使用 STL 库中的智能指针,而是实现了本身的智能指针。本文将会分别对 STL 库中的智能指针和 TarsCpp 组件中的智能指针进行对比分析,并详细介绍 TARS 智能指针的实现原理。git
智能指针github
STL 库中的智能指针segmentfault
在计算机程序中,泄露是常见的问题,包括内存泄露和资源泄露。其中资源泄露指的是系统的 socket
、文件描述符等资源在使用后,程序再也不须要它们时没有获得释放;内存泄露指的是动态内存在使用后,程序再也不须要它时没有获得释放。安全
内存泄露会使得程序占用的内存愈来愈多,而很大一部分每每是程序再也不须要使用的。在 C++ 程序中,内存泄露常见于咱们使用了 new
或者 malloc
申请动态存储区的内存,却忘了使用 delete
或者 free
去释放内存,形成系统内存的浪费,致使程序运行速度减慢甚至系统崩溃等严重后果。并发
随着计算机应用需求的日益增长,应用的设计与开发日趋复杂,开发人员在开发过程当中处理的变量也愈来愈多。如何有效进行内存分配和释放、防止内存泄漏逐渐成为开发者面临的重要难题。为了解决忘记手动释放内存形成的内存泄露问题,智能指针诞生了。框架
常见的智能指针的使用场景,包括类中的成员变量(指针型)和普通的变量(指针型)。智能指针能够实现指针指向对象的共享,而无需关注动态内存的释放。通用实现技术是引用计数(Reference count),下一部分会介绍,简单讲就是将一个计数器与类指向的对象相关联,跟踪有多少个指针指向同一对象,新增一个指针指向该对象则计数器 +1
,减小一个则执行 -1
。socket
引用计数是智能指针的一种通用实现技术,上图为大体流程,基本原理以下:分布式
1
;+1
;=
将左操做数所指对象的引用计数 -1
,将右操做数所指对象的引用计数 +1
;-1
;0
时,删除基础对象;STL 库中的智能指针 shared_ptr
和 TARS 智能指针都使用了该引用计数原理,后面会进行介绍。
C++ 标准模板库 STL 中提供了四种指针 auto_ptr
, unique_ptr
, shared_ptr
, weak_ptr
。
auto_ptr
在 C++98 中提出,但其不能共享对象、不能管理数组指针,也不能放在容器中。所以在 C++11 中被摒弃,并提出 unique_ptr
来替代,支持管理数组指针,但不能共享对象。
shared_ptr
和 weak_ptr
则是 C++11 从标准库 Boost 中引入的两种智能指针。shared_ptr
用于解决多个指针共享一个对象的问题,但存在循环引用的问题,引入 weak_ptr
主要用于解决循环引用的问题。
接下来将详细介绍 shared_ptr
,关于其它智能指针的更多信息和用法请读者自行查阅。
shared_ptr
解决了在多个指针间共享对象全部权的问题,最初实现于 Boost 库中,后来收录于 C++11 中,成为了标准的一部分。shared_ptr
的用法以下
#include <memory> #include <iostream> using namespace std; class A { public: A() {}; ~A() { cout << "A is destroyed" << endl; } }; int main() { shared_ptr<A> sptrA(new A); cout << sptrA.use_count() << endl; { shared_ptr<A> cp_sptrA = sptrA; cout << sptrA.use_count() << endl; } cout << sptrA.use_count() << endl; return 0; }
上述代码的意思是 cp_sptrA
声明并赋值后,引用计数增长 1
,cp_sptrA
销毁后引用计数 -1
,可是没有触发 A
的析构函数,在 sprtA
销毁后,引用计数变为 0
,才触发析构函数,实现内存的回收。执行结果以下
1 2 1 A is destroyed
shared_ptr
主要的缺陷是遇到循环引用时,将形成资源没法释放,下面给出一个示例:
#include <memory> #include <iostream> using namespace std; class B; class A { public: A() : m_sptrB(nullptr) {}; ~A() { cout << " A is destroyed" << endl; } shared_ptr<B> m_sptrB; }; class B { public: B() : m_sptrA(nullptr) {}; ~B() { cout << " B is destroyed" << endl; } shared_ptr<A> m_sptrA; }; int main( ) { { shared_ptr<B> sptrB( new B );//sptrB对应的引用计数置为1 shared_ptr<A> sptrA( new A );//sptrA对应的引用计数置为1 sptrB->m_sptrA = sptrA;//sptrA对应的引用计数变成2,sptrB仍然是1 sptrA->m_sptrB = sptrB;//sptrB对应的引用计数变成2,sptrA是2 } //退出main函数后,sptrA和sptrB对应的引用计数都-1,变成1, //此时A和B的析构函数都不能执行(引用计数为0才能执行),没法释放内存 return 0; }
在上述例子中,咱们首先定义了两个类 A
和 B
:A
的成员变量是指向 B
的 shared_ptr
指针,B
的成员变量是指向 A
的 shared_ptr
指针。
而后咱们建立了 sptrB
和 sptrA
两个智能指针对象,而且相互赋值。这会形成环形引用,使得 A
和 B
的析构函数都没法执行(能够经过 cout
观测),从而内存没法释放。当咱们没法避免循环使用时,可使用 weak_ptr
来解决,这里再也不展开,感兴趣的读者能够自行查阅。
TARS 诞生于 2008 年,当时 shared_ptr
尚未被收录到 STL 标准库中,所以本身实现了智能指针 TC_AutoPtr
。TARS 的智能指针主要是对 auto_ptr
的改进,和 share_ptr
的思想基本一致,可以实现对象的共享,也能存储在容器中。与 shared_ptr
相比,TC_AutoPtr
更加轻量化,拥有更好的性能,本文后续会对比。
在 TARS 中,智能指针类 TC_AutoPtr
是一个模板类,支持拷贝和赋值等操做,其指向的对象必须继承自智能指针基类 TC_HandleBase
,包含了对引用计数的加减操做。计数采用的是 C++ 标准库 <atomic>
中的原子计数类型 std::atomic
。
计数的实现封装在类 TC_HandleBase
中,开发者无需关注。使用时,只要将须要共享对象的类继承 TC_HandleBase
,而后传入模板类 TC_AutoPtr
声明并构造对象便可,以下
#include <iostream> #include "util/tc_autoptr.h" using namespace std; // 继承 TC_HandleBase class A : public tars::TC_HandleBase { public: A() { cout << "Hello~" << endl; } ~A() { cout << "Bye~" << endl; } }; int main() { // 声明智能指针并构造对象 tars::TC_AutoPtr<A> autoA = new A(); // 获取计数 1 cout << autoA->getRef() << endl; // 新增共享 tars::TC_AutoPtr<A> autoA1(autoA); // 获取计数 2 cout << autoA->getRef() << endl; }
使用方式和 shared_ptr
类似,能够经过函数 getRef
获取当前计数,getRef
定义于 TC_HandleBase
类中。运行结果以下
Hello~ 1 2 Bye~
下面咱们将自底向上介绍分析原子计数器 std::atomic
、智能指针基类 TC_HandleBase
和智能指针模板类 TC_AutoPtr
,并对 TC_AutoPtr
与 shared_ptr
的性能进行简单的对比测试。
std::atomic
在 C++11 标准库 <atomic>
中定义。std::atomic
是模板类,一个模板类型为 T
的原子对象中封装了一个类型为 T
的值。
template <class T> struct atomic;
原子类型对象的主要特色就是从不一样线程访问不会致使数据竞争(data race)。所以从不一样线程访问某个原子对象是良性 (well-defined) 行为。而一般对于非原子类型而言,并发访问某个对象(若是不作任何同步操做)会致使未定义 (undifined) 行为发生。
C++11 标准库 std::atomic
提供了针对整型(integral
)和指针类型的特化实现。下面是针对整型的特化实现的主要部分
template <> struct atomic<integral> { ... ... operator integral() const volatile; operator integral() const; atomic() = default; constexpr atomic(integral); atomic(const atomic&) = delete; atomic& operator=(const atomic&) = delete; atomic& operator=(const atomic&) volatile = delete; integral operator=(integral) volatile; integral operator=(integral); integral operator++(int) volatile; integral operator++(int); integral operator--(int) volatile; integral operator--(int); integral operator++() volatile; integral operator++(); integral operator--() volatile; integral operator--(); integral operator+=(integral) volatile; integral operator+=(integral); integral operator-=(integral) volatile; integral operator-=(integral); integral operator&=(integral) volatile; integral operator&=(integral); integral operator|=(integral) volatile; integral operator|=(integral); integral operator^=(integral) volatile; integral operator^=(integral); };
能够看到重载了大部分整型中经常使用的运算符,包括自增运算符 ++
和自减运算符 --
,能够直接使用自增或自减运算符直接对原子计数对象的引用值 +1
或 -1
。
TC_HandleBase
是 TARS 的智能指针基类,包含两个成员变量 _atomic
和 _bNoDelete
,定义以下
protected: /** * 计数 */ std::atomic<int> _atomic; /** * 是否自动删除 */ bool _bNoDelete;
TC_HandleBase
,为 TARS 智能指针模板类 TC_AutoPtr<T>
提供引用计数的相关操做,增长计数和减小计数接口的相关代码以下
/** * @brief 增长计数 */ void incRef() { ++_atomic; } /** * @brief 减小计数 * 当计数==0时, 且须要删除数据时, 释放对象 */ void decRef() { if((--_atomic) == 0 && !_bNoDelete) { _bNoDelete = true; delete this; } } /** * @brief 获取计数. * @return int 计数值 */ int getRef() const { return _atomic; }
能够看到,这里经过整型的原子计数类的对象 _atomic
实现引用计数,管理智能指针指向对象的引用计数。
TC_AutoPtr
的定义及其构造函数和成员变量以下述代码,成员变量 _ptr
是一个 T*
指针。构造函数初始化该指针并调用了 TC_HandleBase
成员函数 incRef
进行引用计数 +1
,这要求类 T
是继承自 TC_HandleBase
的。
/** * @brief 智能指针模板类. * * 能够放在容器中,且线程安全的智能指针. * 经过它定义智能指针,该智能指针经过引用计数实现, * 能够放在容器中传递. * * template<typename T> T必须继承于TC_HandleBase */ template<typename T> class TC_AutoPtr { public: /** * @brief 用原生指针初始化, 计数+1. * * @param p */ TC_AutoPtr(T* p = 0) { _ptr = p; if(_ptr) { _ptr->incRef(); } } ... public: T* _ptr; };
TC_AutoPtr
在使用时能够简单的看成 STL 的 shared_ptr
使用,须要注意的是指向的对象必须继承自 TC_HandleBase
(固然也能够本身实现智能指针基类,并提供与 TC_HandleBase
一致的接口),同时还要避免环形引用。下面咱们看一下 TC_AutoPtr
其余接口的定义:
/** * @brief 用其余智能指针r的原生指针初始化, 计数+1. * * @param Y * @param r */ template<typename Y> TC_AutoPtr(const TC_AutoPtr<Y>& r) { _ptr = r._ptr; if(_ptr) { _ptr->incRef(); } } /** * @brief 拷贝构造, 计数+1. * * @param r */ TC_AutoPtr(const TC_AutoPtr& r) { _ptr = r._ptr; if(_ptr) { _ptr->incRef(); } } /** * @brief 析构,计数-1 */ ~TC_AutoPtr() { if(_ptr) { _ptr->decRef(); } } /** * @brief 赋值, 普通指针 * @param p * @return TC_AutoPtr& */ TC_AutoPtr& operator=(T* p) { if(_ptr != p) { if(p) { p->incRef(); } T* ptr = _ptr; _ptr = p; //因为初始化时_ptr=NULL,所以计数不会-1 if(ptr) { ptr->decRef(); } } return *this; }
能够看到,这些接口都知足通用的引用计数规则。
+1
;+1
;+1
,左边的 -1
;-1
;通过上述分析,能够发现 TC_AutoPtr
和 shared_ptr
在用法和功能上很是类似,都支持多个指针共享一个对象,支持存储在容器中,那 TC_AutoPtr
有什么优点呢?
相比于 STL 库中的 shared_ptr
,TC_AutoPtr
更加轻量,具备更好的性能,咱们能够经过以下简单的测试代码,经过测试两者构造和复制的耗时来衡量它们的性能
#include <iostream> #include <chrono> #include <memory> #include <vector> #include "util/tc_autoptr.h" using namespace tars; using namespace std; using namespace chrono; // 测试类 class Test : public TC_HandleBase { public: Test() {} private: int test; }; // 打印时间间隔 void printDuration(const string & info, system_clock::time_point start, system_clock::time_point end) { auto duration = duration_cast<microseconds>(end - start); cout << info << double(duration.count()) * microseconds::period::num / microseconds::period::den << " s" << endl; } int main() { int exec_times = 10000000; // 次数 // 构造耗时对比 { auto start = system_clock::now(); for (int i = 0; i < exec_times; ++i) { TC_AutoPtr<Test> a = TC_AutoPtr<Test>(new Test); } auto end = system_clock::now(); printDuration("TC_AutoPtr construct: ", start, end); } { auto start = system_clock::now(); for (int i = 0; i < exec_times; ++i) { shared_ptr<Test> a = shared_ptr<Test>(new Test); } auto end = system_clock::now(); printDuration("shared_ptr construct: ", start, end); } // 复制耗时对比 { auto start = system_clock::now(); TC_AutoPtr<Test> a = TC_AutoPtr<Test>(new Test); for (int i = 0; i < exec_times; ++i) { TC_AutoPtr<Test> b = a; } auto end = system_clock::now(); printDuration("TC_AutoPtr copy: ", start, end); } { auto start = system_clock::now(); shared_ptr<Test> a = shared_ptr<Test>(new Test); for (int i = 0; i < exec_times; ++i) { shared_ptr<Test> b = a; } auto end = system_clock::now(); printDuration("shared_ptr copy: ", start, end); } }
最后运行测试,输出的结果以下
TC_AutoPtr construct: 0.208995 s shared_ptr construct: 0.423324 s TC_AutoPtr copy: 0.107914 s shared_ptr copy: 0.107716 s
能够看出,两者的复制性能相近,而构造性能上, TC_AutoPtr
要比 shared_ptr
快一倍以上。
本文主要介绍了 TARS 的智能指针组件 TC_AutoPtr
和 STL 的智能指针 shared_ptr
。TC_AutoPtr
指向继承自智能指针基类 TC_HandleBase
的对象。TC_HandleBase
经过原子计数器 std::atomic<int>
实现引用计数,确保引用计数是线程安全的。相比于 shared_ptr
,TC_AutoPtr
拥有更好的性能;而 shared_ptr
有更加完善的功能。TarsCpp 框架已经支持 C++11,开发者可以根据业务具体需求自由选择。
TARS能够在考虑到易用性和高性能的同时快速构建系统并自动生成代码,帮助开发人员和企业以微服务的方式快速构建本身稳定可靠的分布式应用,从而令开发人员只关注业务逻辑,提升运营效率。多语言、敏捷研发、高可用和高效运营的特性使 TARS 成为企业级产品。
TARS微服务助您数字化转型,欢迎访问:
TARS官网:https://TarsCloud.org
TARS源码:https://github.com/TarsCloud
Linux基金会官方微服务免费课程:https://www.edx.org/course/bu...
获取《TARS官方培训电子书》:https://wj.qq.com/s2/6570357/...
或扫码获取: