类和对象、包

类和对象的概述

面向对象编程的三要素:封装(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句柄(指针):用来指向对象的指针

oop的概念要素

  • Class类:基本模块包含成员变量和方法。在Verilog中module也可以包含变量和方法,只不过它是硬件盒子,而class是软件盒子。
  • Ojbect对象:类的实例。Verilog中module也可以例化,这是'硬件′例化;在SV中可以使用class来例化,这是'软件'例化
  • Handle句柄(指针)︰用来指向对象的指针。在Verilog中,可以通过层次化的索引来找到结构中的设计实例,而在SV的对象索引时,需要通过句柄来索引对象的变量和方法。
  • Property属性(变量)︰在类中声明的存储数据的变量。在Verilog中,它可以是wire或者reg类型。
  • Method方法:类中可以使用task或者function来定义方法以便处理自身或者外部传入的数据。在Verilog中可以在module中定义task/function,也可以使用initial/always处理数据。
  • 再次强调Verilog的例化和SV class例化的差别
  1. 两者的共同点在于使用相同的'模板'来创建内存实例
  2. 不同点在于Verilog的例化是静态的,即在编译链接时完成,而SV class的例化是动态的,可以在任意时间点发生,这也使得类的例化方式更加灵活和节省空间。
  3. Verilog中没有句柄的概念,即只能通过层次化的索引方式A.B.sigx,而SV class通过句柄可以将对象的指针赋予其它句柄,使得操作更加灵活。
  • 在创建对象时,需要注意什么是'声明',什么是`创建′(即例化)
Transaction tr;//声明句柄
tr=new();    //创建对象,开辟新的内存空间,用来存放新的成员变量和方法

等价于
Transaction tr=new();

  • 任何类都需要new函数,创建对象会开辟新的内存空间,用来存放新的成员变量和方法。构建函数new()是系统预定义函数,不需要指定返回值,不加void,函数会隐式地返回例化后的对象指针。若没有自己定义new函数,系统会自动创建空的new函数。
  • new函数三个步骤:系统会通过new函数开辟空间(完成声明的初始化);进入new函数进行初始化;返回句柄。
  • 子类定义new函数时,应该首先调用父类的new函数,若父类new函数没有参数,子类也可以省略该调用,而系统编译时会自动添加super.new()

句柄的传递

类(抽象)和对象(具体)。对象(存储空间)句柄(空间指针),创建对象后,对象的空间位置不会更改,而指向该空间的句柄可以有多个。

Transaction t1,t2;  //声明句柄t1,t2   此时指针悬空
t1=new();           //例化对象,将句柄赋予t1(new例化对象,返回例化后对象的指针;之后赋给t1)
t2=t1;              //将t1的值赋予t2,即t1和t2指向同一个对象
t1=new();           //例化第二个对象,并将句柄赋予t1    例化新对象后,句柄指向最后一个对象

注意:当没有句柄指向对象时,该对象会被销毁。对象在域中会根据其为static、automatic进行销毁,如句柄声明其为动态,过程块外对象就会销毁(某个句柄多次使用时注意受其生命周期的影响)

initial begin
    automatic Transaction t;
    t=new();
end
initial begin
    #1ps           //new()执行完成之后,另一过程块了。变量空间不存在,句柄也销毁了
end

句柄的使用

Transaction t1,t2;  //声明句柄
t1=new(); //创建对象,将其指针赋予t1
t2=new(); //创建对象,将其指针赋予t2
t1=t2;    //将t2的值赋予t1,t1和t2指向同一对象,t1之前指向的对象被释放
t2=null; //将t2赋值为“空”,即不指向任何对象,此时指针‘悬空’,悬空的指针很‘危险’  (只有用句柄时用 
                                                                          null) 
#通过句柄使用对象中的成员变量或成员方法
t1.addr=32'h42; //对象的成员变量赋值                                             
t1.display();   //调用对象的成员方法                                

静态变量

  • 与硬件域如module,interface不同的是,class中声明的变量默认类型为动态变量,即其生命周期在仿真开始后的某时间点开始到某时间点结束。声明周期始于对象创建,终于对象销毁。class中的方法默认也是动态方法。
  • 若使用static声明变量为静态,则其生命周期开始于编译阶段,贯穿于整个仿真(静态指编译的时候就有位置,可以不通过对象引用,load后就有) 类中声明了变量,可以直接引用变量class::var,或者通过例化对象引用object.va。无论例化多少个对象,只可以共享一个同名的静态变量,因此类的静态变量在使用时需要注意共享资源的保护。

静态方法

  • 类里方法默认为动态,可以通过关键词static修改其类型为静态方法
  • 静态方法内可以声明并使用动态变量,但不能使用类的动态成员变量。因为在调用静态方法时,可能并没有创建具体的对象,也因此没有为动态成员变量开辟空间,因此在静态方法中使用类的动态成员变量是禁止,可能会找出内存泄漏,但是静态方法可以使用类的静态变量,因为静态方法同静态变量一样在编译阶段就已经分配好了内存空间。(动态方法一定要通过创建?才能调用)

类的成员

  • 类是成员变量和成员方法的载体,之所以称之为自洽体,是因为其变量和方法应符合`聚拢'原则,即一个类的功能应该尽可能简单,不应当承担过多的职责,更不应该承担不符合它的职责,这在设计模式中称之为单一职责原则(SRP Single Responsibility Principle) 。
  • 类作为载体,也具备了天生的闭合属性,即将其属性和方法封装在内部,不会直接将成员变量暴露给外部,通过protected和local关键词来设置成员变量和方法的外部访问权限。所以封装属性在设计模式中称之为开放封闭原则(OCP Open ClosedPrinciple) 。
  • 如果没有指明访问类型,那么成员的默认类型是public(public在sv中不是关键词),子类和外部均可以访问成员。
    如果指明了访问类型是protected,那么只有该类或者子类可以访问成员,而外部无法访问。
    如果指明了访问类型是local,那么只有该类可以访问成员,子类和外部均无法访问。(比如外部句柄无法访问)
    访问类型的设定是为了更好地封装类,尤其是要发布供他人使用的软件包(商业软件),但对于初学者以及应用范围较窄的验证环境,可以使用默认的访问类型,以便于在类的内部或者外部更方便地修改成员变量或者调用成员方法。
     
  • 封装:将属性和方法封装在内部,通过local和protected设置外部访问权限
  • 继承:继承包括了继承父类的成员变量和成员方法

比如local:外部句柄无法调用使用了local声明的变量,会报错,而通过方法调用则不会报错

ck.get_clock();  //调用内部方法,可返回变量
ck.clock;        //报错

类可以定义在module、interface、program和package,也就是所有‘盒子’,类中可以再声明类成员。类的编译顺序:先编译基本类,再编译高级类。或者说先编译将来被引用的类,再编译引用之前已编译类的类。(先父类后之类)

this:表明所调用的成员是当前类的成员(由近及远),而非同名的局部变量或者形式参数

super:继承父类

virtual function void calc_crc;
   super.calc_cac();          //注意super的使用方式

类的继承

this会先在当前寻找,没有的话会往上一层寻找?super直接从父类寻找?

父类(基类)  子类(派生类)

  • 就继承来看,类的继承包括了继承父类的成员变量和成员方法。
  • 子类在定义new函数时,应该首先调用父类的new函数即super.new()。如果父类的new函数没有参数,子类也可以省略该调用,而系统会在编译时自动添加super.new()。(此时new仅仅不会执行父类new函数中的初始化变量的部分,依然会为父类成员变量和方法先开辟空间)
  • 从对象创建时初始化的顺序来看,用户应该注意有如下的规则:
  1. 子类的实例对象在初始化时首先会调用父类的构造函数。
  2. 当父类构造函数完成时,会将子类实例对象中各个成员变量按照它们定义时显式的默认值初始化,如果没有默认值则不被初始化。
  3. 在成员变量默认值赋予后(声明的同时即赋值),才会最后进入用户定义的new函数中执行剩余的初始化代码。

成员覆盖

basic_test t;
test_wr wr;
initial begin
    wr=new();
    t=wr;     //将子类句柄赋给父类句柄
end
  • 在类定义里,父类和子类拥有同名的变量和方法也是允许的。当子类作用域中如果出现同父类相同的变量名或者方法名,则以子类作用域为准。同时我们也提供方法(super)来调用父类的变量或者方法。
  • 在上面的输出结果中,首先test_ wr类的对象wr调用构造函数new,而在构造函数执行序列中,也是先执行basic_ test::new, 再执行test_ wr::new。 在tes_ wr: :new中,可以通过super.def以及this.def来区分父类域的def或者子类域的def。默认情况下,如果没有super或者this来指示作用域,则依照从近到远的原则来引用变量即:
  1. 首先看变量是否是函数内部定义的局部变量。
  2. 其次看变量是否是当前类定义的成员变量
  3. 最后再看变量是否是父类或者更底层类的变量。

句柄的使用

  • 子类句柄指向子类对象(可以访问全局变量),优先访问子类对象;子类句柄赋值给父类句柄后,父类句柄指向子类对象,但只能访问属于其类型(继承于父类?)的变量和方法,直接理解为只允许父类句柄访问父类成员变量和方法(对于编译器不安全,无法判断指向什么对象,故如此操作)。
  • 父类句柄直接赋值给子类,编译器会报错。SV里只可以用$cast(子类,父类)对父类句柄转为子类句柄作检查,成功后可以访问所有区域。编译时遇到cast会跳过,会在仿真时检查。
  • 默认情况下,没有super或者this来指示作用域,则按照由近到远的原则来引用变量。
class test_wr extends basic_extend;
    int def=200;
    function new();
        super.new(def);  //此处def为子类def(200)
    ……
  • 句柄可以作为形式参数通过方法来完成对象指针的传递,从外部传入方法内部
task gegnerator;
    Transaction t;
    t=new;     //没有括号是因为在sv里,函数没有参数省略小括号
    transmit(t);
endtask

task transmit(Transaction t);   //句柄作为形式参数,通过方法完成对象指针的传递。注意不是对象传递
    ……                               对象创建后放在某个位置
endtask
  • 句柄可以在方法内部首先完成修改,再由外部完成使用
function void create(Transaction tr); //tr默认方向为input,可以改为ref或inout
    tr=new();              
    tr.addr=100;
    ……
endfunction      //函数完成后,tr没有返回                    
Transaction t;  //句柄悬空开始为null值
initial begin
    create(t);  //null传递过去,函数执行完不会做返回,t还是null    
    t.addr=10;  //句柄悬空,无法找任何变量,内存会泄露   会出错                                            
    $display(t.addr);
end
  • 程序执行时,可以在任何时刻为句柄创建新的对象,并将新的指针赋值给句柄
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需要额外定义,容纳各种数据、方法和类。
  • 这么做的好处在于将一簇相关的类组织在了单- -的命名空间(namespace) 下,使得分属于不同模块验证环境的类来自于不同的package,这样便可以通过package来解决类的归属问题
     
  • package里无法定义module、interface、program等硬件相关部分,可以定义类、静态方法和静态变量。类封装在包中,不应该在其他地方编译。没有介绍软件之前,硬件(module、interface、program)都会编译到库中。
  • 'include "stimulator.sv"  include属于纯文本替换;注意编译顺序来放置’include。 `include将文件中所有文本原样插入包含的文件中。这是一个预处理语句,`include在import之前执行。他的主要作用就是在package中平铺其他文件,从而在编译时能够将多个文件中定义的类置于这个包中,形成一种逻辑上的包含关系。import不会复制文本内容。但是import可将package中内容引入import语句所在的作用域,以帮助编译器能够识别被引用的类,并共享数据。
  • library是编译的产物,既可以容纳硬件(module、interface、program)类型,也可以容纳软件类型(如类和方法,也包括package)。从worklibrary里能找到各个package,无法直接找到package中的模块;而mcdt则可以直接从library中索引得到,不用import mcdt。
  • package是将命名空间分隔开,使用不同package中的同名类,只需注明使用哪个package。但内部定义的类也应该尽可能独一无二。如加上前缀。
regs_pkg::monitor mon1=new();   //::为域的索引
arb_pkg::monitor mon2=new();   //package和class都为域

包的命名规则

  • 在创建package的时候,已经在指定包名称的时候隐含地指定了包的默认路径,即包文件所在的路径。如果有其它要被包含在包内的文件在默认路径之外,需要在编译包的时候加上额外指定的搜寻路径选项"+incdir+PATH"。
  • 如果遵循package的命名习惯,不但要求定义的package名称独一无二,其内部定义的类也应该尽可能地独一无。例如,上面的例子中,regs_ pkg和arb_ pkg中有同名的类,这些类如果携带类名的前缀,那么后面的处理会变得更容易一些。
  • 如果不同package中定义的类名也不相同时,在顶层的引用也可以通过"import pkg_ name::*"的形式,来表示在module mcdf_ _tb中引用的类如果在当前域(mcdf内部)中没有定义的话,会搜寻regs_ pkg和arb_ pkg中定义的类,又由于它们各自包含的类名不相同,因此也无需担心下面的搜寻会遇到同名类发生冲突的问题。
module mcdf_tb;
    import regs_pkg::*          //mcdf中可以直接找到是因为就在work library里
    import arb_pkg::*          //当有package时,library只能找到包,包将类隔离,无法直接找到类
    regs_mom mom1=new();
    arb_mom mom2=new();
endmodule

包的使用

  • 在包中可以定义类、静态方法和静态变量。不能定义module、interface、program硬件相关部分。包不需要例化,是个容器,起隔离作用。包内可以导入其他包定义的类。只要import,包里编译的类型可以导入到其他域(如硬件域module、interface、program,软件域class,以及包等)
  • 如果将类封装在某一个包中, 那么它就不应该在其它地方编译,这么做的好处在于之后对类的引用更加方便。
  • 类和包是好朋友,包是类的归宿,类是包的子民。(library>package>class),引用例化也是按这个顺序。
  • 一个完整模块的验证环境组件类,应该由一个对应的模块包来封装。
  • 使用' include的关键词完成类在包中的封装,要注意编译的前后顺序来放置各个‘include的类文件。
  • 编译一个包的背后实际是将各个类文件“平铺”在包中,按照顺序完成包和各个类的有序编译。
  • 使用类的可以通过import'完成包中所有类或者某一个类的导入, 使得新的环境可以识别出该类,否则类会躺在包这个盒子里不被外部识别。
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值