西洲渡

一只还没迈入IC领域的小菜鸡

SV系统验证(6)——类的封装、继承和包

类的概述

类的三要素:类的封装、类的继承、类的多态

  • 类是一种可以包含数据方法(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
    26
    class 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
    17
    class 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
    7
    class Demo; 
    integer x;
    function new(integer x);
    int x=3;
    this.x=x; //若是没有使用this,则默认采用就近原则,寻找最近的成员变量(最好使用this)
    endfunction
    endclass

类的赋值和拷贝

  • 声明变量创建对象是两个过程,也可以一步完成。

    1
    2
    Packet p1;
    p1=new;
  • 如果将p1赋值给另外一个变量p2,那么依然只有一个对象,是指向这个对象的句柄有p1和p2.

  • 以下这种方式表示p1和p2代表两个不同的对象。在创建p2时,将从p1拷贝其成员变量例如integer、string和句柄等种拷贝方式称为浅拷贝(shallow copy)。

    1
    2
    3
    4
    5
    6
    Packet 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
    29
    class 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
    7
    class 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
    35
    class 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
    4
    packet p=new(3); 
    linkedpacked lp=new(1);
    packet tmp;
    tmp=lp; //子类句柄可以赋值给父类句柄,但句柄访问受限

    image.png

写代码的好习惯:
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
    38
    package 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
    4
    pkg_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
    6
    import 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)的联系和差别有哪些?

  • 为什么类的静态方法不能访问类的非静态(动态)成员变量?
  • 如果有同名的模块,那么在编译过程中应该怎么解决“同名”问题?