第五章. 面向对象编程基础
5.1 OOP概述
V属于过程性编程语言(代码逐行执行,无数据结构,类似C语言),V中没有结构,只有位向量和数组。而在对总线事务建模时往往需要数据结构,使用过程性语言不够便利。
SV属于面向对象编程语言(Object Oriented Programming,OOP),OOP所有的功能都是基于类来实现的,类中可以封装成员变量和成员方法,这极大提高了建模的效率。OOP的基本单元是类(class)和对象(object),通过这些基础的单元来实现OOP编程语言的三个特性,封装(encapsulation),继承(iheritance),多态(polymorphism)。因此,可以简单的说:OOP=类+对象+封装+继承+多态。
5.1.1 类
包含成员变量和成员方法。类不是对象,类描述了实例化对象的规则,定义了实例化对象中包含哪些成员变量和成员方法,可以简单理解类是图纸,对象是通过图纸构建的实体。
5.1.2 对象
是类的一个实例。类是抽象的,而对象是由抽象的类具体化的一个实体。在实例化对象时,必须先定义类,否则SV无法实例化对象,因为SV不知道如何创建对象。
5.1.3 封装
SV中使用类来实现封装。类中封装了成员变量和成员方法。
5.1.4 继承
允许通过现有的类去得到一个新类。现有的类称为父类或基类,得到的新类称为子类或派生类。子类拥有父类成员变量和成员方法。
5.1.5 多态
多态的实现基于继承概念。将父类中的方法声明为virtual,并在子类中实现该方法。当父类句柄指向子类对象时,通过父类句柄调用该方法,方法会依据父类句柄实际指向的对象选择调用子类中的方法,而不是父类中的方法。这种父类/子类有相同的方法名称,但能够依据对象准确调用的特性称为多态。
5.1.6 属性
类中存储数据的变量,其实就是成员变量。
5.1.7 方法
类宏定义的方法,其实就是成员方法。
5.1.8 原型
原型即程序的头,包含程序名,返回类型和参数列表。与程序头相对应的为程序体,包含了该函数要执行的代码。
5.1.9 句柄
对标C中指针(pointer)。
5.2 创建新对象
SV中可以把类定义在program,module,package或者在这些块之外的任何地方。类可以在程序和模块中使用。V和SV都具有例化的概念,但是在细节方面存在一些区别。V的例化是静态的(编译的时候例化),就像硬件一样在仿真的时候不会变化,只有信号值在改变。而SV中例化可以理解为动态的(运行的时候例化和释放内存),激励对象不断地被创建并且用来驱动DUT,检查结果,最后这些对象所占用的内存可以被释放,以供新的对象的使用。V的顶层模块是不会被显式的例化的,SV类在使用前必须先例化。另外,Verilog的实例名只可以指向一个实例,而SV句柄可以指向很多对象。
//声明和使用句柄
Transaction tr; //声明句柄
tr = new(); //为tr分配内存空间
在声明句柄时,初始化为特殊值null
,然后调用new()函数创建Transaction对象。new函数为Transaction分配空间,将变量初始化为默认值(二值逻辑默认值为0
,四值逻辑默认值为X
)new为构造函数。
5.2.1 定义构造函数
new()函数称为构造函数,默认情况下构造函数会分配内存,初始化变量。new()函数不能有返回值,因为构造函数总是返回一个指向类对象的句柄,其类型就是类本身。SV怎么知道该调用哪个new()函数呢?这取决于赋值操作符左边的句柄类型。
class Transaction;
logic [31:0] addr, crc, data[8];
function new;
addr = 3;
foreach(data[i]) data[i] = 5;
endfunction
endclass
5.2.2 将声明和创建分开
应该将声明和创建分开,避免在声明一个句柄的时候调用构造函数。虽然在语法上合法,但是这会引起顺序问题。因为构造函数在第一条过程语句之前就被调用了。
5.2.3 new()和new[]的区别
new()用来创建对象,可以包含参数;new[]用来为数组分配内存,只可以包含数组的大小。
5.3 对象的解除分配
SV不允许对句柄做和C类似的改变(C中一个无类型指针知识内存中的一个地址,可以将它设定为任何值,还可以通过预设增量来改变指针),也不允许将一种类型的句柄指向另一种类型的对象,SV中的指针更接近于Java。
垃圾回收就是一种自动释放不再被引用的对象的过程。SV分辨对象不再被引用的方法就是记住指向它的句柄的数量,当最后一个句柄不再引用某个对象了,SV就释放该对象的空间。而在C/C++中,指针可以指向一个不再存在的对象。SV不能回收一个被句柄引用的对象,可以**通过给指针赋值nul
,清除句柄。**如果对象包含从一个线程派生出来的程序,那么只要该线程仍在运行,这个对象的空间就不会被释放。
5.4 使用对象
Transaction t; //声明Transaction类型句柄
t = new(); //创建对象
t.addr = 32'h42; //设置变量值
t.display(); //调用子程序
严格的OOP规定,只能通过对象的公有方法访问对象的变量,如get()和put()。此规定保证了无法通过类实例进行外部赋值来改变类成员变量的值,保证类的对外部的封装性。在创建测试平台中,目标是最大限度的控制所有变量,以产生最广泛的激励,所以不可能实现严格的OOP规定。
5.5 静态变量
全局变量:关键字static,将变量定义为全局变量(静态变量)。静态变量通常在声明时初始化,而不是在类的new函数中初始化。在类中定义静态变量,该变量属于类所有,即通过该类实例化的对象共享同一个变量,通过类名+类作用域操作符(::)+变量名
方法访问class::parameter
类的静态变量。
5.6 类的方法
静态方法:方法名称前加入static
关键字。SV中可以在类中创建一个静态方法读写静态变量的值,SV不允许静态方法读写非静态变量。
在类外定义方法;在类中声明方法时在方法名称前添加extern
关键字。然后将整个方法移至类的外部,并在方法名前加上类名和类作用域操作符::
。类中的方法默认使用自动存储。
class transaction;
extern function void display;
endclass
function void transaction::display;
...
endfunction
5.7 作用域
作用域是一个代码块,例如模块,程序,任务,函数,类或者begin-end块。作用域可以相对于当前作用域,也可以使用绝对作用域,绝对作用域$root
开始(类比Linux中的相对路径和绝对路径)。
//名字作用域
int limit; //$root.limit
program automatic p;
int limit; //$root.p.limit
class Foo;
int limit, array[]; //$root.p.Foo.limit
//$root.p.Foo.print.limit
function void print (int limit);
for(int i=0; i<limit; i++)
begin
$display("%m: array[%0d] = %0d", i, array[i]);
end
endfunction
endclass
endprogram
this
:当你使用一个变量名的时候,SV将首先在当前作用域内寻找,接着在上一级作用域内寻找,直到该找到改变量为止。当你想引用类一级的对象,可以使用this明确地指明变量的作用域为当前类。
编译顺序:如果需要编译一个类,而这个类包含一个尚未定义的类 。声明这个被包含的类的句柄会引起错误,因编译器还不认识这个新的数据类型。可以使用typedef
语句声明一个类名。
//使用typedef class语句声明statistics是一个类
typedef class Statistics
class Transaction;
Statistics status;
...
endclass
class Statistics;
...
endclass
5.8 动态对象
静态分配内存的语言中,每一块数据都有一个变量与之关联,而在OOP语言中,不存在这种一一对于关系。可能有很多对象,但是只定义了少量的句柄。
5.9 对象复制
复制分为浅复制(shallow copy)和深复制(deep copy)。
如果拷贝的对象里的元素只有值,没有句柄,浅拷贝和深拷贝没有差别。都会将原对象复制一份,产生一个新对象,对新对象的值进行修改不会影响原有的对象。
如果**拷贝的对象里的元素包含句柄,则深拷贝和浅拷贝是不同的,浅拷贝复制的是原句柄,其指向与原句柄相同,使用浅拷贝新对象对句柄进行修改会改变原对象句柄指向值。**深拷贝会复制原对象句柄指向的对象,产生一个新的对象,对深拷贝产生的新句柄修改不会影响原句柄的值。简单来说就是浅拷贝只是复制了句柄,并没有对句柄指向对象进行复制,浅拷贝复制的句柄与原句柄指向同一个对象。而深拷贝是将句柄指向的对象复制。
示例:
//使用new复制一个对象,创建了一个新的对象,并且复制了现有对象的所有变量。new复制属于浅复制。
Transaction src, dst;
initial
begin
src = new; //创建第一个对象
dst = new src; //使用new操作符进行浅复制
end
编写copy函数:copy函数属于深复制(注意复制对象中含有句柄的变量,需要调用句柄类型的copy函数,重新分配空间,指向一个新的对象)。
5.10 公有和私有
SV中类似其他OOP语言,成员变量的访问权限有以下三种local(类似C++中的private),protected,public,访问权限依次扩大。
local:只有该类中函数可以访问,子类和外部类都无权访问。
protected:该类和其子类中可以访问,类外部无权访问。
public:类中,子类,类外均可访问。
SV中定的类中,数据默认被定义为public类型。local,protected类型必须显式声明
参考文献:
1. SystemVerilog验证 测试平台编写指南(原书第二版)张春 麦宋平 赵益新 译
2. 百度百科