WebRTC源码分析之智能指针-scoped_refptr


unique_ptrscoped_refptr是WebRTC中使用最多的两种智能指针, unique_ptr是C++标准库中的智能指针,而 scoped_refptr是WebRTC中自实现的智能指针,用法类似于C++标准库中 shared_ptr,但使用上不如 shared_ptr方便,功能也不如 scoped_refptr丰富。

本文主要简单介绍一下如何使用scoped_refptr,同时通过源码看一下scoped_refptr是如何实现的。

scoped_refptr使用示例

工程

示例工程:https://pan.baidu.com/s/1rbI2hwXpMA-Pb-i-zCdVWA 提取码:cenz

示例
#define WEBRTC_WIN

#include <iostream>
#include "scoped_refptr.h"
#include "ref_count.h"
#include "ref_counted_object.h"

using namespace std;
using namespace rtc;

class A: public RefCountInterface       /*需要继承自类RefCountInterface*/
{
public:
	A(int i)
		:data_(i)
	{
		cout << __FUNCTION__ << " : " << this << endl;
	}

	void display()
	{
		cout << "data = " << data_ << endl;
	}

	void set(int i)
	{
		data_ = i;
	}

	~A()
	{
		cout << __FUNCTION__ << endl;
	}

private:
	int data_;
};

void func(scoped_refptr<A> sp)
{
	sp->display();

	sp->set(200);
}

int main()
{
	/*只见new,不见delete。*/
	scoped_refptr<A> sp = new RefCountedObject<A>(100);   

	/*获取托管对象的地址*/
	cout << "sp = " << sp.get() << endl;

	sp->display();

	func(sp);

	sp->display();

	return 0;
}

在这里插入图片描述
使用方法:

  • 类A需要继承自RefCountInterface类。
  • 在new A的时候,需要使用模板类RefCountedObject进行包装,类A作为RefCountedObject的模板参数,后面括号中是构造类A对象的参数。
  • new RefCountedObject<A>(100)产生的指针,交于sp对象托管,指针的引用计数和释放都是通过sp对象自动管理的。这样只需要new对象,而对象的释放不需要再主动调用delete,`当引用计数为零的时候,会主动释放new出的对象。

scoped_refptr源码分析

实现原理

scoped_refptr对托管对象的管理主要是通过资源获取即初始化(RAII)引用计数

资源获取即初始化(RAII)

RAII:Resource Acquisition Is Initialization,最早是由C++的发明者 Bjarne Stroustrup为解决C++中资源分配与销毁问题而提出的。RAII的基本含义就是:C++中的资源(例如内存,文件句柄等等)应该由对象来管理,资源在对象的构造函数中初始化,并在对象的析构函数中被释放。STL中的智能指针就是RAII的一个具体应用。

#include <iostream>

using namespace std;

class A
{
public:
	A()
	{
		cout << "constructor" << endl;
	}

	~A()
	{
		cout << "deconstructor" << endl;
	}
};

class Smart_ptr
{
public:
	Smart_ptr(A* pa)
		:pa_(pa)
	{}


	~Smart_ptr()
	{
		delete pa_;
	}

private:
	A* pa_;
};

int main()
{
	Smart_ptr sp = new A();

	return 0;
}

在这里插入图片描述
传统的智能指针auto_ptr对托管对象的管理使用的就是资源获取即初始化(RAII)。在C++中对内存的管理一直是一件令人头疼的事情,很多时候new了对象以后,会忘记delete,这样就会造成内存泄漏。而RAII利用的是类对象在离开作用域时会主动调用析构器这一特性,将delete这件事放在析构器中,这样就不会忘记释放内存了。

RAII的主要做法是这样的:假如我们需要在堆上new一个类A的对象。再在栈上定义一个类Smart_ptr的对象sp,对象sp在栈上,所以在对象sp离开它的作用域时,会主动调用类Smart_ptr的析构器,对堆上内存的管理主要利用这一特性。sp对象中数据成员pa_保存new A()的地址,在Smart_ptr的构造器中传入托管的地址,在sp离开其作用域的时候,会调用其析构器,在析构器中delete掉托管的内存,这样就不会造成堆内存的泄漏。

在使用的时候,只看见new,是看不见delete的。使用的时候new出对象,将内存托管,至于何时delete掉对象不需要再关心。这样大大简化了类对象的申请和释放。

#include <iostream>

using namespace std;

class A
{
public:
	A()
	{
		cout << "constructor" << endl;
	}

	void display()
	{
		cout << "hello world" << endl;
	}

	~A()
	{
		cout << "deconstructor" << endl;
	}
};

class Smart_ptr
{
public:
	Smart_ptr(A* pa)
		:pa_(pa)
	{}

	A& operator*()
	{
		return *pa_;
	}

	A* operator->()
	{
		return pa_;
	}

	~Smart_ptr()
	{
		delete pa_;
	}

private:
	A* pa_;
};

int main()
{
	Smart_ptr sp = new A();

	sp->display();

	(*sp).display();

	return 0;
}

在这里插入图片描述

为了使sp指针使用起来感觉就像类A对象指针似的,需要对Smart_ptr类重载operator*()operator->(),这样sp指针就像A对象指针似的。

还有一个问题是Smart_ptr类对象只能托管类A的对象,不能托管其他类对象,使用太局限。可以通过模板使Smart_prt类可以托管任意对象。如下代码:

#include <iostream>

using namespace std;

class A
{
public:
	A()
	{
		cout << "A constructor" << endl;
	}

	void display()
	{
		cout << "hello world" << endl;
	}

	~A()
	{
		cout << "A deconstructor" << endl;
	}
};

class B
{
public:
	B()
	{
		cout << "B constructor" << endl;
	}

	void display()
	{
		cout << "hello world" << endl;
	}

	~B()
	{
		cout << "B deconstructor" << endl;
	}
};

template<typename T>
class Smart_ptr
{
public:
	Smart_ptr(T* pa)
		:pa_(pa)
	{}

	T& operator*()
	{
		return *pa_;
	}

	T* operator->()
	{
		return pa_;
	}

	~Smart_ptr()
	{
		delete pa_;
	}

private:
	T* pa_;
};

int main()
{
	Smart_ptr<A> spa = new A();
	spa->display();

	Smart_ptr<B> spb = new B();
	spb->display();

	return 0;
}

在这里插入图片描述

通过模板的方式,实现了Smart_ptr类对任意对象的托管。

引用计数

上面的Smart_ptr对对象的指针的托管还是有问题的,如在传参的时候,如下代码:

#include <iostream>

using namespace std;

class A
{
public:
	A()
	{
		cout << "A constructor" << endl;
	}

	void display()
	{
		cout << "hello world" << endl;
	}

	~A()
	{
		cout << "A deconstructor" << endl;
	}
};

template<typename T>
class Smart_ptr
{
public:
	Smart_ptr(T* pa)
		:pa_(pa)
	{}

	T& operator*()
	{
		return *pa_;
	}

	T* operator->()
	{
		return pa_;
	}

	~Smart_ptr()
	{
		delete pa_;
	}

private:
	T* pa_;
};

void func(Smart_ptr<A> sp)
{
	sp->display();
}

int main()
{
	Smart_ptr<A> sp = new A();
	sp->display();

	func(sp);	

	return 0;
}

在这里插入图片描述

这样会造成重析构(double free),即对同一段内存释放了两次。main函数中的sp和func函数中的sp同时托管同一个堆对象,在func函数调用结束后,其内部的sp离开作用域调用析构器释放托管的内存。在main函数中sp在离开作用域时再次释放托管的内存,同一个堆对象被释放了两次,造成了内存错误。

为了解决这个问题,引入了引用计数,只要在引用计数为零的时候,才释放内存。在scoped_refptr智能指针中,通过继承的方式给托管对象增加引用计数的功能。当有多个scoped_refptr对象托管同一个对象的时候,被托管对象的引用计数会增加,当托管对象的引用计数为零时,调用delete将其析构掉。

类的关系图

在这里插入图片描述

对托管对象的包装
RefCountInterface

RefCountInterface类所在文件的位置:src\rtc_base\ref_count.h

enum class RefCountReleaseStatus { kDroppedLastRef, kOtherRefsRemained };

class RefCountInterface 
{
 public:
  /*增加引用计数*/
  virtual void AddRef() const = 0;       
    
  /*减少引用计数,当引用计数为零是,释放托管的对象。*/
  virtual RefCountReleaseStatus Release() const = 0;  

 protected:
  virtual ~RefCountInterface() {}
};

RefCountInterface是一个接口类,提供了两个函数AddRef()Release(),这个两个函数分别用于增加引用计数和减少引用计数且当引用计数为零时释放对象。

所有需要通过智能指针scoped_refptr托管的类需要继承自这个接口。

RefCounter

RefCounter类所在文件的位置:src\rtc_base\ref_counter.h

class RefCounter 
{
 public:
  explicit RefCounter(int ref_count) 
      : ref_count_(ref_count) {}
    
  /*不可以无参构造*/
  RefCounter() = delete;

  /*增加引用计数*/
  void IncRef() 
  { 
      rtc::AtomicOps::Increment(&ref_count_); 
  }

  /*减少引用计数,同时返回引用计数是否为零。*/
  rtc::RefCountReleaseStatus DecRef() 
  {
    return (rtc::AtomicOps::Decrement(&ref_count_) == 0)
               ? rtc::RefCountReleaseStatus::kDroppedLastRef
               : rtc::RefCountReleaseStatus::kOtherRefsRemained;
  }
  
  /*返回引用计数是否为1*/
  bool HasOneRef() const 
  {
    return rtc::AtomicOps::AcquireLoad(&ref_count_) == 1;
  }

 private:
  volatile int ref_count_;     /*用于引用计数*/
};

RefCounter类是单独用于引用计数的类,其内部数据成员ref_count用于记录指针被引用的次数。

AtomicOps类提供了函数Increment()Decrement(),用于原子的增加一和减少一,避免使用锁来确保原子操作。

DecRef()函数在将引用计数减一的同时,需要判断引用计数是否为零,当引用计数为零的时候,需要释放托管的对象。DecRef()通过返回值告知调用者引用计数是否为零。

RefCountedObject

RefCountedObject类所在文件的位置:src\rtc_base\ref_counted_object.h

template <class T>
class RefCountedObject : public T    /*继承自模板参数T*/
{
 public:
  RefCountedObject() {}

  template <class P0>
  explicit RefCountedObject(P0&& p0) 
  	: T(std::forward<P0>(p0)) {}

  template <class P0, class P1, class... Args>
  RefCountedObject(P0&& p0, P1&& p1, Args&&... args)
      : T(std::forward<P0>(p0),
          std::forward<P1>(p1),
          std::forward<Args>(args)...) {}

  /*引用计数增加一*/
  virtual void AddRef() const 
  { 
  	ref_count_.IncRef(); 
  }

  /*引用计数减一,若引用计数减一后为零,则释放本对象。*/
  virtual RefCountReleaseStatus Release() const 
  {
    /*引用计数减一,同时返回引用计数是否为零。*/
    const auto status = ref_count_.DecRef();
    
    /*若引用计数为零,则释放本对象。*/
    if (status == RefCountReleaseStatus::kDroppedLastRef) 
    {
      delete this;    /*本对象的引用计数为零时,释放本对象。*/
    }
      
    return status;
  }

  /*引用计数是否为1*/
  virtual bool HasOneRef() const 
  { 
  	return ref_count_.HasOneRef(); 
  }

 protected:
  virtual ~RefCountedObject() {}

  /*引用计数*/
  mutable webrtc::webrtc_impl::RefCounter ref_count_{0};

  /*禁用拷贝构造和赋值运算符重载*/
  RTC_DISALLOW_COPY_AND_ASSIGN(RefCountedObject);
};

在new托管对象的时候,需要使用RefCountedObject包装。

在开始的例子中scoped_refptr<A> sp = new RefCountedObject<A>(100);,为了托管类A的对象,需要把类A作为模板参数生成RefCountedObject对象,RefCountedObject是一个模板类,它的参数是类A,同时RefCountedObject又继承自它的模板参数,所以RefCountedObject类A的子类,new RefCountedObject<A>(100)得到的是RefCountedObject *类型的指针。RefCountedObject中有引用计数的功能,通过继承的方式RefCountedObject既有类A的功能,又有引用计数的功能。当一个类有了引用计数的功能就可以配合scoped_refptr智能指针进行内存托管了。具体细节在scoped_refptr中详述。

注意在引用计数减少的时候,当判断引用计数为零时,说明没有智能指针再对本对象的引用了,需要将本对象释放掉。

数据成员ref_count_mutable修饰的原因是,const修饰的成员函数(例如AddRef()和Release()

中不能对数据成员修改,但被mutable修饰过的数据成员是可以在const成员函数中修改的。至于为什么用const修饰AddRef()和Release()函数,是为了const类型RefCountedObject对象可以调用这个两个函数。

RTC_DISALLOW_COPY_AND_ASSIGN(RefCountedObject)是通过宏的方式禁用拷贝构造和赋值运算符重载,宏原型和展开结果如下:

宏原型
#define RTC_DISALLOW_COPY_AND_ASSIGN(TypeName) \
  TypeName(const TypeName&) = delete;          \
  RTC_DISALLOW_ASSIGN(TypeName)

#define RTC_DISALLOW_ASSIGN(TypeName) \
  TypeName& operator=(const TypeName&) = delete
展开结果
RefCountedObject(const RefCountedObject&) = delete;   
RefCountedObject& operator=(const RefCountedObject&) = delete;
小结

在这里插入图片描述

为托管类A对象,类A需要继承自类RefCountInterface,在new A的时候,没有直接申请类A,而是new RefCountedObject<A> ,得到的是RefCountedObject对象指针,只是把类A作为RefCountedObject模板类的参数,RefCountedObject是类A的子类。

通过继承和模板类继承的方式,对托管类A进行了包装,包装后的类A增加了引用计数的功能。

对托管对象的管理
scoped_refptr的概述

scoped_refptr类所在文件的位置:src\api\scoped_refptr.h

scoped_refptr类的主要作用是,管理被托管对象的指针,同时在使用形式上尽量做到和被托管对象指针一致。

template <class T>       /*模板类*/
class scoped_refptr 
{
public:
    /*其他成员函数*/
    
protected:
  T* ptr_;      /*托管对象的指针*/
};

scoped_refptr是模板类,可以进行不同的实例化,托管不同的对象。

构造器和析构器
/*无参构造,将托管的指针置为nullptr。*/
scoped_refptr() 
    : ptr_(nullptr) 
    {}

/*有参构造   保存托管的指针,同时将对象内的引用计数增加一。*/
scoped_refptr(T* p)
	: ptr_(p)   // NOLINT(runtime/explicit)
{
	if (ptr_)
		ptr_->AddRef();   /*增加托管对象的引用计数*/
}

/*拷贝构造   共同托管一个对象*/
scoped_refptr(const scoped_refptr<T>& r)
	: ptr_(r.ptr_)    
{
	if (ptr_)
		ptr_->AddRef();
}

/*拷贝构造器   类成员函数模板*/
template <typename U>
scoped_refptr(const scoped_refptr<U>& r)
	: ptr_(r.get())
{
	if (ptr_)
		ptr_->AddRef();
}

/*移动构造器*/
scoped_refptr(scoped_refptr<T>&& r)
	: ptr_(r.release())   /*r放弃托管的指针,本类接管托管的指针。*/
    {}   

template <typename U>
scoped_refptr(scoped_refptr<U>&& r)
	: ptr_(r.release()) {}

/*智能指针对象在析构的时候,需要将托管的指针引用计数减一。*/
~scoped_refptr()
{
	if (ptr_)
		ptr_->Release();  /*当引用计数为零时,会释放托管的对象。*/
}

再用开头的例子分析一下托管的过程:

scoped_refptr<A> sp = new RefCountedObject<A>(100);

RefCountedObject<A>类A的子类,new RefCountedObject<A>(100)得到的是RefCountedObject<A>对象的指针,也就是类A子对象的指针。

scoped_refptr<A>使用类A进行实例化,则scoped_refptr数据成员T * ptr_中的T *A *,即保存的指针是类A的指针。在调用scoped_refptr构造器构造对象时,传入的却是RefCountedObject<A> *,即将子类对象的指针赋值给父类对象的指针,发生了赋值兼容,这是可以的。并且在scoped_refptr数据成员ptr_调用AddRef()和Release()函数时,会发生多态行为。

template <typename U>
scoped_refptr(const scoped_refptr<U>& r)
	: ptr_(r.get())
{
	if (ptr_)
		ptr_->AddRef();
}

单独说一下这个构造器,这个构造器是一个类成员函数模板构造器。我现在看WebRTC的代码还太少,还没看到这个构造器在WebRTC中是如何使用的。根据自己的理解,找到了一个可能的用法。

根据const scoped_refptr<U>& r知,对象r中托管的指针是类U的指针,若scoped_refptr实例化为类A类型,则托管的是类A的指针,ptr_(r.get())说明类U的指针可以赋值给类A的指针变量。不经过指针类型强转可以这样进行指针赋值的,常用的就是子类指针赋值给父类指针变量。所以一种存在的可能是类U类A的子类。示例如下:

#define WEBRTC_WIN

#include <iostream>
#include "scoped_refptr.h"
#include "ref_count.h"
#include "ref_counted_object.h"

using namespace std;
using namespace rtc;

class A: public RefCountInterface
{
public:
	void display()
	{
		cout << "class A" << endl;
	}

	virtual void dump()
	{
	}
};

class B :public A
{
public:
	void dump() override
	{
		cout << "class B" << endl;
	}
};

int main()
{
	scoped_refptr<B> spb = new RefCountedObject<B>();

	/*发生拷贝构造器,调用scoped_refptr(const scoped_refptr<U>& r)。*/
	scoped_refptr<A> spab = spb;

	spab->display();
	spab->dump();
	   	
	return 0;
}

在这里插入图片描述

spb对象托管的是类B的指针,spab对象托管的是类A的指针。在构造spab对象的时候会调用template <typename U> scoped_refptr(const scoped_refptr<U>& r)函数。

运算符重载函数
/* 转换构造器  用于转成T*指针*/
operator T* () const { return ptr_; }

/* ->运算符重载*/
T* operator->() const { return ptr_; }

/*赋值运算符重载*/
scoped_refptr<T>& operator=(T* p)
{
    if (p)
        p->AddRef();      /*增加托管对象的引用计数*/

    if (ptr_)
        ptr_->Release();  /*将原托管对象的引用计数减一*/

    ptr_ = p;     /*托管新对象*/

    return *this;
}

/*赋值运算符重载*/
scoped_refptr<T>& operator=(const scoped_refptr<T>& r)
{
    return *this = r.ptr_;   /*调用上面的赋值运算符重载*/
}

template <typename U>
scoped_refptr<T>& operator=(const scoped_refptr<U>& r)
{
    return *this = r.get();
}

/*移动赋值运算符重载*/
scoped_refptr<T>& operator=(scoped_refptr<T>&& r)
{
    scoped_refptr<T>(std::move(r)).swap(*this);

    return *this;
}

template <typename U>
scoped_refptr<T>& operator=(scoped_refptr<U>&& r)
{
    scoped_refptr<T>(std::move(r)).swap(*this);
    return *this;
}

说一下scoped_refptr<T>& operator=(scoped_refptr<T>&& r)函数,这个函数实现了移动赋值运算符重载。scoped_refptr<T>(std::move®)调用移动拷贝构造器产生了临时对象,此时临时对象托管的是r对象托管的指针,scoped_refptr<T>(std::move®).swap(*this);临时对象和本对象交换托管的对象,则本对象托管r对象托管的指针,而临时对象托管本对象托管的指针,临时对象离开时会把本对象之前托管的指针计数减一。从而实现了将本对象托管的指针引用计数减一,并放弃对此指针的托管,改为托管r对象托管的指针。

其他成员函数
/*获取托管的指针*/
T* get() const { return ptr_; }

/*放弃指针托管,并将托管对象返回。*/
T* release()
{
	T* retVal = ptr_;
	ptr_ = nullptr;     /*放弃托管,置为nullptr。*/

	return retVal;      /*返回托管的指针*/
}

/*通过传递二级指针,交换一级指针。*/
void swap(T** pp)
{
	T* p = ptr_;
	ptr_ = *pp;
	*pp = p;
}

void swap(scoped_refptr<T>& r)
{
	swap(&r.ptr_);
}

小结

本文介绍了C++中RAII,通过示例和源码介绍了智能指针scoped_refptr的使用和实现方法。

  • 6
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值