Classes, Structs and Enums

A crash tour on how we can create custom types in C++ using classes, structs and enums
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Free, Unlimited Access
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated

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:

  • those who have completed our introductory course, but want a quick review
  • those who are already familiar with programming in another language, but are new to C++
  • those who have used C++ in the past, but would benefit from a refresher

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.

Previous Course

Intro to Programming with C++

Starting from the fundamentals, become a C++ software engineer, step by step.

Screenshot from Cyberpunk 2077
Screenshot from The Witcher 3: Wild Hunt

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;

Access Specifiers

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 instances
  • protected members are only accessible to functions within the class, or any class that inherits from it. Inheritance is covered in the next section
  • private members are only accessible to functions within the class

There 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

Inheritance

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 level
  • protected 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 {};

Multiple Inheritance, Abstract Classes and Interfaces

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

Structs

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

Constructors are functions that are automatically called when we create objects from our classes and structs.

Default Constructor

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;

Custom Constructors

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

Member Initialiser Lists

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;
};

Destructors

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.

Aggregate Initialisation

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};

Structured Binding

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.

Enums

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};

Summary

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:

  • Classes and structs are similar, but class members are private by default while struct members are public
  • Access specifiers (public, protected, private) control the accessibility of class members
  • Constructors are special member functions that initialize objects, and destructors clean up resources when objects are destroyed
  • Inheritance allows classes to derive properties and behaviors from other classes
  • Enums define a set of named constants and are useful for representing a fixed set of values

Was this lesson useful?

Next Lesson

Preprocessor Directives and the Build Process

Learn the fundamentals of the C++ build process, including the roles of the preprocessor, compiler, and linker.
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated
sdl2-promo.jpg
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Free, Unlimited Access
  • 53.GPUs and Rasterization
  • 54.SDL Renderers
sdl2-promo.jpg
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Free, unlimited access

This course includes:

  • 55 Lessons
  • 100+ Code Samples
  • 91% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

Preprocessor Directives and the Build Process

Learn the fundamentals of the C++ build process, including the roles of the preprocessor, compiler, and linker.
Abstract art representing computer programming
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved