sv标准研读第八章-class

书接上回:

sv标准研读第一章-综述

sv标准研读第二章-标准引用

sv标准研读第三章-设计和验证的building block

sv标准研读第四章-时间调度机制

sv标准研读第五章-词法

sv标准研读第六章-数据类型

sv标准研读第七章-聚合数据类型

8 class

8.1综述

  1. 类的定义
  2. Virtual类和方法
  3. 多态
  4. 参数化类
  5. 接口类
  6. 内存管理

8.2概述

类是一种包含数据和对这些数据进行操作的子程序(函数和任务)的类型。一个类的数据被称为类属性,它的子程序被称为方法;他们都是这个类的成员。类属性和方法合在一起定义了某种对象的内容和功能。

例如,一个包可能是一个对象。它可能有一个命令字段、一个地址、一个序列号、一个时间戳和一个数据包负载。此外,还可以对数据包执行各种操作:初始化数据包、设置命令、读取数据包的状态或检查序列号。每个数据包都是不同的,但作为一个类,数据包具有某些可以在定义中捕获的内在属性。

举例:

面向对象的类扩展允许动态地创建和销毁对象。类实例或对象可以通过对象句柄传递,这提供了安全指针功能。可以将对象声明为具有input、output、inout或ref方向的参数。在每种情况下,复制的参数都是对象句柄,而不是对象的内容。

8.3 语法

8.4对象(class实例)

类定义了数据类型。对象是该类的实例。要使用对象,首先声明该类类型的变量(该变量包含对象句柄),然后创建该类的对象(使用new函数)并将其赋值给该变量。

变量p被称为持有类Packet对象的对象句柄。

未初始化的对象句柄默认设置为特殊值null。可以通过比较其句柄与null来检测未初始化的对象。

举例:

通过空对象句柄访问非静态成员(见8.9)或虚方法(见8.20)是非法的。通过null对象进行非法访问的结果是不确定的,并且实现可能会发出错误。

SystemVerilog对象是使用对象句柄引用的。C指针和SystemVerilog对象句柄之间有一些区别(参见表8-1)。C指针在指针的使用上给了程序员很大的自由。管理SystemVerilog对象句柄使用的规则要严格得多。例如,C指针可以递增,但是SystemVerilog对象句柄不能递增。除了对象句柄,6.14还引入了用于DPI的chandler数据类型(参见第35节)。

只有以下操作符在对象句柄上有效:

—与另一个类对象相等(==),不相等(!=)或与null。被比较的对象之一必须与另一个对象的赋值兼容。

—与其他类对象的大小写相等(===)、大小写不相等(!==)或与null(与==和!=的语义相同)。

—条件操作符(参见11.4.11)。

—类数据类型与目标类对象的赋值兼容的类对象的赋值。

—赋值为null。

8.5 对象属性和对象参数数据

类属性的数据类型没有限制。对象的类属性可以通过用实例名限定类属性名来使用。使用前面的例子(参见8.2),Packet对象p的属性可以这样使用:

除了可以使用类作用域解析操作符访问类枚举名之外,还可以通过使用实例名限定类枚举名来访问类枚举名。

还可以通过使用实例名称限定类值参数或局部值参数名称来访问对象的参数数据值。这样的表达式不是一个常量表达式。不允许使用类句柄访问数据类型。例如:

8.6 对象方法

可以使用与访问类属性相同的语法访问对象的方法:

上述对status的赋值不能写成如下方式:

面向对象编程的重点是对象,在本例中是packet,而不是函数调用。而且,对象是自包含的,有自己的方法来操作自己的属性。因此,该对象不必作为参数传递给current_status()。类的属性可以自由地、广泛地为类的方法所使用,但是每个方法只能访问与其对象(即其实例)相关联的属性。

作为类类型的一部分声明的方法的生命周期应该是automatic的。声明具有静态生命周期的类方法是非法的。

8.7 构造器

SystemVerilog不需要c++那样复杂的内存分配和释放。对象的构造是直截了当的;和Java一样,垃圾收集是隐式的和自动的。不可能有内存泄漏或其他微妙的行为,而这些通常是c++程序员的祸根。

SystemVerilog提供了一种在创建对象时初始化实例的机制。例如,当创建一个对象时:

将会执行类的new函数:

如前所述,new现在用于两种具有不同语义的非常不同的上下文中。变量声明创建了一个类Packet的对象。在创建此实例的过程中,将调用new函数,在该函数中可以完成所需的任何专门初始化。new函数也称为类构造函数。

new操作被定义为一个没有返回类型的函数,并且像任何其他函数一样,它应该是非阻塞的。尽管new没有指定返回类型,但赋值操作的左侧决定了返回类型。

如果一个类没有提供显式的用户定义的new方法,一个隐式的new方法将自动提供派生类的new方法应首先调用其基类构造函数super.new(),如8.15中所述。在基类构造函数调用(如果有的话)完成后,类中定义的每个属性都应初始化为其显式默认值,如果没有提供默认值,则应初始化为未初始化的值。初始化属性后,将计算用户定义构造函数中的剩余代码。默认构造函数在属性初始化之后没有额外的作用。属性在初始化之前的值应该是未定义的。

举例:

D类型对象的构造完成后,属性如下:

- c1的值为1

- c2的值为2,因为构造函数赋值发生在属性初始化之后

- c3的值为未定义值,因为D的构造函数调用传递了d3的值,该值在super.new(d3)调用时未定义

- d1的值为4

- d2的值为2,因为super。当d2初始化时,new的调用完成

- d3的值为6

也可以向构造函数传递参数,这允许在运行时自定义对象:

参数的约定与任何其他过程子例程调用的约定相同,例如使用默认参数。

构造函数可以声明为local或protected的方法(见8.18)。构造函数不能声明为static(见8.10)或virtual方法(见8.20)。

8.8 类型化构造函数调用

在前面部分描述的new的使用要求要构造的对象的类型与赋值目标的类型匹配。构造函数调用的另一种形式是类型化构造函数调用,在new关键字之前立即添加class_scope,独立于赋值目标指定构造对象的类型。指定的类型应与目标赋值匹配。

下面的示例说明了类型化构造函数调用。extends关键字在8.13中有描述。superclass类型的概念在8.15中描述。

这个类型构造函数调用的效果就像声明、构造了一个D类型的临时变量,然后将其复制到变量c,如下面的示例片段所示:

类型化构造函数调用必须创建并初始化指定类型的新对象。新对象的创建和初始化应与8.7中描述的普通构造函数完全相同。如果合适,可以将参数传递给类型化构造函数调用,就像普通构造函数一样。

如果要构造的对象类型是参数化的类,如8.25所述,则指定的类型可以具有参数指定。下面的示例延续了上一个示例,演示了参数化类的类型化构造函数调用,并演示了如何将参数传递给8.7中描述的构造函数。

8.9 static类属性

前面的例子只声明了实例类属性。类的每个实例(即,每个类型为Packet的对象)都有它的8个变量的副本。有时,所有实例只需要共享变量的一个版本。这些类属性是使用关键字static创建的。因此,例如,在下面的例子中,一个类的所有实例都需要访问一个公共文件描述符:

现在,fileID只会被创建并初始化一次。此后,每个Packet对象都可以以通常的方式访问文件描述符:

可以在不创建该类型对象的情况下使用静态类属性。

8.10 static方法

方法可以声明为静态的。静态方法受所有类作用域和访问规则的约束,但其行为类似于可以在类外部调用的常规子例程,即使没有类实例化。静态方法不能访问非静态成员(类属性或方法),但可以直接访问静态类属性或调用同一类的静态方法。访问非静态成员或访问静态方法体中的特殊this句柄是非法的,会导致编译器错误。静态方法不能是virtual的

静态方法不同于具有静态生命周期的task。前者指的是类中方法的生命周期,而后者指的是task中参数和变量的生命周期。

8.11 this

this关键字用于明确地引用当前实例的类属性、值参数、局部值参数或方法。this关键字表示一个预定义的对象句柄,该句柄引用用于调用在其中使用该句柄的子例程的对象。this关键字只能在非静态类方法、约束、内联约束方法或嵌入类中的covergroup中使用(参见19.4);否则,将发出错误。例如,下面的声明是编写初始化任务的常用方法:

x现在既是类的属性,也是new函数的参数。在new函数中,对x的不限定引用应通过查看最内层作用域来解决,在本例中,是子例程参数声明。要访问实例类属性,必须使用this关键字限定它以引用当前实例。

8.12 赋值,重命名和复制

声明类变量只创建对象已知的名称。因此,

创建一个变量p1,它可以保存类Packet对象的句柄,但是p1的初始值为空。该对象不存在,并且p1不包含实际句柄,直到创建了一个Packet类型的实例:

因此,如果声明另一个变量并将旧句柄p1赋值给新句柄,如

那么仍然只有一个对象,可以用名称p1或p2引用它。在这个例子中,new只执行了一次;因此,只创建了一个对象。

但是,如果将上例改写如下,则应复制p1:

最后一条语句让new执行第二次,从而创建一个新对象p2,其类属性从p1复制。这就是所谓的浅拷贝。所有的变量都被复制:整数、字符串、实例句柄等等。然而,对象不会被复制,只会复制它们的句柄;和前面一样,为同一个对象创建了两个名称。即使类声明包含实例化操作符new,也是如此。

对浅拷贝使用类型构造函数调用是非法的(见8.8)。

浅拷贝的执行方式如下:

1)分配要复制的class类型的对象。此分配不应调用对象的构造函数或执行任何变量声明初始化赋值。

2)所有类属性,包括用于随机化和覆盖的内部状态,都被复制到新对象。复制对象句柄;这包括covergroup对象的对象句柄(参见第19章)。对于嵌入的covergroup有一个例外(参见19.4)。嵌入式covergroup的对象句柄应在新对象中设置为。随机化的内部状态包括随机数生成器(RNG)状态、约束的constraint_mode状态、随机变量的rand_mode状态和randc变量的循环状态(见第18章)。

3)将新创建对象的句柄分配给左侧的变量。

浅拷贝不会创建新的coverage对象(covergroup实例)。因此,新对象的属性不会被覆盖。

举例:

在最后一条语句中,为base3分配了一个base2的浅拷贝。变量base3的类型是基类baseA的句柄。当调用浅拷贝时,该变量包含扩展类xtndA实例的句柄。浅复制创建引用对象的副本,从而产生扩展类xntdA的副本实例。然后将此实例的句柄分配给变量base3。

有几件事值得注意。首先,类属性和实例化对象可以在类声明中直接初始化。其次,浅拷贝不复制对象。第三,实例资格可以根据需要链接到对象内部或通过对象进行访问:

要进行完整(深度)复制,复制所有内容(包括嵌套对象),通常需要定制代码。例如:

其中copy(Packet p)是一个自定义方法,用于将指定为其参数的对象复制到其实例中。

8.13 继承和子类

前面的小节定义了一个名为Packet的类。可以扩展这个类,以便将数据包链接在一起形成一个列表。一种解决方案是创建一个名为LinkedPacket的新类,其中包含一个名为packet_c的Packet类型变量。

要引用Packet的类属性,需要引用变量packet_c。

因为LinkedPacket是Packet的一种特殊形式,所以更优雅的解决方案是扩展类,创建一个继承基类成员的新子类。因此,例如:

现在,Packet的所有方法和类属性都是LinkedPacket的一部分(就好像它们是在LinkedPacket中定义的一样),并且LinkedPacket有额外的类属性和方法。

基类的方法也可以被重写以更改其定义。

SystemVerilog提供的机制称为单继承,也就是说,每个类都派生自单个基类。

8.14 覆盖成员

子类对象也是其基类的合法代表对象。例如,每个LinkedPacket对象都是一个完全合法的Packet对象。

LinkedPacket对象的句柄可以分配给一个Packet变量:

在这种情况下,对p的引用访问Packet类的方法和类属性。因此,例如,如果LinkedPacket中的类属性和方法被重写,那么通过p引用的这些被重写的成员将获得Packet类中的原始成员。从p开始,LinkedPacket中的新成员和所有被覆盖的成员现在都被隐藏了。

要通过基类对象(在示例中为p)调用被覆盖的方法,需要将该方法声明为virtual(参见8.20)。

8.15 super

在派生类中使用super关键字来引用基类的成员、类值参数或局部值参数。当基类的成员、值参数或局部值参数被派生类覆盖时,有必要使用super来访问这些参数。使用super访问值形参或局部值形参的表达式不是常量表达式。

成员、值参数或局部值参数可以在上一级声明,也可以由上一级的类继承。没有办法达到更高(例如,不允许使用super.super.count)。

子类(或派生类)是当前类的扩展类,而超类(基类)是从当前类扩展而来的类,从原始基类开始。

一个super.new 调用应该是构造函数中执行的第一条语句。这是因为父类必须在当前类之前初始化,如果用户代码没有提供初始化,编译器将插入对super.new的自动调用

8.16 casting

将子类类型的表达式赋值给继承树中较高的类类型的变量(表达式类型的超类或祖先)始终是合法的。直接将超类类型的变量赋值给其子类类型的变量是非法的。但是,可以使用$cast将超类句柄赋值给子类类型的变量,前提是该超类句柄引用的对象与该子类变量的赋值兼容

为了检查赋值是否合法,使用动态强制转换函数$cast(见6.24.2)。

$cast的原型如下:

当$cast应用于类句柄时,它只在三种情况下成功:

1)源表达式和目的类型是赋值兼容的,即目的类型与源表达式相同或为源表达式的超类。

2)源表达式的类型强制转换与目的类型兼容,也就是说,要么

—源表达式的类型是目的类型的超类,要么

—源表达式的类型是接口类(见8.26),源是与目的类型兼容赋值的对象。这种类型的赋值需要$cast提供的运行时检查。

3)源表达式是文字常量null。

在所有其他情况下,$cast都将失败,特别是当源和目标类型不兼容时,即使源表达式的计算结果为空。当$cast成功时,它执行赋值操作。否则,错误处理如6.24.2所述。

8.17 链接构造函数

当子类被实例化时,将调用类方法new()。在对函数中定义的任何代码求值之前,new()采取的第一个动作是调用其超类的new()方法,依此类推直至继承层次结构。因此,以适当的顺序调用所有构造函数,从根基类开始,以当前类结束。类属性初始化在这个序列中发生,如8.7中所述。

如果父类的初始化方法需要参数,则有两种选择:始终提供相同的参数或使用super关键字。如果参数总是相同的,则可以在扩展类时指定它们:

更通用的方法是使用super关键字来调用超类构造函数:

要使用这种方法,super.new(…)应该是函数new中的第一个可执行语句。

如果在扩展类时指定了参数,则子类构造函数不得包含super.new()调用。当子类构造函数不包含super.new()调用时,编译器将自动插入对super.new()的调用(参见8.15)。

注1:将类构造函数声明为局部方法使该类不可扩展,因为在子类中引用super.new()是非法的。

注2 :当从构造函数new()调用虚方法时,构造函数调用8.20中描述的方法。然而,用户必须了解8.7中描述的类属性初始化顺序,因为方法引用的属性可能尚未初始化,这取决于调用该方法的构造函数链中的位置。

8.18 数据隐藏和封装

在SystemVerilog中,默认的类属性和方法是public的,任何有权访问对象名称的人都可以使用。通常,我们希望通过隐藏类属性和方法的名称来限制类外部对它们的访问。这使其他程序员不必依赖特定的实现,并且还可以防止对类内部的类属性的意外修改。当所有数据都被隐藏(即,只能通过公共方法访问)时,代码的测试和维护就变得容易得多。

类参数和类局部参数也是public的。

标识为local的成员仅对类中的方法可用。此外,这些局部成员在子类中不可见。当然,访问局部类属性或方法的非局部方法可以被继承,并作为子类的方法正常工作。

protected的类属性或方法具有局部成员的所有特征,除了它可以被继承;它对子类是可见的

在类中,可以引用同一类的局部方法或类属性,即使它位于同一类的不同实例中。例如:

对封装的严格解释可能会说other.i不应该在这个包中可见,因为它是一个从外部实例引用的局部类属性。然而,在同一个类中,这些引用是允许的。在这种情况下,就是this.i将被拿来和other.i比较,并返回逻辑比较的结果。

类成员可以被确定为local或protected;类属性可以进一步定义为const,方法可以定义为virtual。没有预定义的顺序来指定这些修饰符;但是,每个成员只能出现一次。将成员定义为local或protected的,或者复制任何其他修饰符都是错误的。

8.19 常量类属性

类属性可以像其他SystemVerilog变量一样通过const声明变为只读。然而,由于类对象是动态对象,类属性允许两种形式的只读变量:全局常量实例常量

全局常量类属性包括一个初始值作为其声明的一部分。它们与其他const变量类似,不能在声明之外的任何地方赋值。

实例常量在其声明中不包含初始值,只包含const限定符。这种类型的常量可以在运行时赋值,但只能在相应的类构造函数中赋值一次。

通常,全局常量也被声明为static,因为它们对于类的所有实例都是相同的。但是,不能将实例常量声明为static,因为这样做将禁止构造函数中的所有赋值。

8.20 virtual方法

类的方法可以用关键字virtual来标识。virtual方法是一种基本的多态构造。virtual方法应当覆盖其所有基类中的方法,而非virtual方法应当只覆盖该类及其子类中的方法。看待这个问题的一种方法是,每个类层次结构中只有一个virtual方法的实现,而且总是最新派生类中的那个实现。

virtual方法为以后覆盖它们的方法提供了原型,也就是说,通常在方法声明的第一行找到的所有信息:封装标准、参数的类型和数量,以及返回类型(如果需要的话)。

子类中的virtual方法重写必须具有匹配的参数类型、相同的参数名称、相同的限定符和与原型相同的方向。virtual限定符在派生类方法声明中是可选的。virtual函数的返回类型应该是

——匹配类型(见6.22.1)

——或者超类中virtual函数返回类型的派生类类型。

没有必要有匹配的默认表达式,但是默认表达式的存在必须匹配。

举例:

这个示例说明了virtual的作用:基类中的方法如果带上了virtual且子类重写了该方法,那么如果将子类对象指针赋值给父类变量,那么父类在调用该方法时会被override,而不带virtual的方法不会被override。

举例2:

注意:非virtual方法可以被virtual方法override,但是一旦方法变成了virtual,它的子类里该方法必须为virtual方法。

8.21 抽象类和纯虚方法

可以创建一组类,这些类可以看作是从一个公共基类派生出来的。例如,BasePacket类型的公共基类,它列出了数据包的结构,但不完整,因此永远不会被构造。这被描述为一个抽象类。但是,可以从这个抽象基类派生出许多有用的子类,例如以太网包、令牌环包、GPS包和卫星包。这些包中的每一个看起来都非常相似,都需要相同的一组方法,但是它们在内部细节方面可能有很大的不同。

通过使用关键字virtual来标识基类,可以将其描述为抽象类:

抽象类的对象不能直接构造。它的构造函数只能通过源自扩展的非抽象对象的构造函数调用的链接间接调用。

抽象类中的virtual方法可以声明为原型而不提供实现。这被称为纯虚方法,应该用关键字pure表示,同时不提供方法体。扩展的子类可以通过使用具有方法体的虚方法覆盖纯虚方法来提供实现。

抽象类可以被扩展为进一步的抽象类,但是所有纯虚方法必须有被覆盖的实现,以便被非抽象类扩展。有了所有方法的实现,类就完成了,现在可以构造了。任何类都可以扩展自抽象类,并且可以提供额外的或覆盖的纯虚方法。

举例:

8.22 多态:动态方法查找

多态性允许使用超类类型的变量来保存子类对象,并直接从超类变量引用这些子类的方法。例如,假设Packet对象的基类BasePacket将其子类通常使用的所有公共方法定义为虚函数。这些方法包括发送、接收和打印。尽管BasePacket是抽象的,它仍然可以用来声明一个变量:

现在,可以创建各种数据包对象的实例并将其放入数组:

例如,如果数据类型是整数、位和字符串,则不能将所有这些类型存储到单个数组中,但使用多态性可以做到这一点。在本例中,由于方法被声明为虚方法,因此可以从超类变量访问适当的子类方法,尽管编译器在编译时并不知道将加载到其中的内容。

应该调用与TokenPacket类关联的发送方法。在运行时,系统将正确地从适当的类绑定方法。

这是工作中的多态性的典型示例,它提供的功能远比在非面向对象框架中找到的功能强大。

8.23 类范围解析操作符::

类范围解析操作符::用于指定在类范围内定义的标识符。它的形式如下:

作用域解析操作符::的左操作数应该是类类型名、包名(见26.2)、covergroup类型名、coverpoint名、cross名(见19.5、19.6)、typedef名或类型参数名。当使用类型名称时,该名称应在细化后解析为类或覆盖组类型。

因为类和其他作用域可以具有相同的标识符,所以类作用域解析操作符唯一地标识特定类的成员、参数或局部参数。除了消除类作用域标识符的歧义之外,::操作符还允许从类外部访问静态成员(类属性和方法)、类参数和类局部参数,以及从派生类内部访问父类的publicprotected元素。类形参或局部形参是类的公共元素。类作用域参数或局部参数是常量表达式。

在SystemVerilog中,类范围解析操作符适用于类的所有静态元素:静态类属性、静态方法、typedef、枚举、参数、局部参数、约束、结构、联合和嵌套类声明。类作用域解析表达式可以被读取(在表达式中)、写入(在赋值或子例程调用中)或触发(在事件表达式中)。类作用域还可以用作类型或方法调用的前缀。

和module一样,类也是作用域,可以嵌套。嵌套允许隐藏本地名称和资源的本地分配。当需要一个新类型作为类实现的一部分时,这通常是可取的。在类中声明类型有助于防止名称冲突和外部作用域中仅由该类使用的符号造成的混乱。嵌套在类范围内的类型声明是公共的,可以在类外部访问。

举例:

类作用域解析操作符支持以下功能:

—从类层次结构外部访问静态公共成员(方法和类属性)。

—从派生类中访问父类的公共或受保护类成员。

—从类层次结构外部或派生类中访问类内部声明的约束、类型声明和枚举命名常量。

—从类层次结构外部或派生类中访问类内部声明的参数和局部参数。

嵌套类应具有与包含类中的方法相同的访问权限。它们对包含类的本地和受保护的方法和属性具有完全的访问权限。嵌套类具有词法作用域,对包含类的静态属性和方法、参数和局部参数的非限定访问。它们不能隐式地访问非静态属性和方法,除非通过传递给它的句柄或它可以访问的句柄。外部类没有隐式this句柄。例如:

类作用域解析操作符在与前缀(参数化类的名称)一起使用时具有特殊规则;具体请参见8.25.1。

8.24 块外声明

将方法定义移出类声明体是很方便的。这分两步完成。首先,在类主体中声明方法原型,即,它是函数还是任务、任何限定符(本地的、受保护的或虚拟的)以及完整的参数规范加上extern限定符。extern限定符指示方法体(它的实现)在声明之外。其次,在类声明之外,声明完整的方法(例如,原型但不带限定符),并且,为了将方法绑定回它的类,用类名和一对冒号限定方法名,如下所示:

块外方法声明应该与原型声明完全匹配,但有以下例外:

—方法名前面是类名和类作用域解析操作符。

—函数返回类型也可能需要在out- block声明中添加类作用域,如下所述。

—原型中指定的默认参数值可以在out- block声明中省略。如果在out- block声明中指定了默认参数值,那么在原型中应该有一个语法相同的默认参数值。

块外声明应该在与类声明相同的作用域中声明,并且应该在类声明之后声明。如果为一个特定的外部方法提供了多个out- block声明,则会产生错误。

在某些情况下需要类范围解析操作符,以便用块外声明命名方法的返回类型。当块外声明的返回类型在类中定义时,应使用类作用域解析操作符来指示内部返回类型。

举例:

块外方法声明应该能够访问在其中声明相应原型的类的所有声明。按照正常的解析规则,只有在原型之前声明类类型时,原型才能访问类类型。如果原型中引用的标识符解析到的声明与out- block方法声明头中对应标识符解析到的声明不同,则会产生错误。

在本例中,方法f的原型中的标识符T解析为外部作用域中T的声明。在方法f的out- block声明中,由于out- block声明对类C中的所有类型都是可见的,因此如果t解析为C:: t,则会报告错误。因为out- block声明中t的解析与原型中的解析不匹配,因此会报告错误。

8.25 参数化类

定义一个泛型类通常很有用,它的对象可以实例化为具有不同的数组大小或数据类型。这避免了为每种大小或类型编写相似的代码,并允许对根本不同且不可互换的对象(如c++中的模板化类)使用单一规范。

SystemVerilog参数机制用于参数化类:

这个类的实例可以像模块或接口一样被实例化:

当使用类型作为参数时,此特性特别有用:

前面的类定义了一个泛型堆栈类,它可以用任意类型实例化:

任何类型都可以作为参数提供,包括用户定义的类型,如类或结构。

泛型类和实际参数值的组合称为专门化。类的每个特化都有一组单独的静态成员变量(这与c++模板化类一致)。要在几个类专门化之间共享静态成员变量,必须将它们放在非参数化的基类中。

上例中的变量count只能通过对应的disp_count方法访问。类vector的每个特化都有自己唯一的count副本。

专门化是特定泛型类与一组唯一参数的组合。除非所有参数都相同,否则两组参数必须唯一,定义规则如下:

a)一个参数为类型参数,两个类型为匹配类型。

b)形参是一个值形参,其类型和值都是相同的。

一个特定泛型类的所有匹配的特化应该表示相同的类型。泛型类的匹配专门化集由类声明的上下文定义。因为包中的泛型类在整个系统中都是可见的,所以包泛型类的所有匹配的专门化都是相同的类型。在其他上下文中,例如模块或程序,包含泛型类声明的作用域的每个实例创建一个唯一的泛型类,从而定义一组新的匹配专门化。

泛型类不是类型;只有具体的专门化才表示类型。在前面的例子中,类vector只有在被形参应用时才成为具体类型,例如:

为了避免在声明中重复特化或创建该类型的形参,应该使用typedef:

参数化类可以扩展另一个参数化类。例如:

类D1使用基类的默认类型(bit)参数扩展基类C。类D2使用一个整数形参扩展基类C。类D3使用参数化类型(P)扩展基类C,扩展类使用参数化类型(P)。类D4扩展了由类型参数P指定的基类。

当类型参数或typedef名称用作基类时,如上面的类D4,该名称应在细化后解析为类类型。

参数化类的默认专门化是带有空参数覆盖列表的参数化类的专门化。对于参数化的类C,默认的专门化是c#()。除了作为作用域解析操作符的前缀外,使用参数化类的未修饰名称应表示该类的默认专门化。并非所有的参数化类都有默认的专门化,因为类不提供默认参数是合法的。在这种情况下,所有的专门化都应该至少覆盖那些没有默认值的参数。

8.25.1 用于参数化类的类范围解析操作符

类作用域解析操作符的前缀为参数化类的未修饰名(见8.25),应限制在命名的参数化类的作用域内及其out-of - block声明(见8.24)内使用。在这种情况下,参数化类的未修饰名称不表示默认专门化,而是用于明确地引用参数化类的成员。当引用默认专门化作为类作用域解析操作符的前缀时,应该使用#()的显式默认专门化形式。

在参数化类或其块外声明的上下文中,可以使用类作用域解析操作符访问任何类参数。在这种情况下,应当采用明确的专业化形式;参数化类的未修饰名称应该是非法的。显式专门化形式可以表示特定的参数或默认的专门化形式。类作用域解析操作符可以访问值形参和类型形参,这些形参要么是类的局部形参,要么是类的参数。

在参数化类方法块外声明的上下文中,类作用域解析操作符的使用应该是对名称的引用,就好像它是在参数化类内部进行的一样;没有暗含专门化。

​​​​​8.26 接口类

可以创建一组类,这些类可以被视为具有一组共同的行为。这样一组通用的行为可以使用接口类来创建。接口类使得相关类不需要共享一个公共抽象超类,也不需要该超类包含所有子类所需的所有方法定义。非接口类可以声明为实现一个或多个接口类。这就要求非接口类提供一组方法的实现,这些方法必须满足虚方法覆盖的要求(见8.20)。

接口类只能包含纯虚方法(见8.21)、类型声明(见6.18)和参数声明(见6.20、8.25)约束块、覆盖组和嵌套类(见8.23)不允许出现在接口类中。一个接口类不能嵌套在另一个类中。接口类可以通过extends关键字继承一个或多个接口类,这意味着它继承它所扩展的接口类的所有成员类型、纯虚方法和参数,除了它可能隐藏的任何成员类型和参数。在多重继承的情况下,可能会出现必须解决的名称冲突(见8.26.6)。

类可以通过implements关键字实现一个或多个接口类。任何成员类型或参数都不能通过implements关键字继承。子类隐式地实现其父类实现的所有接口类。在下面的例子中,类C隐式地实现接口类A,并具有所有的需求和功能,就好像它显式地实现接口类A一样:

接口类中的每个纯虚方法都应该有一个虚方法实现,以便由非抽象类实现。当一个接口类由一个类实现时,接口类方法所需的实现可以由继承的虚方法实现提供。虚类应当定义或继承一个纯虚方法原型或虚对象,为每个实现的接口类中的每个纯虚方法原型定义或继承一个纯虚方法原型。除非继承了virtual方法,否则必须使用关键字virtual。

声明类型为接口类类型的变量的值可以是对实现指定接口类的类的任何实例的引用(参见8.22)。一个类仅仅为接口类的所有纯虚方法提供实现是不够的;类或其父类之一应通过implements关键字声明为实现接口类,否则类不实现接口类。

下面是接口类的一个简单示例。

该示例有两个接口类PutImp和GetImp,它们包含原型纯虚拟方法put和get。Fifo和Stack类使用关键字implements来实现PutImp和GetImp接口类,它们提供put和get的实现。因此,这些类共享共同的行为,但不共享共同的实现。

8.26.1 接口类语法

8.26.2 扩展与实现

从概念上讲,扩展是一种添加或修改超类行为的机制,而实现是为接口类中的纯虚方法提供实现的要求。当扩展类时,类的所有成员都继承到子类中。实现接口类时,不会继承任何东西。

接口类可以扩展,但不能实现一个或多个接口类,这意味着接口子类继承来自多个接口类的成员,并且可以添加额外的成员类型、纯虚方法原型和参数。类或虚类可以实现,但不能扩展一个或多个接口类。因为虚类是抽象的,所以它们不需要从它们实现的类中完全定义方法(见8.26.7)。以下内容突出了这些差异:——一个接口类

•可能扩展零个或多个接口类

•可能不会实现一个接口类

•不得扩展一个类或虚拟类

•可能不会实现

一个类或虚拟类或虚拟类

•不得扩展接口类

•可能实现零个或多个接口类

•扩展最多一个类或虚拟类

•可能不会实现一个类或虚拟类

•可能同时扩展一个类,实现接口的类

在下面的例子中,一个类既扩展了基类,又实现了两个接口类:

在这个例子中,PipeQueue属性和deleteQ方法在Fifo类中被继承。此外,Fifo类还实现了PutImp和GetImp接口类,因此它将分别为put和get方法提供实现。

下面的示例演示了可以在类定义中参数化多个类型,以及在实现的类PutImp和GetImp中使用的解析类型。

继承的虚方法可以为已实现的接口类的方法提供实现。下面是一个例子:

ExtClass通过提供funcExt的实现和从BaseClass继承funcBase的实现来满足实现IntfClass的要求。

继承的非虚方法不提供已实现接口类的方法的实现。

BaseClass中的非虚函数f()不满足实现IntfClass的要求。在ExtClass中实现f() i同时隐藏了BaseClass的f() o并满足了实现IntfClass的要求。

8.26.3 类型访问

接口类中的参数和类型可以通过扩展接口类来继承,但不能通过实现接口类来继承。接口类中的所有形参和类型都是静态的,可以通过类作用域解析操作符::来访问(参见8.23)。通过接口类句柄访问参数与通过类句柄访问参数具有相同的限制(参见8.5)。

8.26.4 类型使用限制

类不能实现类型参数,接口类也不能扩展类型参数,即使类型参数解析为接口类。以下例子说明了这一限制,并且是非法的:

类不能为接口类实现前向类型定义。接口类不能从接口类的前向类型定义扩展。接口类必须在实现或扩展之前声明。

8.26.5 类型转换和对象引用赋值

将对象句柄分配给对象实现的接口类类型的变量应该是合法的。

如果实际的类句柄可以有效地赋值给目标,那么在接口类变量之间动态强制转换应该是合法的。

在前面的例子中,put_ref是fifo# (int)的一个实例,它实现了getimp# (int)。如果实际对象实现了接口类类型,那么从对象句柄强制转换为接口类类型句柄也应该是合法的。

像抽象类一样,不能构造接口类类型的对象。

从为null的源接口类句柄进行强制转换的方式与从为null的源类句柄进行强制转换的方式相同(参见8.16)。

8.26.6 命名冲突和解决办法

当一个类实现多个接口类时,或者当一个接口类扩展多个接口类时,标识符将从不同的名称空间合并到单个名称空间中。发生这种情况时,可能会在单个名称空间中同时看到来自多个名称空间的相同标识符名称,从而产生必须解决的名称冲突。

8.26.6.1 方法名冲突及解决办法

一个接口类可能继承多个方法,或者一个类可能需要通过实现来提供多个方法的实现,其中这些方法具有相同的名称。这是一个方法名称冲突。方法名称冲突应通过单个方法原型或实现来解决,该方法原型或实现同时为任何实现的接口类的同名所有纯虚方法提供实现。该方法原型或实现也必须是任何同名继承方法的有效虚方法覆盖(见8.20)。

类ClassExt提供了一个funcBase的实现,它覆盖了来自ClassBase的纯虚拟方法原型,并同时提供了来自IntfBase1和IntfBase2的funcBase的实现。

在某些情况下,方法名称冲突无法解决。

在这种情况下,funcBase在IntfBaseA和IntfBaseB中原型化,但是返回类型不同,分别是bit和string。虽然funcBase的实现是对IntfBaseA::funcBase的有效覆盖,但它不是同时对IntfBaseB:: funcBase的原型的有效覆盖,因此会发生错误。

8.26.6.2 参数和类型声明继承冲突及解决方法

接口类可以从多个接口类继承参数和类型声明。如果从不同的接口类继承了相同的名称,则会发生名称冲突。子类应该提供参数和/或类型声明,覆盖所有这样的名称冲突。

在上面的示例中,参数T继承自PutImp和GetImp。尽管PutImp::T与GetImp::T匹配,但仍会发生冲突,并且PutGetIntf从未使用过冲突。PutGetIntf用类型定义覆盖T以解决冲突。

8.26.6.3 关系

如果接口类由相同的类实现或由相同的接口类以多种方式继承,则会出现菱形关系。在菱形关系的情况下,为了避免名称冲突,将只合并来自任何单个接口类的符号的一个副本。例如:

在上面的例子中,类IntfExt3继承了来自IntfExt1和IntfExt2的参数SIZE。由于这些参数来自同一个接口类IntfBase,因此只有一个SIZE的副本可以继承到IntfExt3中,因此不应将其视为冲突。

参数化接口类的每个唯一参数化都是接口类专门化。每个接口类专门化都被认为是唯一的接口类类型。因此,如果同一参数化接口类的不同专门化由同一接口类继承或由同一类实现,则不存在菱形关系。因此,可能会发生8.26.6.1中描述的方法名称冲突以及8.26.6.2中描述的参数和类型声明名称冲突。例如:

在前面的示例中,接口类IntfBase有两种不同的参数化。IntfBase的每一个参数化都是一个专门化;因此不存在菱形关系,并且必须解决参数T和方法funcBase之间的冲突。

8.26.7 部分实现

可以创建未完全定义的类,并通过使用虚类来利用接口类(见8.21)。因为虚拟类不必完全定义它们的实现,所以它们可以自由地部分定义它们的方法。下面是一个部分实现的虚拟类的例子。

在不满足接口类原型要求的情况下,使用接口类部分定义虚类是违法的。换句话说,当一个接口类被虚类实现时,虚类必须为每个接口类的方法原型做以下其中一件事:

—提供一个方法实现

—用纯限定符重新声明方法原型在前面的例子中,ClassA完全定义了funcA,但重新声明了原型funcB。

8.26.8 方法默认参数值

接口类中的方法声明可以有默认的参数值。默认表达式应该是一个常量表达式,并在包含子例程声明的作用域中求值。对于实现该方法的所有类,常量表达式的值应该是相同的。更多信息请参见13.5.3。

8.26.9 约束块,covergroup和随机化

约束块和覆盖组不能在接口类中声明。

随机方法调用对于接口类句柄应该是合法的。虽然内联约束也是合法的,但接口类不能包含任何数据,这意味着内联约束只能表示与状态变量相关的条件,因此效用非常有限。使用rand_mode和constraint_mode是不合法的,因为名称解析规则是不允许的,因为接口类不允许包含数据成员。

接口类包含两个内置的空虚拟方法pre_randomize()和post_randomize(),它们在随机化之前和之后自动调用。这些方法可以被重写。作为一种特殊情况,pre_randomize()和post_randomize()不会导致方法名冲突。

8.27 typedef class

有时需要在声明类本身之前声明类变量;例如,如果两个类都需要彼此的句柄。当编译器在处理第一个类的声明过程中遇到对第二个类的引用时,该引用是未定义的,并且编译器将其标记为错误。

这可以通过使用typedef为第二个类提供前向声明来解决:

在本例中,C2被声明为class类型,这一事实在后面的源代码中得到了加强。类构造总是创建类型,而不需要为此目的声明typedef(如typedef class…)。在前面的例子中,语句中的class关键字typedef class C2;不是必需的,仅用于文档目的。语句类型为C2;是相等的,应该以同样的方式工作。与6.18中描述的其他前向类型一样,前向类声明的实际类定义应在相同的局部作用域或生成块中解析。类的前向类型定义可以引用带有参数端口列表的类。

8.28 类和结构体

从表面上看,似乎类和结构提供了相同的功能,并且只需要其中一个。然而,事实并非如此;class与struct在以下三个基本方面有所不同:

  1. SystemVerilog struct是严格的静态对象;它们要么在静态内存位置(全局或模块作用域)中创建,要么在自动任务的堆栈中创建。相反,SystemVerilog对象(即类实例)完全是动态的;它们的声明并不创建对象。创建对象是通过调用new来完成的。
  2. SystemVerilog对象使用句柄实现,从而提供类似c语言的指针功能。但是SystemVerilog不允许将句柄转换到其他对象上,而不允许将句柄转换到其他对象上。因此,SystemVerilog句柄没有与C指针相关的风险。
  3. SystemVerilog对象构成了提供真正多态性的面向对象数据抽象的基础。类继承、抽象类和动态强制转换是功能强大的机制,它们远远超出了结构提供的单纯封装机制。

8.29 内存管理

对象、字符串、动态数组和关联数组的内存是动态分配的。创建对象时,SystemVerilog分配更多内存。当一个对象不再需要时,SystemVerilog自动回收内存,使其可重用。自动内存管理系统是SystemVerilog的一个组成部分。如果没有自动内存管理,SystemVerilog的多线程、可重入环境会给用户带来很多问题。手动内存管理系统,例如C语言的malloc和free提供的系统,是不够的。考虑下面的例子:

在本例中,主进程(分离两个任务的进程)不知道何时可以使用对象obj完成这两个进程。同样,task1和task2都不知道其他两个进程何时不再使用对象obj。从这个简单的示例中可以明显看出,没有一个进程拥有足够的信息来确定何时释放对象是安全的。用户仅有的两个选项如下:—小心行事,永远不要回收对象,或者—添加某种形式的引用计数,可用于确定何时可以安全回收对象。采用第一个选项可能会导致系统很快耗尽内存。第二种选择给用户带来了很大的负担,他们除了管理测试平台之外,还必须使用不太理想的模式来管理内存。为了避免这些缺点,SystemVerilog自动管理所有动态内存。用户不需要担心悬空引用、过早回收或内存泄漏。系统将自动回收不再使用的物品。在前面的示例中,用户所做的就是在不再需要handle obj时,将所有引用它的变量赋值为null。当一个对象在任何活动作用域中存在对该对象的未完成引用,或者对该对象的非静态成员的未完成的非阻塞赋值时,该对象不得被回收。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值