类的概述
类的三要素:类的封装、类的继承、类的多态
- 类是一种可以包含数据和方法(function,task)的类型。
- 例如一个数据包,可能被定义为一个类,类中可以包含指令、地址、队列ID、时间戳和数据等成员。
- packet这个类可以在其中对这些数据做初始化,设置指令,读取该类的状态以及检查队列ID。
- 每一个packet类例化的具体对象其数据成员都可能不相同,然而packet类作为描述这些数据的抽象类型,将其对应的数据成员和操作这些数据成员的方法都定义在其中。
术语
- 类(class):包含成员变量和成员方法。
- 对象(object):类在例化后的实例。
- 句柄(handle):指向对象的指针。
- 原型(prototype):程序的声明部分,包含程序名、返回类型和参数列表。
构建函数
- SV并不像C++语言一样要求复杂的存储空间开辟和销毁的而是采用了像Java一样空间自动开辟和回收的手段。
- 因此SV的类在定义时,只需要定义构建函数(constructor)而不需要定义析构函数(destructor)。当全局没有任何一个句柄指向它时类的空间就自动释放了
- 类在定义时,需要定义构建函数,如果未定义,则系统会自助定义一个空的构建函数new()(没有形式参数,函数体亦为空)
- 对象在创建时(对象天然是动态的,仿真之后才有值),需要先声明再例化,同时进行亦可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26class packet_c; //类的声明
integer command;
function new(int inival);
command = inival; //new函数没有返回值,默认每个类中都有new函数,主要为了开辟空间和初始化句柄
endfunction
endclass
module packet_m; //模块声明
integer command;
endmodule
typedef struct{ //结构体声明
integer command;
}packet_s;
module tb;
packet_m ml(); //模块例化,只能在模块中,不能再initial中
packet_m m2();
packet_s s1='{1}; //结构体例化,可以在外部也可以在initial中
packet_c c1 = new(2); //开辟空间,句柄指向创建对象的头部
initial begin:ini_pro1
packet_s s2 = '{1}; //结构体是静态的,在仿真之前就已经有值
packet_c c2 = new(3); //类例化,可以在外部也可以在initial中
end
end endmodule //对象天然是动态的,仿真之后才有值,除非类中有静态变量,否则创建对象前不开辟空间
静态成员(变量/方法)
- 类的成员(变量/方法)默认都是动态(automatic)生命周期,即每一个对象的变量和方法都会为其开辟新的空间。
- 如果多个对象为了共享一个成员(变量/方法),那么可以为其添加关键字static。
- 多个对象因此可以共享同一个成员变最或者方法。
- 访问该成员时,无需进行对象的例化。
- 成员方法也可以声明为静态
- 静态的方法无法访问非静态成员(变量/方法)否则会发生编译错误。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17class packet_c; //类的声明
integer command;
static data; //静态变量声明,静态变量在仿真前就开辟了空间
function new(int inival);
command = inival;
endfunction
endclass
moduletb3;
packet_c cl, c2;
initial begin: ini_proc
$display("packet_c static data is 80d", packet_c::data);
c1=new(10);
$display("cl. command=80d, cl. data=80d", cl.command, cl.data);
c2=c1; //
$display("c2. command=80d,c2. data=80d",c2.command,c2.data);
end endmodule
this语句
- this是用明确索引当前所在对象的成员(变量/参数/方法)
- this只可以用来在类的集静态成员、约束和覆盖组中使用。
- this的使用可以明确所指向变量的作用域。
1
2
3
4
5
6
7class Demo;
integer x;
function new(integer x);
int x=3;
this.x=x; //若是没有使用this,则默认采用就近原则,寻找最近的成员变量(最好使用this)
endfunction
endclass
类的赋值和拷贝
声明变量和创建对象是两个过程,也可以一步完成。
1
2Packet p1;
p1=new;如果将p1赋值给另外一个变量p2,那么依然只有一个对象,是指向这个对象的句柄有p1和p2.
以下这种方式表示p1和p2代表两个不同的对象。在创建p2时,将从p1拷贝其成员变量例如integer、string和句柄等种拷贝方式称为浅拷贝(shallow copy)。
1
2
3
4
5
6Packet pl;
Packet p2;
Packet p3;
p1 = new(); //只有经历过new函数才算生成一个对象
p3 = p1; //句柄赋值,只有一个对象,但是有两条路寻找到(一个对象,两个句柄)
p2 = new p1; //浅拷贝,两个对象,但只能拷贝对象的内容,如果里面有嵌套的对象内容无法复制(下层对象句柄可以)
深拷贝和浅拷贝的区别
- shadow copy,是一种简易复制,类似于原对象的影印本,只拷贝原对象的内容,不拷贝对象(用new操作符)
- 如果类中包含一个指向另一个类的句柄,那么只有高一级的对象(变量和句柄)被new复制,下层的对象的内容不会被复制(但是下层对象的句柄可以被复制,即复制后,两个对象指向同一个低一级的对象!)
- deep copy可将拷贝对象中所包含的对象拷贝过来(只能自定义copy函数,copy中需要调用new函数)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29class Transaction;
bit [31:0] addr,crc,data[8];
statistics stats; //子对象
static int count=0;
int id;
function new;
stats=new();
id=count++;
endfunction
function copy; //深拷贝函数
copy=new();
copy.addr=addr;
copy.data=data;
copy.crc=crc;
copy.stats=stats.copy();
id=count++;
endfunction //将每一个量都拷贝一遍
endclass
Transaction src, dst;
initial begin
src=new();
src.stats.startT=42;
dst=src.copy();
dst.stats.startT=96;
$display(src.stats.startT);
end
参考:SV中shadow copy和deep copy的区别
数据的隐藏和封装
- 类的成员(变量/方法)默认情况下,即是公共属性的。这表示对于类自身和外部均可以访问该成员。
- 对于商业开发,类的提供方会限制一些类成员的外部访问权限,继而隐藏类成员的更多细节。
- 这种方式也使得类的外部访问接口更为精简,减轻了类的维护工作量,也使得类在修改时便于与旧版本保持兼容。
- 数据隐藏的方式使得类的测试和维护都变得更为简单。
- 对于成员的限定,如果使用local,则只有该类可以访问此成员,而子类或者外部均无法访问。
- 对于成员的限定,如果使用protected,则表示该类和其子类均可以访问此成员,而外部无法访问。
1
2
3
4
5
6
7class Packet;
local integer i; //只有该类可以访问此成员,而子类或者外部均无法访问 本地成员
protected integer j;//该类和其子类均可以访问此成员,而外部无法访问 内部成员
function integer compare (Packet other);
compare=(this.i == other.i);
endfunction
endclass
类的继承和子类
之前定义过的类Packet, 通过extends,LinkedPacket继承于其父类Packet,包括继承其所有的成员(变量/方法)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35class packet;
integer i=1; //例化对象时首先在这里开辟空间顺便赋初值(如果new中赋值,这里最好只声明)
function new(int val);
i=2; //然后再运行new函数,覆盖初值
endfunction
function shift()
i=i<<2;
endfunction
endclass
class linkedpacked extends packet; //子类可以继承父类所有的变量&方法,并可以进行增补
//integer i=3; //如果子类声明了i那优先索引的是子类的i
function new(); //不管子类有没有new函数,都会先默认调用父类的new函数,如果子类有再执行自己的new函数
super.new(val) //如果父类new有参数,那么必须在子类的new中通过super.new(val)完成继承,如果父类没有参数,系统默认调用继承
i=3;
endfunction
function shift()
i=i<<2;
endfunction
endclass
module tb;
initial begin
packetp=new(3);
linkedpacked lp;
$display("p.i=%0d",p.i);
$display("lp.i=%0d",lp.i);
p.shift();
$display("after shift p.i=%0d",p.i);
lp.shift();
$display("after shift ,lp.i=%0d",lp.i);
end
endmodule子类和父类存在重名变量或者函数,那调用子类的函数(如果没有使用
super
),子类如果没有才会调用父类的。子类执行new函数,必须先调用父类的new完成继承,如果父类new没参数,则可以自动完成调用,如果父类new函数有参数必须在子类的new中通过
super.new(val)
完成继承子类如果有相应的变量或函数那就调用自己的,没有的话就去父类找找。
子类成员优先级高于父类,调用this、super则是明确调用域。但子类必须通过new函数继承父类。
子类句柄赋值给父类句柄,访问权限只有子类从父类继承的那一部分。
父类的句柄不能赋值给子类的句柄,因为父类较小,会有非法空间出现。
1
2
3
4packet p=new(3);
linkedpacked lp=new(1);
packet tmp;
tmp=lp; //子类句柄可以赋值给父类句柄,但句柄访问受限
写代码的好习惯:
1、所有的类都要写new函数(哪怕为空)
2、子类的new函数中,第一行先写上super.new()完成继承
3、子类的变量不要和父类重名,方法可以
4、在类中调用函数的时候最好写上this、super提高可读性
5、在包、类等初始化的时候加上后缀如pkt_a
包的使用
- 为了便得可以在多个模块(硬件)或者类(软件)之间共享用户定义的类型,SV添加了包(package)Verilog没有包的概念。
- 包的概念参考于VHDL,用户自定义的类型譬如类、方法、变量、结构体、枚举类等都可以在package…..endpackage中定义。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38package pkg a; //声明包a
class packet_a;
endclass
typedef struct{
int data;
int command;
}struct a;
int va=2;
endpackage
package pkg_b; //声明包b
class packet_b;
endclass
typedef struct{
int data;
int command;
}struct b;
int vb=3;
endpackage
module tb;
import pkg_a::packet_a; // 导入包内的类
import pkg_a::packet_a;
import pkg_a::va; // 导入包内的变量
class packet_tb;
endclass
typedef struct{
int data;
int command;
} struct_tb;
initial begin
pkg_a::packet_a pa=new();
pkg_b::packet_b pb=new(); // 初始化一个包,和例化类的对象不同需要声明域pkg_b::
packet_tb ptb=new();
$display(pkg_b::vb) // 直接通过域索引包内变量
end endmodule
包的索引
方法一,直接通过域索引
1
2
3
4pkg_a::packet_a pa=new();
pkg_b::packet_b pb=new(); // 声明域pkg_b::索引包内的类
packet_tb ptb=new();
$display(pkg_b::vb); // 直接通过域索引包内变量方法二,导入包中内容
1
2
3
4
5
6import pkg_a::packet_a; // 导入包内的类
import pkg_a::packet_a;
import pkg_a::va; // 导入包内的变量
import pkg_a::*; //导入包内全部内容,编译器自己找
import pkg_a::*; //但这这种 非精确查找优先级会很低,在编译器找不到的时候,才会考虑这两个包
思考问题
类和结构体的联系和差别有哪些?
联系:
1.定义属性用来存储值,并进行了封装;
2.定义下标脚本用来允许使用下标语法访问值;
区别:
1.类里面可以有函数方法,可以继承,结构体不行
2.类的成员默认动态,而结构体默认静态
3.类的成员权限默认public但是可以限定,结构体只能是public类和模块(module)的联系和差别有哪些?
- 为什么类的静态方法不能访问类的非静态(动态)成员变量?
- 如果有同名的模块,那么在编译过程中应该怎么解决“同名”问题?