文章目录
- [SystemVerilog OOP 核心进阶:多态、虚方法、深拷贝实战 | UVM底层原理](#SystemVerilog OOP 核心进阶:多态、虚方法、深拷贝实战 | UVM底层原理)
-
- [📌 本篇导读](#📌 本篇导读)
- [1 回顾与衔接](#1 回顾与衔接)
- [2 虚方法与多态](#2 虚方法与多态)
-
- [2.1 为什么需要虚方法](#2.1 为什么需要虚方法)
- [2.2 重写的黄金规则](#2.2 重写的黄金规则)
- [2.3 终极陷阱:虚方法的默认参数是编译时绑定的](#2.3 终极陷阱:虚方法的默认参数是编译时绑定的)
- [2.4 多态数组(验证中常用)](#2.4 多态数组(验证中常用))
- [3 纯虚方法与抽象类](#3 纯虚方法与抽象类)
- [4 `final` 关键字:锁定核心逻辑](#4
final关键字:锁定核心逻辑) - [5 `cast()\`:安全的类型转换](#5 `cast()`:安全的类型转换)
-
- 向上转型(安全,自动转换)
- [向下转型(不安全,必须用 `cast()\`)](#向下转型(不安全,必须用 `cast()`))
- [6 深拷贝的实现](#6 深拷贝的实现)
-
- [6.1 浅拷贝的问题](#6.1 浅拷贝的问题)
- [6.2 手动实现深拷贝](#6.2 手动实现深拷贝)
- [6.3 深拷贝覆盖范围](#6.3 深拷贝覆盖范围)
- [6.4 垃圾回收与 UVM 参考](#6.4 垃圾回收与 UVM 参考)
- [7 参数化类(模板类)](#7 参数化类(模板类))
- [8 运算符重载](#8 运算符重载)
- [9 常量成员与 const 函数](#9 常量成员与 const 函数)
- [10 类内部的枚举与 typedef](#10 类内部的枚举与 typedef)
- [11 完整验证示例](#11 完整验证示例)
- [12 核心概念速查表](#12 核心概念速查表)
- [13 常见错误与编码建议](#13 常见错误与编码建议)
SystemVerilog OOP 核心进阶:多态、虚方法、深拷贝实战 | UVM底层原理
本文是 SystemVerilog OOP 教程的下篇 ,假设你已经掌握了类、对象、继承、静态成员等基础知识。
进阶篇将深入讲解多态、虚方法、类型转换、参数化类、运算符重载、深拷贝等 UVM 底层核心机制。
学完本文,你将理解 UVM 工厂、phase 机制、sequence 等核心组件的 OOP 原理。
📌 本篇导读
| 知识点 | 难度 | 实战价值 | 典型场景 |
|---|---|---|---|
| 虚方法与多态 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | 统一接口、动态调用 |
| 纯虚方法与抽象类 | ⭐⭐⭐ | ⭐⭐⭐⭐ | 定义接口规范 |
$cast() 类型转换 |
⭐⭐⭐ | ⭐⭐⭐⭐⭐ | UVM 中每天使用 |
final 关键字 |
⭐⭐ | ⭐⭐⭐⭐ | 锁定核心逻辑 |
| 深拷贝 | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | 事务复制、避免共享 |
参数化类 #(type T) |
⭐⭐⭐ | ⭐⭐⭐⭐ | 通用数据结构 |
运算符重载 == |
⭐⭐ | ⭐⭐⭐⭐ | 比较事务内容 |
| 常量与 const 函数 | ⭐⭐ | ⭐⭐⭐ | 配置参数、只读接口 |
| 类内枚举与 typedef | ⭐⭐ | ⭐⭐⭐⭐ | 增强封装性 |
💡 常见误区 :虚方法重写变成重载;虚方法中使用默认参数;$cast 前不检查类型;深拷贝时忘记递归复制子对象;混淆句柄比较与内容比较。
🧭 本文所有内容 【验证】 专用,不可综合。
1 回顾与衔接
本文假设你已经掌握以下基础知识(上篇):
- 类的定义与实例化、构造函数
new()、null句柄检查 - 属性与方法、外部方法
extern - 对象句柄与赋值、浅拷贝概念
- 静态属性与方法
- 继承
extends与super.new() - 访问权限
protected/local
如果你对这些概念还不熟悉,建议先阅读上篇:SystemVerilog语法(10)-面向对象编程上篇
2 虚方法与多态
虚方法 :用 virtual 修饰的方法,支持动态绑定(运行时根据对象实际类型调用对应方法)。
2.1 为什么需要虚方法
systemverilog
class Base;
function void print();
$display("Base");
endfunction
endclass
class Derived extends Base;
function void print();
$display("Derived");
endfunction
endclass
initial begin
Base b;
Derived d = new();
b = d; // 父类句柄指向子类对象
b.print(); // 输出 "Base" ------ 不是我们想要的!
end
因为 print 不是虚方法,编译器根据句柄类型(Base)而非对象实际类型(Derived)来决定调用哪个方法。
解决方案 :在基类方法前加 virtual。
systemverilog
class Base;
virtual function void print();
$display("Base");
endfunction
endclass
class Derived extends Base;
virtual function void print(); // 可省略 virtual,但建议保留
$display("Derived");
endfunction
endclass
// 此时 b.print() 输出 "Derived" ✅
📌 子类重写虚方法时,可以通过
super.方法名()调用父类的实现
systemverilog
class Derived extends Base;
virtual function void print();
super.print(); // 先调用父类的print方法
$display("Derived"); // 再添加子类的逻辑
endfunction
endclass
我们可以用一句口诀来记忆:
实方法,看句柄;虚方法,看对象。
💡 多态生效的三个必要条件(缺一不可):
- 存在继承关系
- 子类重写父类的虚方法
- 父类句柄指向子类对象
2.2 重写的黄金规则
⚠️ 子类重写虚方法时,参数类型、数量、顺序、方向以及返回值类型必须与父类完全一致 ,否则会变成方法重载而非重写,多态不会生效。
❌ 错误示例(参数类型不同,变成重载):
systemverilog
class Base;
virtual function void f(int a);
$display("Base.f(int)");
endfunction
endclass
class Derived extends Base;
virtual function void f(bit a); // 参数类型不同,变成重载
$display("Derived.f(bit)");
endfunction
endclass
initial begin
Base b = new Derived();
b.f(1); // 输出 "Base.f(int)",多态失效!
end
❌ 错误示例(参数方向不同,变成重载):
systemverilog
class Base;
virtual function void f(input int a);
$display("Base.f(input int)");
endfunction
endclass
class Derived extends Base;
virtual function void f(output int a); // 参数方向不同,变成重载
$display("Derived.f(output int)");
endfunction
endclass
initial begin
Base b = new Derived();
int x;
b.f(x); // 输出 "Base.f(input int)",多态失效!
end
2.3 终极陷阱:虚方法的默认参数是编译时绑定的
⚠️ 虚方法的默认参数是编译时绑定的,不会被动态重写!这是 SystemVerilog OOP 中最隐蔽的 bug。
systemverilog
class Base;
virtual function void f(int a = 1);
$display("Base.f(%0d)", a);
endfunction
endclass
class Derived extends Base;
virtual function void f(int a = 2); // 重写默认参数
$display("Derived.f(%0d)", a);
endfunction
endclass
initial begin
Base b = new Derived();
b.f(); // 输出 "Derived.f(1)" ------ 方法体是子类的,但默认参数是父类的!
end
⚠️ 这是所有主流面向对象语言的通用行为 (包括C++、Java、C#)。默认参数始终是编译时绑定的,基于声明的句柄类型而非运行时的对象类型。很多工程师误以为默认参数会被动态重写,这是最常见的认知错误。
结论:永远不要在虚方法中使用默认参数。如果需要默认值,使用重载方法代替。
2.4 多态数组(验证中常用)
systemverilog
// transaction_classes.sv
module polymorphism_demo;
class Transaction;
int id;
static int next_id = 0;
function new();
id = next_id++;
endfunction
virtual function void display();
$display("Transaction[%0d]", id);
endfunction
endclass
class WriteTransaction extends Transaction;
bit [31:0] addr, data;
function new(bit [31:0] a, bit [31:0] d);
super.new();
addr = a;
data = d;
endfunction
virtual function void display();
$display("WRITE[%0d]: addr=0x%h, data=0x%h", id, addr, data);
endfunction
endclass
class ReadTransaction extends Transaction;
bit [31:0] addr;
function new(bit [31:0] a);
super.new();
addr = a;
endfunction
virtual function void display();
$display("READ[%0d]: addr=0x%h", id, addr);
endfunction
endclass
initial begin
Transaction tr_q[$];
WriteTransaction wr = new(32'h1000, 32'hA5);
ReadTransaction rd = new(32'h2000);
tr_q.push_back(wr);
tr_q.push_back(rd);
// 打印头和尾
$display("\n========== Transaction Queue Dump ==========");
$display("Queue size: %0d", tr_q.size());
$display("--------------------------------------------");
foreach (tr_q[i])
tr_q[i].display(); // 多态调用
$display("============================================\n");
$finish;
end
endmodule

💡 多态的好处:我们可以用一个统一的父类句柄数组来存储所有不同类型的子类对象,然后通过一个统一的接口(display 方法)来操作它们,不需要为每种类型写单独的处理逻辑。
根据你的要求,在保留原文结构的基础上,修正了缺失 pure 关键字的关键错误,并优化了表述的准确性。以下是优化后的版本:
3 纯虚方法与抽象类
用 pure virtual 声明并省略方法体,表示子类必须实现。含有纯虚方法的类是抽象类,不能直接实例化。
标准语法 :纯虚方法直接以分号结尾,不需要 endfunction。
systemverilog
abstract class Shape; // 显式 abstract 关键字(推荐)
pure virtual function float area(); // 纯虚方法,无方法体
endclass
class Circle extends Shape;
float radius;
function float area();
return 3.14159 * radius * radius;
endfunction
endclass
抽象类完整规则:
- 只要包含至少一个纯虚方法,就是抽象类
- 抽象类不能直接实例化
- 子类必须实现所有继承的纯虚方法,否则子类也会成为抽象类
- 抽象类可以包含普通方法、成员变量和构造函数
- 纯虚方法不能声明为
extern,必须在类内部直接声明 - 纯虚方法不能有默认参数(IEEE 标准明确禁止)
- 抽象类的构造函数可以声明为
protected,但不能声明为local,否则子类无法调用super.new()
注意 :SystemVerilog 中纯虚方法必须使用
pure virtual关键字组合,单独virtual并省略方法体不会产生纯虚方法。
4 final 关键字:锁定核心逻辑
final 关键字用于禁止进一步的继承或重写,是大型验证项目的安全保障。
| 用法 | 含义 | UVM 典型场景 |
|---|---|---|
final class A; |
类不能被继承 | 封装不可修改的工具类 |
virtual final function void f(); |
方法不能被子类重写 | UVM phase 机制 |
const int WIDTH = 32; |
实例级常量,不能修改 | 定义只读配置参数 |
static const int WIDTH = 32; |
类级常量,所有对象共享 | 全局配置参数(推荐) |
示例:
systemverilog
class BaseDriver;
// 锁定驱动主流程,防止子类破坏
virtual final task run();
pre_drive();
drive_transaction();
post_drive();
endtask
// 留给子类重写的钩子方法
virtual protected task pre_drive(); endtask
virtual protected task drive_transaction(); endtask
virtual protected task post_drive(); endtask
endclass
5 $cast():安全的类型转换
多态中经常需要在基类句柄和子类句柄之间转换,SystemVerilog 提供 $cast() 函数进行安全的运行时类型检查。
向上转型(安全,自动转换)
systemverilog
Transaction tr;
WriteTransaction wr = new();
tr = wr; // 向上转型,安全,自动转换
向下转型(不安全,必须用 $cast())
systemverilog
Transaction tr = new WriteTransaction();
WriteTransaction wr;
// ❌ 错误:直接赋值编译错误
wr = tr;
// ✅ 正确:使用 $cast() 进行运行时类型检查
if ($cast(wr, tr)) begin
$display("转换成功");
wr.data = 32'hA5;
end else begin
$error("转换失败,tr 不是 WriteTransaction 类型");
end
⚠️ 标准强制行为 :
$cast失败时,目标句柄保持其原有值不变 ,不会被自动置为null。因此,在使用$cast前,建议先将目标句柄置为null,或在失败后明确处理原有值,避免残留旧数据导致逻辑错误。
systemverilog
// 推荐写法
WriteTransaction wr = null;
if ($cast(wr, tr)) begin
// 转换成功,使用 wr
end else begin
$error("类型不匹配");
// 此时 wr 仍为 null,安全
end
UVM 典型场景:
systemverilog
uvm_sequence_item item;
my_transaction tr;
$cast(tr, item); // UVM 中最常用的操作之一
6 深拷贝的实现
⚠️
new()执行的是浅拷贝。对于包含动态数组、句柄成员的对象,必须手动实现深拷贝。
6.1 浅拷贝的问题
systemverilog
Packet p1 = new();
p1.payload = new[4];
p1.payload[0] = 8'hAA;
Packet p2 = new(p1); // 浅拷贝
p2.payload[0] = 8'hFF;
$display("%h", p1.payload[0]); // 输出 FF ------ 被意外修改了!
原因 :动态数组变量本身是句柄,浅拷贝只复制句柄,导致两个对象的 payload 指向同一块堆内存。
6.2 手动实现深拷贝
深拷贝的目标:递归复制所有成员,创建完全独立的副本。
systemverilog
class Header;
int length;
function Header deep_copy();
Header c = new();
c.length = this.length;
return c;
endfunction
endclass
class Packet;
Header hdr; // 对象句柄
bit [7:0] payload[]; // 动态数组
function Packet deep_copy();
Packet c = new();
// 递归复制子对象(判空)
c.hdr = (this.hdr != null) ? this.hdr.deep_copy() : null;
// 重新分配动态数组并复制元素
c.payload = new[this.payload.size()];
c.payload = this.payload;
return c;
endfunction
endclass
6.3 深拷贝覆盖范围
| 数据类型 | 处理方式 |
|---|---|
| 基本类型(int, bit 等) | 值复制(自动) |
| 静态数组 | 自动复制所有元素 |
| 动态数组/队列 | 重新 new[size] + 元素复制 |
| 对象句柄 | 递归调用子对象的 deep_copy() |
| 邮箱/信号量 | 通常不复制(破坏通信语义) |
| 静态成员 | 不复制(属于类本身) |
6.4 垃圾回收与 UVM 参考
- 垃圾回收:SystemVerilog 自动回收无句柄指向的对象,无需手动释放内存。
- UVM 中的深拷贝 :
uvm_object提供clone()和copy()方法,原理与本节的deep_copy一致,UVM 用户应优先使用clone()。
使用示例:
systemverilog
Packet p1 = new();
p1.payload = new[4];
p1.payload[0] = 8'hAA;
Packet p2 = p1.deep_copy();
p2.payload[0] = 8'hFF;
$display("%h", p1.payload[0]); // 输出 AA,独立
关键要点:深拷贝必须处理判空、动态数组需重新分配、子对象需递归调用。静态成员不参与复制。
7 参数化类(模板类)
使用 #(type T) 定义类,实现通用容器。
systemverilog
class Stack #(type T = int);
protected T items [$];
function void push(T t);
items.push_back(t);
endfunction
function T pop();
if (items.size() == 0) begin
$error("Stack empty");
return null;
end
return items.pop_back();
endfunction
function int size();
return items.size();
endfunction
endclass
initial begin
Stack #(string) str_stack = new();
str_stack.push("Hello");
str_stack.push("World");
$display("%s", str_stack.pop()); // World
Stack #(Transaction) tr_stack = new();
tr_stack.push(new());
end
注意:不同特化的参数化类是完全独立的类,静态成员不共享。
📌 参数化类支持多个类型参数和值参数:
systemverilogclass FIFO #(type T = int, int DEPTH = 1024); protected T mem [DEPTH]; // ... endclass
使用 typedef 简化:
systemverilog
typedef Stack#(Transaction) TransactionStack;
TransactionStack tr_stack = new(); // 无需每次都写参数
参数化类的继承(UVM 中最常用的写法):
systemverilog
class TransactionStack extends Stack #(Transaction);
function void push_back(Transaction t);
push(t);
endfunction
endclass
8 运算符重载
重载运算符可以让对象的操作更直观,验证中最常用的是重载 == 运算符来比较两个事务的内容是否相等。
📌 默认情况下,
==和===对于句柄的作用完全相同 。因为句柄是二值类型(只有0和1两种状态,null等价于0),不存在x或z状态,所以两种比较运算符没有区别。
标准建议 :使用 const ref 引用传递。
systemverilog
class Transaction;
bit [31:0] addr;
bit [31:0] data;
bit [7:0] payload [];
function bit operator==(const ref Transaction rhs);
if (rhs == null) return 0;
if (this.addr != rhs.addr) return 0;
if (this.data != rhs.data) return 0;
if (this.payload.size() != rhs.payload.size()) return 0;
foreach (this.payload[i])
if (this.payload[i] != rhs.payload[i]) return 0;
return 1;
endfunction
endclass
initial begin
Transaction tr1 = new(), tr2 = new();
tr1.addr = 32'h1000; tr2.addr = 32'h1000;
if (tr1 == tr2) $display("内容相等");
end
⚠️ 重要提示 :SystemVerilog 不会自动根据重载的
==生成对应的!=运算符 。如果需要使用!=比较对象内容,有两种方式:
- 手动重载
!=运算符- 在代码中使用
!(a == b)(推荐,避免重复代码)
9 常量成员与 const 函数
systemverilog
class Config;
const int BUS_WIDTH = 32; // 常量成员,不可修改
const bit [31:0] BASE_ADDR; // 实例常量,可在构造函数中赋值
function new(bit [31:0] base);
BASE_ADDR = base; // 常量只能在构造函数中初始化一次
endfunction
// const 成员函数:承诺不修改非静态成员
virtual function void display() const;
access_count++; // 正确:可以修改静态成员
// BUS_WIDTH = 64; // 错误:不能修改非静态 const 成员
$display("Bus width = %0d, base = 0x%h", BUS_WIDTH, BASE_ADDR);
endfunction
endclass
⚠️
const函数限制:
const函数只能调用其他const函数,不能调用非const函数,否则会编译错误------因为非const函数可能修改类成员,违反const的承诺。const函数承诺不修改任何非静态 成员变量。它可以修改静态成员变量,因为静态成员属于类本身,不属于任何特定对象。
10 类内部的枚举与 typedef
systemverilog
class Packet;
typedef enum { GOOD, BAD, CORRUPTED } pkt_type_e;
pkt_type_e pkt_type;
function void set_type(pkt_type_e _type);
this.pkt_type = _type;
endfunction
function void display();
$display("Packet type = %s", pkt_type.name());
endfunction
endclass
initial begin
Packet p = new();
p.set_type(Packet::GOOD); // 通过类作用域访问
p.display();
end
11 完整验证示例
下面是一个综合运用虚方法、深拷贝、参数化类、运算符重载以及多态驱动器的完整示例,可直接编译运行。
systemverilog
//===========================================================
// 文件名: oop_advanced_demo.sv
// 功能: 多态驱动器 + 深拷贝 + 参数化栈
// 运行: vcs -sverilog oop_advanced_demo.sv -R
//===========================================================
// 基础事务类(支持多态)
class Transaction;
int id;
static int next_id = 0;
function new();
id = next_id++;
endfunction
virtual function void display();
$display("Transaction[%0d]", id);
endfunction
endclass
class WriteTransaction extends Transaction;
bit [31:0] addr, data;
function new(bit [31:0] a, bit [31:0] d);
super.new();
addr = a;
data = d;
endfunction
virtual function void display();
$display("WRITE[%0d]: addr=0x%h, data=0x%h", id, addr, data);
endfunction
endclass
class ReadTransaction extends Transaction;
bit [31:0] addr;
function new(bit [31:0] a);
super.new();
addr = a;
endfunction
virtual function void display();
$display("READ[%0d]: addr=0x%h", id, addr);
endfunction
endclass
// 驱动器基类与派生类(多态)
class Driver;
virtual function void send(Transaction t);
$display("Driver sending:");
t.display();
endfunction
endclass
class AxiDriver extends Driver;
virtual function void send(Transaction t);
$display("AXI starting...");
super.send(t);
$display("AXI done.");
endfunction
endclass
// 深拷贝示例类
class Header;
int length;
function Header deep_copy();
Header c = new();
c.length = this.length;
return c;
endfunction
endclass
class Packet;
Header hdr;
bit [7:0] payload [];
// 深拷贝函数
function Packet deep_copy();
Packet c = new();
c.hdr = (this.hdr != null) ? this.hdr.deep_copy() : null;
c.payload = new[this.payload.size()];
c.payload = this.payload;
return c;
endfunction
// 改用普通equals函数(VCS 2016 不支持 operator==)
function bit equals(Packet rhs);
if (rhs == null) return 0;
if (payload.size() != rhs.payload.size()) return 0;
foreach (payload[i])
if (payload[i] != rhs.payload[i]) return 0;
return 1;
endfunction
endclass
// 参数化类
class Stack #(type T = int);
protected T items[$];
function void push(T t); items.push_back(t); endfunction
function T pop();
T tmp;
if (items.size() == 0) begin $error("Empty"); return tmp; end
return items.pop_back();
endfunction
function int size(); return items.size(); endfunction
endclass
// 测试模块
module tb_advanced;
initial begin
// 直接用类名声明变量
AxiDriver drv;
WriteTransaction wr;
ReadTransaction rd;
Stack #(string) s;
Packet p1, p2;
Stack #(Packet) ps; // 直接参数化
// ----- 多态驱动器演示 -----
$display("\n=== 多态驱动器 ===");
drv = new();
wr = new(32'h1000, 32'hA5);
rd = new(32'h2000);
drv.send(wr);
drv.send(rd);
// ----- 参数化类演示 -----
$display("\n=== 参数化类 ===");
s = new();
s.push("Hello");
s.push("World");
$display("Pop: %s", s.pop());
// ----- 深拷贝与比较演示 -----
$display("\n=== 深拷贝与比较 ===");
p1 = new();
p1.payload = new[2];
p1.payload[0] = 8'hAA;
p2 = p1.deep_copy();
$display("p1 equals p2 ? %b", p1.equals(p2));
// ----- 参数化类演示 -----
$display("\n=== 参数化类栈 ===");
ps = new();
ps.push(p1);
$display("Stack size: %0d", ps.size());
$finish;
end
endmodule
运行输出示例 :

以下是根据您的要求,删除第14节后优化并更正的内容:
12 核心概念速查表
| 概念 | 语法 | 作用 |
|---|---|---|
| 虚方法 | virtual function ... |
运行时动态绑定,实现多态 |
| 纯虚方法 | pure virtual function ...(); |
定义接口,强制子类实现 |
| 抽象类 | virtual class ... |
不能实例化,只能作为基类 |
| final 类 | final class ... |
禁止继承 |
| 向下转型 | $cast(sub, base) |
安全类型转换,运行时检查 |
| 深拷贝 | 手动实现 deep_copy() |
递归复制所有成员 |
| 参数化类 | class #(type T) |
通用数据结构模板 |
| 运算符重载 | virtual function bit operator==(ref class_type rhs); |
自定义对象内容比较 |
13 常见错误与编码建议
常见错误清单
- 虚方法重写变成重载 → 检查参数签名(类型、数量、顺序、方向、返回值)是否完全一致。
- 虚方法中使用默认参数 → 永远不要使用。
- 忘记
$cast直接向下转型 → 编译错误。使用$cast并检查返回值。 - 深拷贝时忘记递归复制子对象 → 子对象仍是共享引用。
- 混淆句柄比较和对象内容比较 → 重载
==或实现compare()方法。 - 抽象类直接实例化 → 编译错误。
- 在类中使用
initial块 → 初始化放在new()中。 $cast失败后未处理目标句柄 → 目标句柄保持原值,建议提前置null。- 抽象类构造函数声明为
local→ 子类无法调用super.new(),应使用protected。
编码规范建议
- 所有需要多态的方法都声明为
virtual - 抽象类显式使用
virtual class关键字(符合 IEEE 标准,不可使用abstract) - 深拷贝必须检查子对象
null - 全局配置参数使用
static const定义 - 子类重写虚方法时保留
virtual关键字 - 使用
typedef简化参数化类的重复实例化 - 优先使用
!(a == b)而非重载!=,避免代码重复
📢 关于文章 :原创实战分享,转载需注明出处。
📚 参考文档 :IEEE Standard for SystemVerilog