This lesson is a quick introductory tour of classes, structs, and enums within C++. It is not intended for those who are entirely new to programming. Rather, the people who may find it useful include:
It summarises several lessons from our introductory course. Those looking for more thorough explanations or additional context should consider completing Chapters 3, 4, and 8 of that course.
In C++, classes are created like this:
class Character {
// Member Variable
int Health{100};
// Member Function (Method)
void TakeDamage(int Damage) {
Health -= Damage;
}
}
// Creating Objects from the class
Character Frodo;
We can control how accessible class variables and methods are. This allows us to have a "public" part of our objects - an interface which we expect other objects to interact with.
Meanwhile, we can restrict access to other methods and variables, ensuring they’re only available to internal code. This makes our objects easier to use, and our classes simpler to create.
C++ has three access levels:
public
members are available to be called by any code that has access to one of our class instancesprotected
members are only accessible to functions within the class, or any class that inherits from it. Inheritance is covered in the next sectionprivate
members are only accessible to functions within the classThere can be any number of specifiers within a class. Members have the accessibility defined by the closest proceeding specifier.
By default, class members are private
.
class Character {
int a; // Private
public:
int b; // Public
int c; // Public
protected:
int d; // Protected
private:
int e; // Private
public:
int f; // Public
};
int main() {
Character Player;
// Not allowed - a is private
Player.a;
// Allowed - b is public
Player.b;
}
error: 'Character::a': cannot access
private member declared in class 'Character'
Where required, getters and setters can be implemented manually, allowing us to maintain class invariants like "the Health
of a Character
is never negative":
#include <iostream>
class Character {
public:
int GetHealth(){ return Health; }
void SetHealth(int NewValue){
if (NewValue < 0) {
Health = 0;
} else {
Health = NewValue;
}
}
private:
int Health;
};
int main(){
Character Player;
// Not allowed - Health is private
// Player.Health = -10;
// Must use the setter instead:
Player.SetHealth(-10);
std::cout << "Health: " << Player.GetHealth();
}
Health: 0
Our classes can inherit from other classes like this:
class Character {};
class Dragon : Character {};
Inheritance can also be private, protected, or public. This sets the maximum access level of inherited members, ie:
public
inheritance lets inherited members maintain their original access levelprotected
inheritance changes inherited public
members to protected
private
inheritance makes all inherited members private
By default, C++ inheritance is private
. The most useful, and most similar to other programming languages, is public inheritance:
class Character {};
class Dragon : public Character {};
C++ supports multiple inheritance and abstract classes.
Pure virtual classes are also available, which cover similar use cases to interfaces from other programming languages.
All three of these concepts are introduced later in this course
In C++, structs are almost identical to classes. The only difference is that by default, struct members are public
struct Vector3 {
float x; // Public
float y; // Public
float z; // Public
};
Vector3 MyPosition;
Despite the similarities, by convention, we prefer to use classes for complex requirements and restrict our use of structs to simpler use cases.
For example, something requiring inheritance, references to other complex types, and lots of methods should probably be a class
. Something to hold a few primitive values together should probably be a struct
.
Constructors are functions that are automatically called when we create objects from our classes and structs.
A default constructor does not require any arguments (ie, it does not have any parameters, or all of its parameters are optional)
Our classes come with an implicit default constructor, which is called when we write code like this:
// Call the default constructor
Vector MyVector;
We can define custom constructors for our classes and structs by creating a member function with no return type, and the same name as our class or struct. For example, a constructor for the Vector
struct would also be called Vector
:
struct Vector {
// Initialise all components to the same value
Vector(float Value) {
x = Value;
y = Value;
z = Value;
}
float x;
float y;
float z;
};
Once we define any custom constructor, the implicit default constructor is deleted:
// Call custom constructor
Vector MyVector{4.0f};
// Compilation error
Vector AnotherVector;
'Vector': no default constructor available
We can re-add it if desired, simply by providing a constructor that requires no arguments, or explicitly re-adding the default implementation using this syntax:
struct Vector {
Vector() = default;
// ...
};
In general, we can define as many constructors as we want, as long as their parameter lists are sufficiently unique such that the compiler knows which constructor we’re calling each time we initialize an object:
#include <iostream>
struct Vector {
// The default constructor
Vector(){
std::cout << "Default Constructor\n";
}
// Initialise all components to the same value
Vector(float Value){
std::cout << "(float) Constructor\n";
x = Value;
y = Value;
z = Value;
}
// Construct from another vector and length
Vector(Vector OtherVector, float Length){
std::cout <<
"(Vector, float) Constructor\n";
// ...
}
float x;
float y;
float z;
};
int main(){
Vector A;
Vector B{1.4f};
Vector C{B, 1.f};
}
Default Constructor
(float) Constructor
(Vector, float) Constructor
Where a constructor is initializing member variables, the preferred way of doing this is through a member initializer list. This is a unique piece of syntax between our constructor heading and body. We add a colon :
followed by a list of comma-separated initializations.
Below, our constructor is initializing x
, y
, and z
to 1.f
, 2.f
, and 3.f
using a member initializer list:
struct Vector {
Vector() :
// Member Initialiser List
x{1.f}, y{2.f}, z{3.f}
{
// Constructor Body
std::cout << "Constructing!";
}
float x;
float y;
float z;
};
Member initializer lists can use expressions, as well as constructor parameters:
struct Vector {
Vector(float x) :
// Member Initialiser List
x{x},
y{1.f + 2.f},
z{GetRandomFloat()}
{
// Constructor Body
std::cout << "Constructing!";
}
float x;
float y;
float z;
};
When we have code we need to run when an object is destroyed, our classes and structs can define a destructor. A destructor uses the ~
symbol followed by the class/struct name:
struct Vector {
~Vector() {
std::cout << "Destructing!";
}
};
Destruction of objects, and the object life cycle more generally, is covered later in this course.
When all members of a class or struct are public
, we can use aggregate initialization to provide values for those members when constructing an object:
struct Vector {
float x;
float y;
float z;
};
Vector MyVector{1.1f, 5.8f, 2.f};
We can think of structured binding as aggregate initialization in reverse. If we have an object, we can extract all its public members as variables in a single expression. Rather than doing this:
float x{MyVector.x};
float y{MyVector.y};
float z{MyVector.z};
We can instead do this:
auto [x, y, z] { MyVector };
When using structured binding, we must use auto
rather than specifying the variable types.
In C++, enums are implemented in the following way:
enum class DamageType {
Fire,
Frost,
Arcane
}
Access to the values is through the scope resolution operator:
DamageType Weakness{DamageType::Fire};
We can add a using enum
statement if we want to reduce the need to qualify the enum name within the scope where the using
statement is in effect:
using enum DamageType;
DamageType Weakness{Fire};
Classes, structs, and enums are how we create custom types in C++. They allow you to encapsulate related data and functionality into reusable and modular components. Key takeaways:
A crash tour on how we can create custom types in C++ using classes, structs and enums
Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games