08 User Defined Types
In the previous chapter, we explored how to organize data using C-style arrays, strings and STL containers. These are great when you’re working with a list of similar things—like numbers, coordinates, or game items. But they only work when all the elements are of the same type.
In real-world programs, you often need to combine different kinds of values into one thing. Think of a game character: you might need to track their name, score, and health—all of which are different types of data. Handling each one separately quickly becomes awkward and error-prone.
That’s why programming languages like C++ let us create our own complex data types—custom structures that bundle different kinds of information together under a single name. Instead of juggling multiple variables for one concept, you can wrap them into a single, unified object that better reflects what you’re actually working with.
In this chapter, we’ll explore how to build and use these custom structures to keep your programs clean, organized, and scalable. We’ll start with the simplest and most practical form: grouping variables into one meaningful unit.
Structs
A struct lets you group related data under a single name. Its members can be of different types, and you access them using the dot operator. Structs are great for organizing simple, related pieces of information—like integers, floating-point numbers, or strings—into a single unit. Like arrays, the members of a struct are stored in contiguous memory. If you remember why this matters, it’s the reason we can perform type punning with structs.
When a struct contains only simple data types without extra behavior, it’s often called a Plain Old Data (POD) type.
main.cpp | |
---|---|
output | |
---|---|
In this example, the struct
name Player
defines a custom data type, allowing us to create variables of that type.
Notice that the struct
like other structure we will be covering is defined outside the main function, which gives it global scope.
This isn’t strictly necessary—if you only need to use it within a single function or other scope, you can define it there.
However, in most cases, the entire translation unit needs access to it, so defining it globally is the usual approach.
Also, the struct
definition must end with a semicolon, forgetting this will cause a compilation error.
Beyond just holding data, a struct can also include functions that can manipulate that data. These are called member functions or methods, and they can operate on the struct’s members because they’re defined inside the structs scope.
output | |
---|---|
Note
Adding methods to structs is a feature unique to C++. This is important to note because structs originate from C, where attaching methods is not possible.
We can also initialize all member variables at once using curly braces with values — this is known as aggregate initialization.
However, the values must be provided in the same order as the member variables are declared in the struct
, since they are assigned from top to bottom.
C++ again offers a clearer alternative called designated initializers, where you explicitly specify which value corresponds to each member by name.
This improves readability, especially when the struct
definition is far from its usage or when dealing with many members.
output | |
---|---|
Unions
A union
is similar to a struct
in that it groups related variables under a single name.
However, unlike a struct
, a union
shares the same memory space among all its members.
This means a union
can only store one value at a time, and its total size is determined by the largest member it contains.
For example, if a union holds four float
members named a
, b
, c
and d
, it will only occupy the size of one float—4 bytes—instead of the combined size of all four, which would total 16 bytes.
Since all members share the same memory location, changing one member will affect the others.
Unions are useful for type punning, which means interpreting the same chunk of memory in different ways.
They also enable aliasing, where the same data can be accessed under different names.
For instance, a union
might let you treat a 3D vector (x, y, z) as an RGB color (r, g, b) without duplicating memory.
Also unions can be declared on their own or embedded anonymously inside other structures for convenience. Unlike structs, unions do not support member functions, except for constructors and destructors.
In this example, we define a union called Vector3
that allows accessing its data in two different ways:
- As coordinates (x, y, z) when used as a 3D vector.
- As color components (r, g, b) when used in a color palette.
Notice that these variables are grouped inside anonymous structs within the union. Without the structs, the union would only allow one active variable at a time, so you couldn’t store all three values simultaneously.
Anonymous structs act like regular structs but inherit their member names directly into the union.
This lets us write vec3.x
instead of something longer like vec3.color.r
, keeping the code clean.
Enums
Enums, short for enumerations, allow us to define a set of named constants that are represented by integer values. They’re useful for giving meaningful names to integer values, making the code easier to read and less prone to errors.
One big advantage of enums is that they limit the possible values a variable can hold. Instead of using plain integers to represent different states, enums ensure only predefined values are allowed.
For example, say we want to represent different screen states in a program, like a Start Screen and an End Screen.
Without enums, we might use integers like 1
for the start screen and 2
for the end screen—but this can cause bugs if invalid numbers are used accidentally.
Enums prevent that by defining a fixed set of allowed values.
main.cpp | |
---|---|
In this example, the enum members start at 0
, which is the default behavior in C++, and each subsequent member increases by 1
.
However, you can specify a custom starting value, and any subsequent members without an explicit value will continue incrementing from the last assigned number.
This continues until another manually assigned value appears, which resets the counting from that new number.
By default, an enum uses int (typically 4 bytes) as the underlying type to store its values. But if that's more memory than necessary, you can choose a smaller integer type by using a colon after the enum name, followed by the type you want.
Warning
C++ allows you to print regular enums and use them in arithmetic expressions because they implicitly convert to integers.
But this doesn't apply to all enums.
If you use enum class
(scoped enum), or if you manually specify an underlying type (like char
, short
, or unsigned int
), you lose automatic conversion in many contexts — especially with enum class
.
This means you'll need to explicitly cast the value to an integer type before printing it or doing math with it.
As you’ve seen, regular enums in C++ are a handy way to assign names to integer or other numeric values. They make code easier to read and help avoid using magic numbers — literal numbers in code without clear meaning.
But traditional enums have some downsides that become more noticeable as your code grows in size and complexity.
One of the main issues is that all the enum
values share the same global scope, which can easily lead to naming conflicts.
Also, regular enums implicitly convert to integers, which can result in unexpected behavior if you're not careful.
To address these problems, C++ introduced scoped enums using enum class
.
Scoped enums behave more strictly — and that's a good thing. Here's what makes them different:
- No global clutter: Enum members no longer leak into the global scope.
- Type safety: You can’t accidentally treat them like integers without a cast.
- Explicit usage: You must qualify enum members with their enum name, which makes the code easier to follow.
Classes
By now, we've looked at different ways to group related pieces of data — using structs, unions, and even basic enums. These approaches all come from the C side of things. While they don't behave identically in C++, they still follow the same procedural mindset.
But as your programs begin to model real-world entities — for instance, in medical software where you might represent a patient, track their medical history, and perform operations like scheduling appointments or updating test results — a more powerful abstraction becomes useful. That’s where classes come in.
This isn’t to say that classes are mandatory. Many developers stick with procedural programming, which avoids classes and sticks to functions and data structures. It's perfectly valid and often simpler for smaller or tightly focused programs.
However, another widely used approach is Object-Oriented Programming (OOP) — and classes are at the heart of it.
A class is a blueprint for building objects that combine data and behavior. Classes form the foundation of OOP, a style of programming that models things as self-contained units called objects. Each object holds both state (data) and functionality (methods).
Unlike Java or C#, C++ doesn’t force you into OOP. It leaves the choice to you. But when your project calls for organizing logic around real-world concepts, classes give you the right tools.
Just like a struct, a class lets you group related member variables under a single name. But with classes, you also get access control, encapsulation, and structured member functions — also known as methods — that act on the internal state of the object.
main.cpp | |
---|---|
output | |
---|---|
With the class implementation, we can see some key differences compared to structs. The most notable one is access control, made possible through access specifiers. In C++, classes support three kinds of access.
Access Specifier | Description |
---|---|
public |
Members can be accessed from anywhere in the program. |
private |
Members can only be accessed from within the class. This is the default access level in classes. They cannot be accessed directly from outside the class, but can be accessed indirectly through public methods. |
protected |
Like private , but members are also accessible by derived classes (i.e., subclasses in inheritance). |
Note
In C++, the only difference between a struct
and a class
is their default access level: members of a struct
are public by default, while members of a class
are private by default.
In C, structs
cannot contain methods, but in C++, they can.
However, it’s generally best to use structs for simple data grouping—types that hold multiple variables without associated functionality—to keep design intentions clear and maintain a clean separation between plain data and more complex behavior.
In the example above, the name
and age
members were declared as public, which allows us to set and access them directly in main()
using the dot operator.
If they had been marked private, we wouldn’t be able to access them directly from outside the class.
Instead, we’d need to use getter and setter methods (or another kind of public interface) to interact with those values — something we’ll cover a bit later.
Another common convention is to prefix private member variables with m_
or suffix them with _
(e.g., m_name
or age_
).
Here, we’ll use the underscore suffix style as it tends to be cleaner and easier to read.
This naming convention helps clearly distinguish member variables from local variables or function parameters, especially in larger codebases.
It also improves readability, reduces naming conflicts, and eliminates the need to use the this pointer explicitly in most cases — something we’ll explore a bit later as well.
Static Inside Class
So far, we’ve seen how classes let each object maintain its own set of member variables with data unique to that particular object, created from the blueprint of a class like Patient
.
But sometimes, you want a variable or function to belong to the class itself, rather than to any single object.
This can be achived by placing static
keyword before a member variable or method’s data type, you make it shared across all instances of the class.
Instead of belonging to one object, the member belongs to the class as a whole.
For example, a static variable can count how many objects have been created — like tracking how many patients have been registered. This shared state is useful for managing common data or resources that all objects of the class need to access.
We can see that the value
is shared across all instances of the class.
However, since it's a static member, it must be defined outside the class—this step is required to allocate memory for it.
If you forget to do this, you'll get a linker error.
Static Outside Class
The static
keyword in C++ has two distinct meanings depending on where it's used.
One use, as we saw in the previous section, is inside a class—where it makes a member variable or method shared across all instances.
But static
can also be used outside of a class, and in that context, it means something different.
When applied at file scope, static
makes a variable or function local to the current translation unit—in other words, visible only within the file in which it's declared.
This limits external access, prevents unintended usage from other files, and helps avoid name conflicts across a project.
If a variable is declared as static
at the top level of a file (not inside any function or class), it exists for the lifetime of the program but remains completely invisible to other source files.
This allows the same variable name to safely exist in multiple translation units without clashing.
static.cpp | |
---|---|
main.cpp | |
---|---|
output | |
---|---|
If we declare a global variable like value
in static.cpp
and then declare another global value
in main.cpp
, the compiler produces a linker error due to multiple definitions of the same symbol.
Global variables without the static keyword have external linkage by default, meaning their names are visible across translation units.
That’s what causes the conflict here.
To avoid this problem, we can mark the variable in main.cpp
as static
.
This limits its visibility to just that file, preventing it from colliding with value in static.cpp
.
static.cpp | |
---|---|
main.cpp | |
---|---|
output | |
---|---|
Now both files can define a variable named value
without conflict, because value
in main.cpp
is local to that translation unit thanks to the static
keyword.
If we actually want to share the variable from static.cpp
with main.cpp
, we need to remove static
and use the extern keyword to declare the variable in main.cpp
.
This way, main.cpp
refers to the definition from static.cpp
, and there's no redefinition.
static.cpp | |
---|---|
main.cpp | |
---|---|
output | |
---|---|
By using extern
, we give main.cpp
access to the variable defined in another file.
This is known as external linkage, and it allows multiple files to share global variables while keeping a single definition in one place.
Getters and Setters
In general, member variables should not be exposed directly outside of a class. This encapsulation helps prevent unintended modification or misuse. Instead, we provide getter and setter functions—public member functions designed specifically for reading or writing private data.
This approach allows us to maintain control over how internal state is accessed or changed. For example, setters can enforce validation rules before assigning values, adding a layer of protection and integrity to our data.
output | |
---|---|
Constant Member Functions
In the previous section, we can see a const
qualifier behind the class method declaration.
When designing a class, it’s important to distinguish between member functions that modify the object’s internal state and those that don’t.
Functions that do not change the object should be explicitly marked as const
.
That is because doing so allows those functions to be safely called on constant instances of your class—objects that have been declared immutable and are not supposed to be changed.
output | |
---|---|
Constructors
A constructor is a special kind of method that runs automatically when an object of a class is created. Unlike regular methods, a constructor has no return type—not even void—and its name must match the class exactly.
They are typically declared public so that objects can be created from outside the class. However, this isn’t always the case—when inheritance is involved, constructors might be protected or private, allowing them to be called only by derived classes or within the class itself. This is often used to enforce controlled object creation or to support specific inheritance patterns, which we’ll explore later.
The primary purpose of a constructor is to initialize member variables and set up the object so it’s ready to use immediately. This makes code cleaner and more expressive by reducing the need for separate assignment statements after object creation.
Constructors also play a critical role in how C++ manages memory and resources. They are involved not only in basic initialization but also in how objects are copied or moved, which can have performance implications.
If you don’t explicitly define a constructor, compiler will automatically generate a default one—a no-argument constructor that initializes the object with default values. However, these defaults may not be meaningful unless you define them yourself. By writing your own constructors, you gain control over how your objects are initialized and what their initial state is. It’s also important to note that the default constructor is only implicitly generated if no other constructors are defined. If you define any constructor, such as one that takes parameters, the compiler will not automatically generate a default constructor unless you explicitly tell it to.
Note
In these examples, we’ll use struct
instead of class
.
As mentioned earlier, in C++, the only difference between a struct
and a class
is that struct
members are public by default, whereas class
members are private.
Using struct
helps us keep the examples simple and avoids cluttering the code with access modifiers, getters and setters.
However, in real-world programs, use class
for better clarity, encapsulation, and adherence to object-oriented design practices.
Member Initializer List
In addition C++ provides a efficient way to write parameterized constructors using member initializer lists.
This isn’t just a stylistic choice—it can significantly improve performance by initializing members in place, rather than first default-initializing them and then reassigning values inside the constructor body.
In other words, member initializer lists construct the members directly with the given values, similar to how emplace_back
works with vectors by constructing objects directly where they belong.
output | |
---|---|
Using a member initializer list initializes members directly with the provided values. In contrast, assigning values inside the constructor body causes members to be default-initialized first and then assigned new values, which can be less efficient—especially for complex types.
Moreover, member initializer lists are required to initialize const members, reference members, and base classes, since these cannot be assigned inside the constructor body.
Beyond the performance benefits, member initializer lists make the constructor’s intent clearer and the code more concise by eliminating unnecessary syntax. For these reasons, using member initializer lists should be preferred whenever possible.
Copy Constructor
Data copying in programming is a lot like copying a chunk of text—it duplicates the contents of one instance into another.
For primitive types, copying is straightforward and typically has little impact on performance.
But when it comes to user-defined types or containers like std::vector
or std::map
, copying can involve a lot more behind the scenes.
These complex types may hold large amounts of data or even manage resources like memory or file handles. Copying such objects isn't just a matter of duplicating values—it can be expensive in terms of performance and memory usage.
That's why it's important to understand how copying works in C++, and what mechanisms are involved—especially when your types grow in complexity.
main.cpp | |
---|---|
output | |
---|---|
In the example above, b
is a copy of a
.
It holds its own separate memory, so changes to b.x
have no effect on a.x
.
This is value-based copying, and it's the default behavior in C++ for objects that aren’t using pointers.
However, when pointers are involved, copying behaves differently. Instead of copying the data being pointed to, only the address to the data is copied. That means both variables will point to the same memory location.
main.cpp | |
---|---|
This kind of copying is handled by a special constructor called the copy constructor. C++ compiler automatically provides one for your class if you don’t explicitly define it. The default copy constructor performs a member-wise copy—copying each member field from one object to another.
If you want to customize what happens when an object is copied (for example, to deeply copy a pointer or to log when copying happens), you can define your own.
Copy Assignment Operator
The copy assignment operator is closely related to the copy constructor, but it comes into play after an object has already been constructed.
Whereas the copy constructor creates a new object from an existing one, the copy assignment operator replaces the contents of an already existing object with those from another.
C++ provides a default version of this operator if you don't write your own. Like the default copy constructor, it performs a member-wise copy of each field.
But if your type manages resources manually, or you just want to log or tweak the behavior, you can define your own.
Move Constructor
Info
We’ve already covered move semantics in detail in earlier chapters.
Here, we focus on how to implement a move constructor for your custom types, enabling them to support move semantics properly.
Earlier in the book, we introduced move semantics—a technique where an object’s resources are transferred instead of copied. Now, let's see how this idea applies when working with classes and structs, especially when you're managing resources like heap-allocated memory.
Moving in programming is like reusing an object’s existing resources instead of duplicating them. While copying creates a new version of the data, moving transfers ownership from one object to another. This is especially useful when working with large objects, heap-allocated data, or temporary values, where performance matters.
Warning
After moving from an object, that object remains in a valid but unspecified state. In practice, this usually means its internal pointers are null or empty, but you shouldn't make assumptions beyond that unless you reset or reassign the object yourself.
main.cpp | |
---|---|
In this example, b
takes over the internal resources that were originally held by a
.
After the move, a
is left in a valid but unspecified state.
While many implementations leave moved-from strings empty, the C++ standard only guarantees that the object can still be safely destroyed or assigned to.
You shouldn't rely on its contents unless you explicitly assign a new value.
This is one of the main reasons move constructors are so useful—they allow efficient transfer of ownership without unnecessary copying, while keeping resource management safe.
But they're not really needed in classes that don't manage resources manually, such as those that only contain primitive types or rely entirely on STL containers like std::vector
or std::string
.
In those cases, the default move constructor generated by the compiler is usually sufficient, because all the member types already know how to move themselves efficiently. You only need to define a custom move constructor when your class holds raw resources—like heap memory, file handles, or pointers—that require careful cleanup and transfer logic.
When your class manages raw resources—like heap memory—you'll often want to take control of how those resources are transferred between objects. In such cases, defining a custom move constructor lets you explicitly steal ownership and prevent expensive deep copies.
This pattern is essential whenever your class directly manages memory or other system resources. It gives you precise control over how resources are moved, helping ensure both correctness and performance.
Move Assignment Operator
Just like how the copy assignment operator complements the copy constructor, the move assignment operator is the companion to the move constructor.
Where the move constructor initializes a new object by stealing resources, the move assignment operator replaces the contents of an already existing object by taking ownership of another object’s resources.
This is especially important when working with resource-owning types, like classes that manage dynamic memory or handles.
Tip
Just like with the move constructor, if your type doesn't manage raw resources manually, you probably don’t need to write a custom move assignment operator. The compiler-generated one is often sufficient.
Together, the move constructor and move assignment operator complete your class's ability to participate in move semantics safely and efficiently.
Explicit Constructor
By default, if a constructor takes a single argument (or a set of arguments that could be matched in a single call), C++ allows implicit conversions from those argument types to the class type. This means the compiler might automatically create an object—even if you didn’t explicitly write a constructor call—which can sometimes lead to unintended or surprising behavior.
To prevent this, you can use the explicit
keyword to disable automatic conversions.
This makes your code safer and more predictable, especially when conversions shouldn’t happen silently.
An explicit constructor can only be called when written directly in code. The compiler won’t use it for implicit type conversions.
main.cpp | |
---|---|
output | |
---|---|
In this example, the compiler automatically creates a temporary Point
from the {5, 10}
initializer.
That’s possible because the constructor accepts two int
values, and it isn’t marked explicit
.
However, this might not always be desirable—especially if you want to avoid accidental conversions.
To make construction explicit-only, mark the constructor with the explicit
keyword.
output | |
---|---|
With explicit, the compiler no longer allows the conversion from {5, 10}
to a Point
automatically.
You must create the object yourself before passing it into the function—making your intention clear and preventing accidental misuse.
Rule of Zero and Five
In C++, the Rule of Five is an important guideline when you're working with classes that manage resources manually—like dynamic memory, file handles, or network sockets. If you define any one of the following special member functions, you should usually define all five to ensure consistent, predictable behavior. This is the Rule of Five.
Special Member Function | When You Might Need It |
---|---|
Destructor | When your class manages resources like heap memory, file handles, or network sockets that must be manually released. |
Copy Constructor | When you need to define how your object is copied (deep vs. shallow copy). |
Copy Assignment Operator | When your object needs proper behavior during copy assignment (e.g., a = b ). |
Move Constructor | When you want to efficiently transfer resources from temporaries (e.g., MyClass a = std::move(b) ). |
Move Assignment Operator | When your object should efficiently take over resources during assignment from an rvalue (e.g., a = std::move(b) ). |
However, if your class doesn't manage resources directly—and instead relies only on built-in types or STL containers like std::vector
or std::string
—then you don't need to define any of these functions yourself.
This is known as the Rule of Zero: let the compiler generate the special functions for you automatically.
Destructors
Info
This section is fairly short, as the core concept is straightforward. However, we’ll return to the topic later—particularly when we cover inheritance—where destructors become a bit more important.
Similar to a constructor, a destructor is a special method, but it's called automatically when an object goes out of scope or is explicitly deleted. Its job is to clean up—releasing resources the object may have acquired during its lifetime, such as dynamic memory, file handles, or network connections.
Just like constructors, destructors have a unique syntax:
- They have no return type (not even void).
- Their name matches the class, but with a tilde
~
prefix. - A class can have only one destructor, and it cannot take any parameters.
Destructors are declared public in most cases, especially when objects are created on the stack or dynamically via new. However, in more advanced designs (like factories or polymorphic hierarchies), destructors may be protected or virtual. We’ll explore those patterns later.
The most common use case for a destructor is to free dynamically allocated memory or close resources.
Note
If your class acquires resources using new
, malloc
, or similar raw APIs, you should always define a destructor to release them.
For polymorphic classes—those meant to be used via base pointers—you must make the destructor virtual to ensure the correct one is called.
We’ll cover that in detail when we get to inheritance.
Arrow Operator
Up until now, we've been using the dot operator to access members of structs and classes. But that only works when we have a direct instance of the object.
When we have a pointer to an object, things change.
We can’t use the dot operator anymore — because the pointer itself doesn’t have those members.
Instead, we use the arrow operator (->
) to access them through the pointer.
output | |
---|---|
Note
If you're thinking, "Wait, couldn't we just dereference the pointer and call the method like this: (*ptr_point).Print()
?" — you're absolutely right.
However, that syntax is a bit messy to read.
That’s exactly why the arrow operator exists: it's just a cleaner, more readable shortcut for (*pointer).member
.
The arrow operator can even be used in clever low-level tricks — like calculating the byte offset of a struct member from the beginning of the structure.
Info
This section covers advanced memory management concepts, including how data is organized in memory and how to optimize it. This knowledge is generally not required unless you are working in performance-critical or low-level systems.
Current Instance Pointer
In C++, we have access to a special keyword called this
.
It is only available inside member functions, and it points to the current instance of the class or object the method is acting on.
While using this
is often optional, it becomes especially useful when there is a name clash—for example, between constructor parameters and member variables.
To resolve this ambiguity, we can use the this
pointer to explicitly refer to the member variables.
Since this
is a pointer, we access members using the arrow operator.
example | |
---|---|
Note
Remember that in many C++ style guides—like Google’s or others—it’s common to suffix member variables with _
(e.g., name_
) or prefix them with m_
(e.g., m_name
).
This naming convention helps avoid name clashes entirely and improves readability without needing to use this->
.
Friend Functions
In C++, we can grant non-member functions access to a class’s private and protected members by declaring them as friend. Although these functions are not part of the class itself, they are treated as trusted and can interact with the internal state of the class directly.
This approach is useful when a function needs to work closely with a class—such as when overloading operators—but doesn’t logically belong as a member, helping keep responsibilities separated while still allowing close access.
output | |
---|---|
Inheritance
In more complex programs—especially games—we often need to structure our code in a way that’s both reusable and maintainable.
Imagine you have a Player
class and an Enemy
class.
At first glance, they may seem different, but both share several common traits: they might have a name
, hitpoints
, a level
, or a position
in the game world.
Rather than duplicating those shared members in every class, we can extract them into a more general-purpose class—say, Entity
.
Both Player
and Enemy
can then be built on top of Entity
, inheriting its functionality and enriching it with their own unique behavior.
This mechanism is called inheritance.
Inheritance lets us write cleaner, more modular code. It allows us to group shared functionality in one place and extend or override it as needed in specialized types.
output | |
---|---|
Inheritance Type | public Members in Base |
protected Members in Base |
private Members in Base |
---|---|---|---|
public |
Remain public in derived |
Remain protected in derived |
Not accessible |
protected |
Become protected |
Remain protected |
Not accessible |
private |
Become private |
Become private |
Not accessible |
Polymorphism
Polymorphism refers to the ability of different classes to respond to the same function call in their own way. Unlike regular function overloading (where multiple functions share a name but differ in parameters), polymorphism usually involves methods of a base class that are overridden in derived classes to provide specialized behavior.
output | |
---|---|
Virtual Methods
In the previous section, we used the virtual keyword to enable polymorphism. Let’s now take a moment to focus on what virtual methods actually are.
A virtual method is a member function that can be overridden in a derived class. When you call it through a base class pointer or reference, the version that's selected is based on the actual type of the object — not the type of the pointer or reference.
This mechanism is called dynamic dispatch, and it only happens when you're using a base class pointer or reference and the method is marked as virtual.
Keyword | Purpose | Where it's used | Notes |
---|---|---|---|
virtual |
Marks a method as overridable in derived classes | In base class declarations | Enables runtime polymorphism. |
override |
Indicates that a method intentionally overrides a base virtual method | In derived class definitions | Helps catch bugs caused by mismatched method signatures. |
final |
Prevents a virtual method from being overridden further | In derived class definitions | Can also be used to seal entire classes: class MyFinalClass final {} |
Tip
Always use override when overriding virtual methods. It makes your intent clear and helps the compiler catch mistakes.
Interfaces
Now that we've seen how virtual methods work, let's talk about a special case: interfaces.
In C++, an interface is typically just a class that contains only pure virtual functions — methods that must be implemented by any derived class.
They have no body and are declared using = 0
.
Interfaces are especially useful for enforcing consistent behavior across many unrelated types — for example, in plugins, simulation systems, or game objects. You can build systems that interact with any class that implements the interface, without caring what kind of object it is.
You can think of an interface as an abstract base class made up entirely of pure virtual methods. And yes — in C++, a class can inherit from multiple interfaces.
Note
A class that contains at least one pure virtual method becomes an abstract class, and it cannot be instantiated directly.
main.cpp | |
---|---|
output | |
---|---|
Singletons
Sometimes, we only ever want a single instance of a class to exist across an entire program. Maybe it's a logger, a configuration manager, or a global game state—whatever the case, the Singleton design pattern is exactly for this pupose. Instead of letting code freely create new objects, a Singleton class controls its own instantiation and ensures only one object is ever made. The typical approach involves a static method that hands out a reference to the sole instance, creating it the first time it's needed.
Exercises
More to come ...