文章目录
unique_ptr和 scoped_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的使用和实现方法。