乐趣区

关于c++:TarsCpp-组件-之-智能指针详解

作者 Eaton

导语 在 C++ 中,内存治理是非常重要的问题,一不小心就会造成程序内存泄露,那么怎么防止呢?通过智能指针能够优雅地治理内存,让开发者只须要关注内存的申请,内存的开释则会被主动治理。在文章 开源微服务框架 TARS 之 根底组件 中曾经简要介绍过,TARS 框架组件中没有间接应用 STL 库中的智能指针,而是实现了本人的智能指针。本文将会别离对 STL 库中的智能指针和 TarsCpp 组件中的智能指针进行比照剖析,并具体介绍 TARS 智能指针的实现原理。

目录

  • 智能指针

    • 简介
    • 援用计数原理
  • STL 库中的智能指针

    • shared_ptr
  • TARS 智能指针 TC_AutoPtr 实现详解

    • 原子计数类 std::atomic
    • 智能指针基类 TC_HandleBase
    • 智能指针模板类 TC_AutoPtr
    • TC_AutoPtr 劣势

智能指针

简介

在计算机程序中,泄露是常见的问题,包含内存泄露和资源泄露。其中资源泄露指的是零碎的 socket、文件描述符等资源在应用后,程序不再须要它们时没有失去开释;内存泄露指的是动态内存在应用后,程序不再须要它时没有失去开释。

内存泄露会使得程序占用的内存越来越多,而很大一部分往往是程序不再须要应用的。在 C++ 程序中,内存泄露常见于咱们应用了 new 或者 malloc 申请动静存储区的内存,却忘了应用 delete 或者 free 去开释内存,造成零碎内存的节约,导致程序运行速度减慢甚至零碎解体等严重后果。

随着计算机利用需要的日益减少,利用的设计与开发日趋简单,开发人员在开发过程中解决的变量也越来越多。如何无效进行内存调配和开释、避免内存透露逐步成为开发者面临的重要难题。为了解决遗记手动开释内存造成的内存泄露问题,智能指针诞生了。

常见的智能指针的应用场景,包含类中的成员变量(指针型)和一般的变量(指针型)。智能指针能够实现指针指向对象的共享,而无需关注动态内存的开释。通用实现技术是援用计数(Reference count),下一部分会介绍,简略讲就是将一个计数器与类指向的对象相关联,跟踪有多少个指针指向同一对象,新增一个指针指向该对象则计数器 +1,缩小一个则执行 -1

援用计数原理

援用计数是智能指针的一种通用实现技术,上图为大抵流程,基本原理如下:

  1. 在每次创立类的新对象时,初始化指针并将援用计数置 1
  2. 当对象作为另一对象的正本而创立时(复制构造函数),复制对应的指针并将援用计数 +1
  3. 当对一个对象进行赋值时,赋值操作符 = 将左操作数所指对象的援用计数 -1,将右操作数所指对象的援用计数 +1
  4. 调用析构函数数,援用计数 -1
  5. 上述操作中,援用计数减至 0 时,删除根底对象;

STL 库中的智能指针 shared_ptr 和 TARS 智能指针都应用了该援用计数原理,前面会进行介绍。

STL 库的智能指针

C++ 规范模板库 STL 中提供了四种指针 auto_ptr, unique_ptr, shared_ptr, weak_ptr

auto_ptr 在 C++98 中提出,但其不能共享对象、不能治理数组指针,也不能放在容器中。因而在 C++11 中被摒弃,并提出 unique_ptr 来代替,反对治理数组指针,但不能共享对象。

shared_ptrweak_ptr 则是 C++11 从规范库 Boost 中引入的两种智能指针。shared_ptr 用于解决多个指针共享一个对象的问题,但存在循环援用的问题,引入 weak_ptr 次要用于解决循环援用的问题。

接下来将具体介绍 shared_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 申明并赋值后,援用计数减少 1cp_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;
}

在上述例子中,咱们首先定义了两个类 ABA 的成员变量是指向 Bshared_ptr 指针,B 的成员变量是指向 Ashared_ptr 指针。

而后咱们创立了 sptrBsptrA 两个智能指针对象,并且互相赋值。这会造成环形援用,使得 AB 的析构函数都无奈执行(能够通过 cout 观测),从而内存无奈开释。当咱们无奈防止循环应用时,能够应用 weak_ptr 来解决,这里不再开展,感兴趣的读者能够自行查阅。

TARS 智能指针 TC_AutoPtr 实现详解

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_AutoPtrshared_ptr 的性能进行简略的比照测试。

原子计数类 std::atomic

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

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

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 劣势

通过上述剖析,能够发现 TC_AutoPtrshared_ptr 在用法和性能上十分类似,都反对多个指针共享一个对象,反对存储在容器中,那 TC_AutoPtr 有什么劣势呢?

相比于 STL 库中的 shared_ptrTC_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_ptrTC_AutoPtr 指向继承自智能指针基类 TC_HandleBase 的对象。TC_HandleBase 通过原子计数器 std::atomic<int> 实现援用计数,确保援用计数是线程平安的。相比于 shared_ptrTC_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/…

或扫码获取:

退出移动版