Classes Basics
Every page before this one has covered types that hold data — signals, structs, arrays. None of them bundle behavior with that data, and none of them have any notion of "make a new one of these while the simulation is running." A testbench, though, is naturally full of things that are exactly that: a transaction, a driver, a scoreboard entry — each one a bundle of related data plus the operations that act on it, potentially created and destroyed many times over a test's run. SystemVerilog's class is where real object-oriented programming enters the language, and it's the single biggest addition SystemVerilog makes over plain Verilog.
Declaring a class
class transaction;
rand bit [7:0] data;
bit valid;
function new();
valid = 1;
endfunction
function void display();
$display("data=%0h valid=%0b", data, valid);
endfunction
endclass
A class bundles properties (data, valid — its data members) and methods (display() — functions/tasks that operate on those properties) into one named type. rand on data marks it as randomizable — covered in full in Section D, later in this topic; for now it's enough to recognize it as a class-property annotation.
new(): the constructor
Every class has a built-in method called new() — called automatically whenever a class is instantiated, and the place to put any setup a fresh object needs (as valid = 1 does above). A class can also declare new() with arguments, letting the caller supply initial values:
class transaction;
rand bit [7:0] data;
bit valid;
function new(bit [7:0] init_data = 0);
data = init_data;
valid = 1;
endfunction
endclass
Handles vs. objects: the pointer distinction
transaction tr; // tr is a handle — currently pointing at nothing
tr = new(); // now tr points at a real, newly-allocated transaction object
transaction tr2;
tr2 = tr; // tr2 now points at the SAME object as tr, not a copy
tr2.data = 8'hFF; // this also changes what tr.data reads, since they're the same object
Declaring transaction tr; creates a handle — a reference to an object, not the object itself, exactly like a pointer or reference in most software languages. Nothing is actually allocated until new() runs. This is the single most important mental model shift from every other type covered so far: a logic or struct variable directly contains its value, but a class variable only ever refers to an object living elsewhere, and two handles can refer to the same object — a genuinely new idea for anyone arriving from plain Verilog, where no such aliasing is possible.
null: a handle pointing at nothing
transaction tr; // tr is null until assigned
if (tr == null)
$display("tr has not been constructed yet");
tr = new();
if (tr != null)
tr.display();
A handle that hasn't been assigned via new() (or from another handle) holds the special value null — using it (calling a method, reading a property) while null is a runtime error, exactly like a null-pointer dereference in C or Java. Checking != null before using a handle that might not have been constructed yet is a standard, necessary defensive pattern in class-based testbench code.
The this keyword: when a name would otherwise collide
The earlier constructor example dodges a naming collision by calling its argument init_data instead of data. Real code often prefers matching the argument name to the property it initializes — and that's exactly when this becomes necessary:
class transaction;
rand bit [7:0] data;
bit valid;
function new(bit [7:0] data);
this.data = data; // this.data is the property; the bare data is the argument
valid = 1;
endfunction
endclass
Inside a method, a bare identifier resolves to the nearest enclosing scope — here, that's the argument data, not the property of the same name. this.data explicitly means "the property on the object currently executing this method," resolving the collision unambiguously. Without this, data = data; would just assign the argument to itself and leave the actual property untouched. this isn't limited to constructors — it works in any method whenever a local name (an argument or a local variable) shadows a property of the same name.
No delete: objects are garbage-collected automatically
SystemVerilog has no delete/free keyword for class objects, unlike C++. An object is eligible for automatic garbage collection the moment no handle anywhere still refers to it — for example, when the one handle that pointed to it is reassigned to point elsewhere, set to null, or simply goes out of scope with nothing else pointing at the same object:
transaction tr;
tr = new(); // one live object, referenced by tr
tr = new(); // a second object is created; the first object now has zero
// referencing handles and becomes eligible for garbage collection
There's no explicit call to make this happen and no way to force it early — the simulator's runtime tracks references and reclaims memory on its own, the same automatic-garbage-collection model languages like Java and C# use, in contrast to C++'s manual new/delete pairing. This is also why a class never declares a destructor: there is no matching concept to new() on the teardown side.
Copying an object: new from an existing handle
transaction tr1 = new();
tr1.data = 8'hAB;
transaction tr2 = new tr1; // shallow copy: a new object with tr1's property values
tr2.data = 8'hFF; // does not affect tr1.data — tr2 is a genuinely separate object
tr2 = tr1; (no new) makes tr2 a second handle to the same object — the aliasing behavior from the handles section above. tr2 = new tr1; is different: it allocates a genuinely new object and copies tr1's property values into it field-by-field, without re-running tr1's own constructor body. This is a shallow copy — if a property itself is a class handle, the copy shares that same nested object rather than recursively copying it too; a true deep copy of nested objects needs a hand-written copy() method that explicitly news each nested handle in turn.
Why this matters immediately, even before inheritance
A transaction class, once declared, can be instantiated as many times as a test needs — new() called once per stimulus item, each one an independent object — something no plain Verilog construct can express (a reg/struct variable is one fixed piece of storage, not something you can dynamically create more copies of at runtime). This is also, not coincidentally, exactly what UVM's entire class library is built from: every uvm_object and uvm_component your UVM testbenches extend is, underneath the UVM-specific machinery, an ordinary SystemVerilog class using exactly the new()/handle/null mechanics on this page.
What's next
A class on its own is a container. The next page covers what makes classes genuinely powerful for building a testbench: inheritance — extending one class from another to reuse and specialize behavior — and polymorphism, calling the same method name on different object types with the right behavior chosen automatically.