验证过程中经常会有新的测试用例(DUT)被加进来,原有的验证平台就需要做对应的修改,可能会引入意外错误。为此,SV提供类的继承和多态操作,使得被调用的方法能够动态的选择实现方式。
类的继承
①类的拓展与类成员的重写
一个类可以被拓展,产生一个派生类。派生类(子类)也就继承了基类(父类)所有的属性。一些修改就可以在子类中完成。也就是类的重写。
子类可以通过super操作符来引用父类中的方法和成员,以区分重写的属性或方法。
使用extends关键字来扩展类:
class PACKET; //基类(父类)
integer status=10; //父类的数据成员
task showstatus(); //父类的方法_任务
$display(status);
endtask
function int chkstat(int s); //父类的方法_函数
returen(status==s);
endfunction
endclass
class drivedpacket extends PACKET; //类的拓展:drivedpacket为PACKET的子类
integer status=15; // 重写:改写数据成员status的值
integer b,c,d; // 增加了新的数据成员b,c,d
function int chkstat(int s); // 重写:修改了函数chkstat的功能
chkstat=super.status+status; //使用super操作符引用父类成员
$display(status);
endfunction
endclass
以上代码使得存储空间多出一个子类drivedpacked对象,其分配空间不仅有来自父类继承而来的数据和方法,还有自己添加以及修改的部分。
②子类对象对父类对象的赋值
1.子类对象赋值给父类对象是合法的,而父类对象赋值给子类对象不一定合法,2.可以通过$cast来检查赋值是否合法。
3.通过父类对象去引用子类中已经重写的属性或者方法,结果只会调用父类。
4.通过子类对象可以直接访问重写属性或方法
module drivered_base;
initial begin
drivedpacket p1=new; //构造子类对象,其父类自动构造(③)
packet p2=p1; //将子类句柄赋值给父类,使父类指向子类(1.)
j=p1.status //j的结果为15,也就是访问了重写属性(4.)
j=p2.status //j的结果为10,也就是尽管指向了子类,但依然访问的是子类中的父类属性(3.)
$cast(p1,p2); //将父类p2赋给子类p1,检查是否合法(2.)
end
endmodule
③构造函数的调用
在new函数中定义的任何代码执行之前,new()执行的第一个默认动作是调用父类的new(),并且会沿着这种继承关系一直向上回溯调用。
如果父类的构造函数需要参数,则有两种办法传递:第二种是最常用的办法
- 在类的扩展时指定:class drivedpacket extends packed(5);
- 在构造函数中使用super来传递父类参数:
-
function new(); super.new(5); endfunction
虚方法与多态
在类的扩展中,新增属性和方法对父类不可见,因此以下代码:
子类仅重写了build_payld方法,在子类对象der中调用了内嵌了build_payld方法的build_packed方法时,由于子类重写方法对父类不可见,所以该对象调用的依然是子类空间中继承的父类方法,也就是未被重写的方法,调用过程如下图:
因此,为了使重写的方法可以被父类看到,sv提供了虚方法
①虚方法
普通的方法在重写后仅对本身以及其子类中有效,而不能够被父类看到
虚方法的定义:使用virtual关键字
一开始p1和p2空间独立:
当父类对象p1指向子类对象p2后,p1.printA被调用时会访问空间中p2中的p1部分,发现PrintA为普通方法时,执行调用后结束。过程如下:
调用p1.printB时,程序同样找到空间中p2中的p1部分,而此时找到的printB声明成了虚方法,这时会咨询系统,查看该方法是否被p2对象或其子类对象所重写,一直查询到最后被重写的那个方法去调用(动态查找)。过程如下:
注意:一旦类中的一个方法被声明成了虚方法,那么在后续的继承过程中,无论是否使用关键字,他就一直是一个虚方法。因此,每一个类的继承关系,只有一个虚方法的实现,而且是在最后一个派生类中。
②多态
虚方法是类中声明的非静态方法,其带有关键字virtual,称带有虚方法的类为多态类。
继承是为了实现代码重用,而多态则是为了接口重用。多态提供了相同的操作接口,但能适用于不同的应用要求,当基于父类所设计的程序需要适应子类操作变化时,就可以采用虚方法来通知编译器这种可能的变化。
注意:只有当父类对象指向不同的子类对象时,虚方法可以表现出不同的实现方法,且都可以通过父类对象实现访问。如果父类对象没有指向子类对象,即便父类中声明了虚方法,父类对象调用该虚方法时也不会进行动态查找,而是直接调用父类方法(见①中代码p1=p2之前的结果)
子类句柄赋值给父类除了直接赋值(p1=p2),还可以通过函数调用的方式赋值:
process是某一个模块中的任务,形参tr是一个父类对象,在process(etr)语句中将实参etr传递给形参tr,也就是子类句柄传递给了父类句柄,也就是相当于执行了语句:tr=etr;由于类中compute_crc方法不是虚方法,所以执行process(etr)任务时,tr.compute_crc()依然调用的是父类方法。
虚类和参数化类
为了进一步实现代码重用,sv还提供了虚类(抽象类)和参数化类(模板)
①虚类
我们可以从一个基类派生出很多的子类,也就是说,一个基类定义了很多子类的原型,于是我们可以抽象出一个类来说明子类的共有特征,这个抽象的类就是虚类,同样的使用virtual来定义。
virtual class base_packet;
虚类一般不用来定义对象,通常其内部会定义虚方法,而该虚方法只提供原型,而没有具体实现,也就是不需要写函数体,因此被称为纯虚方法,使用pure关键字来定义:
pure virtual function int send(bit [31:0] data);
注意:只有虚类可以定义纯虚方法,简而言之,虚类就是一个抽象的类模板。不可以实例化。
虚类同样可以扩展为非虚类,且扩展为非虚类后,其内部的纯虚方法必须被重写,也就是要提供具体实现:
class ether_packet extends basepacket;
virtual int function send(bit[31:0] data);
具体实现
endfunction
endclass
②参数化类
参数化类常用来修改例化对象的数组大小和数据类型:
1.参数化对象数组大小
class vector #(int size=1); //此时vector是一个参数化的类,将数组长度参数化,
bit[size-1:0] a; 参数size的初始值为1
endclass
vector #(10) V1; //对象V1的数组长度为10
vector #(.size(2)) V2; //对象V2的数组长度为2
typedef vector #(4) V3 //V3是size=4的vector类(为避免重复声明参数类,常用typedef)
2.参数化对象数据类型
class stack #(type T=int); //参数化类型T,参数T的初始类型为int
T item[]; //定义了T类型的动态数组
endclass
stack S1; //对象S1的数组类型为缺省值:int
stack #(bit[1:10]) S2; //对象S2的数组类型为10位向量(bit [1:10]item[])
typedef stack#(V3) S3; //S3是一个类
3.参数化类的扩展extends
class c#(type T=bit);...endclass //c为基类
class d #(type P=real) ectends c#(intenger); //d类继承了参数为integer的c类
约束重写
B类中对约束进行了修改,约束与虚方法类似,将子类句柄赋值给父类句柄后,调用随机函数,会动态查找重写约束。而无需用virtual声明。
数据的隐藏与封装
在sv中,未被限定的类属性和方法默认是公共的(public)
被声明为local的数据成员或方法:只对自身可见,对于外部或者子类不可见
被声明为pritected的数据成员或方法,对外部不可见,对自身和子类可见。
在指定这些修饰符的时候没有预定义的顺序; 然而, 对于每一个成员它们
只能出现一次, 也就是不可以将成员同时定义为 local 和 protected。