对象模型
理解程序设计的最佳切入点?——对象模型
对象模型的作用?
一、单个类的管理。(基类与子类)
尽管每个类表示的网络元素不同,但它们往往都具有某些相同的功能需求,如动态内存管理和属性配置等。没必要在每一个类中分别实现这些需求。因此需要一些顶层的基类来统一定义与实现这些共性特征。而子类只需关注自身专属特性即可。
二、多个类的管理。(关联各个类)
任何一个单一的类时无法完成网络模拟的。这些类需要被有机地关联起来,形成一个可以处理各种网络事件的独立主题。例如,一个网络结点需要整合应用程序、通信信道、网络设备和TCP/IP层协议等多个C++类才能与其他结点进行通信。这些类之间如何灵活、高效的关联,也是对象模型解决的问题之一。
如何实现上述目的?
定义三个基类:SimpleRefCount
、ObjectBase
和Object
。
SimpleRefCount
:解决针对单个类的动态内存管理问题,智能指针的实现。
ObjectBase
解决单个类需要配置属性和trace变量的需求。
Object
类解决了多个类之间动态关联问题。
这种类之间的动态关联是通过一种叫做对象聚合的功能实现的。简单地说,对象聚合能够让一个Object对象在运行时动态地关联其他对象,而不是在编译时关联。
下图给出了上面三个基类的继承关系,以及它们各自的一些子类代表。箭头指向的类是基类。
智能指针
为什么使用智能指针?
传统的动态内存是从new运算符创建到被delete运算符销毁。尽管这种设计增加了内存管理的灵活度,但同时造成了两个严重的安全隐患。
内存泄漏:动态内存没有被及时释放
空悬指针:在尚有其他指针引用的情况下释放动态内存
智能指针如何解决这两个安全隐患的呢?
除了与常规指针有着相似的行为(拷贝、赋值等),更重要的是其能够自动释放那些没有指针指向的动态内存,从而避免了内存泄漏和空悬指针问题,提高了程序的安全性。
如何实现智能指针?
智能指针由Ptr
和SimpleRefCount
两个类模板组成。
其中,SimpleRefCount
类是ns-3智能指针的内部实现。它定义了一个引用计数器来记录指向自身内存的指针数量。
Ptr
类则定义了ns-3智能指针的外部接口。
设计原理
Ptr使用范式
Ptr<类名>指针变量名
Ptr的实现
两部分:
第一部分负责保存原始指针和模拟原始指针操作(复制、赋值),这一部分通过Ptr
实现;
第二部分负责记录所有指向所分配的对象内存的指针数量(引用计数器),这一部分通过SimpleRefCount
实现。
下面的代码中,模板形参T(typename T)是Ptr指针指向对象的类名称。指向对象内存的原始指针m_ptr
保存在Ptr类中。
SimpleRefCount
类定义了一个引用计数器m_count
变量。
这段代码还包括SimpleRefCount
类的Ref()
和Unref()
函数,分别负责计数器值的增减,当计数器变为0时,意味着没有任何指针指向该内存,这时,Unref()
函数会调用删除函数销毁对象。
类模板
Ref()和Unref()函数怎么使用?
定义一个SimpleRefCount
的子类MyTest
。Ptr
指针t1
和t2
分别指向两个MyTest
对象。
Ptr<MyTest>t1 = Create<MyTest>();
Ptr<MyTest>t2 = Create<MyTest>();
t2 = t1; //t1指向t2???
由于赋值操作使得MyTest_2
获得了一个新的Ptr
指针t1
,因此t1
在指向新内存之后立即调用MyTest_2
的Ref()
函数,使其计数器加1,变为2。相反,MyTest_1
对象在这个过程中失去了原来的Ptr
指针t1
,因此在t1
指向新内存之前会调用MyTest_1
的Unref()
函数,使其计数器减1。此时,MyTest_1
的计数器会变为0,被销毁。
使用实例
- 初始化
Packet *p = NULL;
Ptr<Packet>ptr;
- 创建对象
//使用Packet()构造函数
Packet *p = new Packet();
Ptr<Packet>ptr = Create<Packet>();
//使用Packet(uint32_t size)构造函数
Packet *p = new Packet(100);
Ptr<Packet>ptr = Create<Packet>(100);
- 赋值操作
Packet *p = new Packet();
Ptr<Packet>ptr_1 = p;
Ptr<Packet>ptr_2 = ptr; //???
- 指针运算
ptr->GetUid(); //->
(*ptr).GetUid(); //*
一个是指针指向的函数,一个是指针解引用之后的函数,但这两行最后都会执行函数,效果相同。
- 比较运算
if(ptr == ptr_1){}
if(ptr != ptr_1){}
if(ptr == p){}
if(ptr != p){}
等于和不等于符号可以用于两个Ptr指针之间,也可以用于一个Ptr指针和一个原始指针之间。
if(ptr < ptr_1){}
if(ptr <= ptr_1){}
if(ptr > ptr_1){}
if(ptr >= ptr_1){}
- 流插入
std::cout<<"address:"<<ptr<<std::endl;
输出Ptr
指针中原始指针的地址。
- 拷贝
Ptr
指针的拷贝有两种:指针拷贝和对象拷贝。
指针拷贝:
被拷贝的和新的Ptr指针均指向同一个对象。
Packet *p = new Packet();
Ptr<Packet>ptr(p);
Ptr<Packet>ptr_1(ptr); //Packet对象计数器加1
对象拷贝:
使用的场景:不只需要一个新的指针,还需要拷贝出一个新的对象,开辟新的内存空间。这时候就需要用到Copy()函数。
使用比较少。
Ptr<Packet>ptr = Create<Packet>();
Ptr<Packet>ptr_1 = Copy(ptr);//ptr_1指向一个新创建的Packet对象
- 类型转换
ns-3提供了DynamicCast()
、StaticConst()
和ConstCast()
三个Ptr指针类型转换函数。
注意:每次调用这些函数后,都需要检查其返回Ptr指针是否为空,确认转换是否成功。
//DynamicCast
Ptr<NetDevice>pDev = Create<LoopbackNetDevice>();
Ptr<LoopbackNetDevice>pLoopDev = DynamicCast<LoopbackNetDevice>(pDev);
if(!pLoopDev )
NS_LOG_UNCOND("DynamicCast failure");
//StaticCast
Ptr<LoopbackNetDevice>pLoopDev1 = StaticCast<LoopbackNetDevice>(pDev);
if(!pLoopDev1 )
NS_LOG_UNCOND("StaticCast failure");
//ConstCast
Ptr<Packet const>ptr = Create<Packet>();
Ptr<Packet>ptr_1 = ConstCast<Packet>(ptr);
if(!ptr_1)
NS_LOG_UNCOND("ConstCast failure");
- 获取原始指针
获取原始指针有两个函数:PeekPointer()
和GetPointer()
。
GetPointer()
会对Ptr
指针指向对象的计数器加1。相当于新创建了一个指针指向这个对象。使用极少。
PeekPointer()
则没有这个要求。
Ptr<Packet>ptr = Create<Packet>();
Packet *p = PeekPointer(ptr);
适用范围
Ptr的作用:
提供了和原始指针相同的操作集,实现了内存的动态管理。
局限性:
- 只能支持构造函数参数少于或等于7个的类。
- 只能引用与SimpleRefCount子类对象。