---------感谢移知的SV课程-----------
OOP
- OOP的语法介绍
-
- 继承
- 多态
OOP的语法介绍
OOP的全称为Object Oriented Programming, 即面向对象编程。SystemVerilog引入了一个面向对象的数据类型,对象是类的实例,类是对象的模板。比如人是一个类,是一个模板,里面有吃饭睡觉说话等特性,然后男人女人老人小孩就是类的实例化对象。在SV中,这样描述
P a;//P是定义的一个类,属于一个数据类型,a属于P的一个实例
int b;//int是一个数据类型,b属于int的一个实例
类是一种数据类型, 它包含了数据以及对数据进行操作的子程序(函数和任务)。类的数据被称为类属性,它的子程序被称为方法,无论是类属性还是方法都是类的成员。类属性和方法结合在一起,定义了某种类型的对象的内容和能力。
可以通过类来创建类的实例,类的实例称为对象。对象可以动态地产生、删除(回收)、 赋值、以及通过对象句柄访问。对象句柄为语言提供了一种安全的、类似于指针的机制。类提供了继承和抽象建模的能力,OPP有三大特性:封装、继承、多态
下例为一个类的定义,类中包含一些属性和方法,类可以创建不同的实例,调用他们所需要的方法,其中有两个类的方法。
class packet;//packet为类的名字
//数据或类属性
bit [3:0] command;
bit [4:0] address;
integer status;
//初始化
function new();//new函数是sv本身有的,也叫构造函数 也可
//以带参数,那么创建对象的时候可以用,没有就默认变量
command = IDLE;//赋默认初始值
address = 4'b0;
endfunction
//类的方法
//公共访问入口
task clean();
command = 0;
address = 0;
endtask
function integer current_status();
current_status = status;
endfunction
endclass
在上例子中,在使用一个类的时候,需要先声明一个类型为该类的变量(为这个变量保存一个对象句柄),然后使用new函数产生该类的一个对象。
Packet p;// 声明一个为packet数据类型的变量,相当于即将要创建对象的指针
p = new;//,调用类里面的构造函数进行初始化。将变量初始化成packet新分配的一个对象,开辟一个空间
变量p保存了一个对象句柄(handle/类似于指针),该句柄指向了Packet类的一 个对象。缺省情况下,未初始化的对象句柄被设置成一个特殊值null。比如c语言中int a;没有赋值情况下就是null。 我们可以通过将一个对象的句柄与null进行比较来确定对象是否已被初始化。可以通过p.* 访问对象里面的类属性或者方法。
packet p = new;
p.command = 0;//对类属性的访问
p.address = $random;
status = p.current_status(); //对方法的访问
其中p为对象的handle,对对象的引用通过“.”的方式访问对象的属性与方法,p相当于指针。
下例为对多个对象分配地址
packet t1, t2;
t1 = new;//首先开辟一个空间,初始化了对象,t1指针指向这个对象
t2 = t1;//t2指针也指向和t1一样的对象,同种类型才可以这样
t1 = new;//又开辟新的空间,t1指向新开空间的对象
packet t1;
t1 = new();//首先开辟一个空间,t1指向对象
t1 = new();//指向新的对象,之前的就没有指针指向,会被回收,释放内存
静态变量
对于同一个类,例化不同对象,里面的值的变化各不相关。有时我们需要共享所有对象里面的一个类属性或者方法。就要使用关键字static,称为静态变量。
class packet;
static integer a = $fopen("data","r");
endclass
packet p1,p2;
p1 = new;
p2 = new;
p2.a = 2;//等价于p1.a = 2
对静态变量的访问
1.通过handle来访问,2通过类名来访问,即使该类没有实例也可通过操作符 ::
class packet;
static int count = 0;
...
endclass
initial begin
$display("%d packet is apple", packet::count);
end
静态方法
方法也可以被声明成静态的。它就像一个常规的子例程一样可以在类的外部被调用,即使没有该类的实例。
一个静态方法不能访问非静态的成员(类属性和方法),**但它可以直接访问静态类属性或调用同一个类的静态方法。**在一个静态方法体内部访问非静态成员或访问特殊的this句柄是非法的,并且会导致一个编译错误。
静态方法不能是虚拟的。
class packet;
static config,cfg;
static int count = 0;
int id;
static function void display_statics();
$display("packet cfg.mode = %s, count = %d",
cfg.mode,name(),count);
endfunction
config cfg;//类的实例 另外定义
initial begin
cfg = new(MODE_ON);
packet::cfg = cfg;//创建的对象赋给静态的packet里面的cfg
packet::display_statics()
end
在类外面定义方法
Systemverilog中,可以将类的方法的原型定义(方法名和参数)放在类的内部,而方法的程序体放在类的外部。通过关键字extern可以实现方法的声明,然后通过作用域操作符::来表名这个函数作用于哪个类。
class packet;
extern function void display();
endclass
function void packet::display();
...
endfunction
this关键字
如果在类的深处,想要引用类一级的变量, 可以通过this关键字很方便的做到。this关键字被用来明确地弓|用当前实例的类属性或类方法。
this关键字应使用在非静态的类方法中,否则会发布一个错误。
class packet;
string name;
function new(string name);
this.name = name;//这个指的就是上一级的name
endfunction
endclass
packet p1 = new(2);//等于那个string name等于2
在一个类中调用另外一个类:可以在一个类的内部,通过先定义另一个类的变量然后再function对另一个类进行new,然后还可以在这个类中通过handle访问另外的类,用“."访问。
对象的复制:下图产生了一个变量p1,它保存了一个 类型为Packet的类对象的句柄, 但p1的初始值为null。在Packet类型的一个实例被产生之前,对象并不存在,并且p1没有包含一个真实的句柄。
通过new操作符,产生了一个对象,并且将p1赋值给p2。那么,此时仍然只有一个对象,它可以使用p1或p2引用。
注意: new仅仅执行一次,所以只产生了一个对象。
packet p1;
p1 = new;
packet p2;
p2 = p1;
通过new操作符拷贝对象
packet p1,p2;
p1 = new;
p2 = new p1;
最后一条语句使得new第二次执行,因此产生了一个新的对象p2,它的类属性拷贝自p1。
这种方式的拷贝被称作浅拷贝(shallow copy) 。所有的变量都被拷贝:整数、字符串、实例句柄等。然而其中包含的对象没有被拷贝,拷贝的是它的句柄;与前面的例子一样,产生了相同对象的两个名字。(p1中包含的类不会拷贝,只是拷贝了句柄)
类似于静态变量,下图后后面的对象的数值在两个句柄里面是一样的
继承
SystemVerilog支持单继承,也就是说,每一个子类都由一 个单的父类继承而来。继承是面向对象编程的重要概念。
使用继承可以定义子类型,在子类型中增加新的方法和数据。被继承的类一般称为基类或者父类 ,得到的新类一般称为子类。
Systemverilog中使用extends来实现继承。
class packet;
int addr, data;
function new();
endfunction
endclass
class ppp extends packet;//ppp继承packet
string s;
function ppp get_next();
endfunction
endclass
例如:子类除了具有父类的属性和方法外,还可以扩展自己的属性和方法,例如本例中,子类ppp不仅具有父类的addr,data等属性,还扩展了自己的方法get_ next。
super关键字在一个继承内部使用,可以用来引用父类的成员变量。当父类和子类的变量命名不冲突时,在子类对父类的变量进行操作可以不用super关键字,如果冲突则需要。
class packet;
int a,b;
function new();
.
.
endfunction
endclass
////////////////////////////
class link_packet extends packet;
int c,d;
function new();
super.new();
//也可以调用方法,还是相同操作
endfunction
endclass
子类在实例化的时候会调用类方法new()。在函数中定义的任何代码执行之前,new( )执行的一个动作需要调用其父类的new()方法,并且会沿着继承树按这种方式一直向上调用。因此,所有的构造函数都会按正确的顺序调用,它们都是起始于根基类并结束于当前的类。如果父类的初始化方法需要参数,那么会有两种选择:
1)总是提供相同的参数。
2)使用super关键字
function new();//更加通用
super.new(5);//必须是函数的第一条语句
endfunction
继承的一些特性
1)数据隐藏
2)在SystemVerilog中,未被限定的类属性和方法是public的,它们对访问对象名字的任何人都是有效的。即所有的类属性和方法都毫无限制地在类外可见。
3)然而,有时我们希望通过隐藏类属性和类方法来限制在类的外部访问他们,而且防止仅对类内部有效的类属性被偶然地修改。因此需要对这些属性和方法做隐藏。
4)Systemverilog通过local和protected关键字来实现数据的隐藏。
protected关键字
-一个被标识成protected的类属性或方法具有local成员的所有特性,但是它可以被继承以及对子类可见。
class p;
protected int a;//可以被继承,对子类可见
endclass
local关键字
一个被标识成local的成员仅对类内的方法有效。而且这些本地成员在子类内是不可见得。以local来声明某些member,这些member只能被这个class使用,继承的子类无法使用。当然,访问本地类属性或方法的非本地方法可以被继承,并且作为子类的方法它可以正确地工作。local类似C+ +的private。
class ppp;
local int i;
function int compare(ppp other);
compare = (this.i == other.i);//当子类继承该父类,
//并且采用compare函数时候,可以通过this关键字对i进行访问
endfunction
endclass
继承总结
继承后的类可以实现以下功能:
子类继承了父类的属性和方法,并可以修改。可以添加新的属性和方法。
在实现以上功能的同时需要满足一定的规则:
子类可以重写父类中的属性和方法,如果一个方法被重写,其必须保持和父类的原有定义一致的参数
子类可以通过super操作符来引用父类中的方法和成员
被声明为local的数据成员和方法只能对自己可见,对外部和子类都不可见,对声明为protected的数据成员和方法,对外部不可见,对自身和子类可见
多态
抽象类
我们可以产生一组类,它们可以看作是来源于一个共用的父类。此父类只定义公共的模型,并不会实例化,这样的类我们定义成抽象的类,virtual class,抽象类不能实例化。
虚方法
虚方法是基本的多态构造,通过关键字virtual来实现。子类中的虚拟方法重写应具有与父类匹配的参数类型,相同的参数名称,相同的限定符以及与原型相同的方向。
OOP多态
先看一个例子
class packet;
int i;
function int get();
get = i;
endfunction
endclass
class ppp extends packet;
int i = 2;
function int get();
get = -i;
endfunction
endclass
ppp lp = new;
packet p = lp;
j=p.i;//j=1
j=p.get();//j = 1
创建ppp类的对象称为lp,将lp赋值给packet的handle p,通过p调用get()。子类的指针不可以指向父类,父类指针可以指向子类。
Virtual
Systemverilog中多态通过virtual关键字来实现,多态性使得父类中的一个变量 (变量就是lp或者p)能够保存子类对象,并且能够直接从父类变量中引用这些子类的方法。
多态性如下:
如果方法不是virtual方法,则调用此方法时,由handle的类型来决定调用哪个方法。(上面例子不是virtual方法,handle p的类型是父类)
如果方法是virtual方法,则调用方法时,由object的类型来决定调用哪个方法。
class packet;
int i;
virtual function int get();
get = i;
endfunction
endclass
class ppp extends packet;
int i = 2;
virtual function int get();
get = -i;
endfunction
endclass
ppp lp = new;
packet p = lp;//父类句柄指向子类对象
j=p.i;//j=2
j=p.get();//j = -2
实现多态1)方法的重写,2)有无virtual关键字 3)句柄的复制
补充知识
上图的p和lp是关于对象的一个引用,p和lp是一个handle(句柄)/指针。那些对象指的是object。
强制类型转换
将一个子类变量赋值给层次树中较高的父类变量是合法的。将一个父类变量直接赋值给一 个子变量则是非法的。(上面例子)
如果父类句柄引用了指定子类的句柄(即父类已经指向了子类),那么将一个父类句柄赋值给一个子类变量则是合法的。就是虽然这个句柄的类型是父类。但是指向的对象类型是子类
为了检查赋值是否合法,需要使用动态强制类型转换函数$cast,语法如下:
$cast(dest_handle,source_handle);
常量类属性
类属性可以通过const限定符声明为只读的。由于类属性是动态的对象,类属性允许有两种形式的只读变量:全局常量和实例常量。
全局常量类属性是那些在声明中包含了初始值得常量类属性。它们与其它的const变量类似,也就是它们不能在除声明之外的其它地方赋值。
实例常量在声明中不包含初始值,仅包含const限定符。这种类型的常量可以在运行时赋一个值,但赋值只能在对应的类构造器(new函数)中完成一次。
典型情况下,全局常量还能被声明成static,因为它们对于类的所有实例是共享的。然而,一个实例常量不能声明成static,因为它们在构造器不接受所有的赋值。
参数化的类
如果定义一个基本类,它的对象在实例化的时候可以具有不同的数组尺寸或数据类型,这种功能通常很有用,可以使用参数化的类来实现。普通的Verilog参数机制可以用来参数化一个类, 例如:
class vector #(int size = 1);
bit [size-1:0] a;
endclass
vector #(10) vten;
vector #(.size(2)) vtwo;
typedef vector#(4) vfour;
参数话一个类,也可以将数据类型作为参数,如下例定义了一个基本的stack类,它可以使用任意的数据类型实例化:
任何类型都可以作为参数,包括用户定义的类型
class stack #(type T = int);//初始为int型
local T item[];
endclass
stack is;
stack #(bit[1:10]) bs;
stack #(real) rs;
typedef类
有时一个类变量需要在类本身被声明之前声明。
例如,如果两个类中的每一个类都需要另外一 个类的句柄。 在编译器处理第一个类的声明期间,编译器遇到了第二个类的引用,而这个弓|用是未定义的,因此编译器会将它标记为一个错误。
通过typedef可以解决此问题,为第二个类提供提前声明,例如:
typedef class C2;//C2声明为class类型
class C1;
C2 c;
endclass
class C2;
C1 c;
endclass