面向对象编程的三要素:封装(encapsulation)、继承(inheritance)、多态(polymorphism)
类与结构体异同
- 类的定义的核心是属性声明(property declaration)和方法定义(method definition),所以类是数据和方法的自洽体(self-compatible),即可以保存数据和处理数据,与结构体的重要区别
- 两者都可以定义数据成员
- 类变量在声明后,需要构造(construction)才会构建对象(object)实体,而struct在变量声明时已经开辟内存
类与模块的异同
- 从数据和方法定义看,都可以作为封闭容器来定义和存储
- 例化,模块必须在仿真一开始就确定是否应该被例化,可通过generate来实现设计结构的变化;对于类而言,可以在仿真的任何时候被构造(开辟内存)创建新的对象。硬件部分必须在仿真一开始就确定下来,即module和其内部过程块、变量都应该是静态的(static);而软件部分,即类的部分可以在仿真任何阶段声明并动态创建出新的对象
- 封装性,模块变量和方法对外部公共(public)开放,而类根据需要确定访问的权限是默认的公共类型或者保护类型(protected)还是私有类型(local)。
- 继承性,模块没有继承性可言,无法在原有基础进行新的拓展,唯一可支持的方式恐怕是简单的拷贝和在拷贝的module上修改。而继承性是类的一大特点。
class不能出现initial和always(行为级建模基本语句),变量默认为var,不能定义为硬件里的reg或wire;module里再initial和always里(过程块)调用方法;class通过方法嵌套方法
handle句柄(指针):用来指向对象的指针
- 任何类都需要new函数,创建对象会开辟新的内存空间,用来存放新的成员变量和方法。构建函数new()是系统预定义函数,不需要指定返回值,函数会隐式地返回例化后的对象指针,会自动创建空的new函数。
- new函数三个部分:系统会通过new函数开辟空间;进入new函数进行初始化,返回句柄。
- 子类定义new函数时,应该首先调用父类的new函数,若父类new函数没有参数,子类也可以省略该调用,而系统编译时会自动添加super.new()
例化新对象后,句柄指向最后一个对象
注意:对象在域中会根据其为static、automatic进行销毁,如句柄声明其为动态,过程块外对象就会销毁(某个句柄多次使用时注意受其生命周期的影响)
与硬件域如module,interface不同的是,class中声明的变量默认类型为动态变量,即其生命周期在仿真开始后的某时间点开始到某时间点结束。class中的方法默认也是动态方法。若使用static声明其为静态,则其生命周期开始于编译阶段,贯穿于整个仿真(静态指编译的时候就有位置,可以不通过对象引用) 类中声明了变量,可以直接引用变量class::var,或者通过例化对象引用object.var
静态方法内可以声明并使用动态变量,但不能使用类的动态成员变量。因为在调用静态方法时,可能并没有创建具体的对象,也因此没有为动态成员变量开辟空间,因此在静态方法中使用类的动态成员变量是禁止,可能会找出内存泄漏,但是静态方法可以使用类的静态变量,因为静态方法同静态变量一样在编译阶段就已经分配好了内存空间。
封装:将属性和方法封装在内部,通过local和protected设置外部访问权限
继承:继承包括了继承父类的成员变量和成员方法
比如local:外部句柄无法调用使用了local声明的变量,会报错,而通过方法调用则不会报错
ck.get_clock(); //调用内部方法,可返回变量
ck.clock; //报错
this:表明所调用的成员是当前类的成员,而非同名的局部变量或者形式参数
super:继承父类
virtual function void calc_crc;
super.calc_cac(); //注意super的使用方式
对象创建初始化顺序
- 子类实例对象在初始化时首先调用父类的构造函数(new函数)
- 当父类构造函数完成时,会将子类实例对象中各个成员变量按照定义时显示的默认值初始化,如果没有默认值则不被初始化
- 当成员变量默认值赋予后(声明的同时赋值),才会最后进入用户定义的new函数中执行剩余的初始化代码
成员覆盖
- 子类句柄指向子类对象(可以访问全局变量),优先访问子类对象;子类句柄赋值给父类句柄后,父类句柄指向之类对象,但只能访问属于其类型的变量和方法
- Sv里用$cast(子类,父类)对父类句柄转为子类句柄作检查
- 默认情况下,没有super或者this来指示作用域,则按照由近到远的原则来引用变量
句柄的使用
- 句柄可以作为形式参数通过方法来完成对象指针的传递,从外部传入方法内部
- 句柄可以在方法内部首先完成修改,再由外部完成使用
- 程序执行时,可以在任何时刻为句柄创建新的对象,并将新的指针赋值给句柄
最终报错
task generate trans();
Transaction t; //声明句柄
Transaction fifo[$]; //声明储放句柄的队列,存放的是句柄,不是对象
t=new(); //创建对象, 注意,整个代码块只有一个对象
for (int i=0;i<3;i++) begin
t.addr=i<<2;
fifo.push_back(t); //为什么说有三个句柄?若要存放三个对象,将其new放在for循环里即可
end 因为t.addr改变,所以才会说三个句柄
t.fifo.pop_front(); //t.addr结果
endtask //储存的是句柄,但三个句柄指向一个对象,三个值都为t,结果由最后一次
赋值决定
包的使用
将不同模块的类定义归整到不同package。通过package实现在多个module、interface和program之*享parameter、data、type、task、function、class。意义在于将软件(类、类型、方法等)封装在不同的命名空间中,以此来与全局命名空间进行隔离。package里无法定义module、interface、program等硬件相关部分,可以定义类、静态方法和静态变量。类封装在包中,不应该在其他地方编译。没有介绍软件之前,硬件(module、interface、program)都会编译到库中。
'include "stimulator.sv" include属于纯文本替换;注意编译顺序来放置’include
`include将文件中所有文本原样插入包含的文件中。这是一个预处理语句,`include在import之前执行。他的主要作用就是在package中平铺其他文件,从而在编译时能够将多个文件中定义的类置于这个包中,形成一种逻辑上的包含关系。import不会复制文本内容。但是import可package中内容引入import语句所在的作用域,以帮助编译器能够识别被引用的类,并共享数据。
library是编译的产物,既可以容纳硬件类型,也可以容纳软件类型。从worklibrary里能找到各个package,无法直接找到package中的模块;而mcdt则可以直接从library中索引得到,不用import mcdt。
package是将命名空间分隔开来,使用不同package中的同名类,只需注明使用哪个package。但内部定义的类也应该尽可能独一无二。如加上前缀。
如: regs_pkg::monitor mon1=new(); arb_pkg::monitor mon2=new(); (域的索引)