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:

Making Games with SDL

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

7d.jpg
Ryan McCombe
Ryan McCombe
Posted

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 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. Anyone looking for more thorough explanations or additional context should consider 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 which keeps our class working as intended.

C++ has three access levels by default:

  • public members are accessible 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 d; // Public
};

int main() {
  Character Player;

  // Not allowed - a is private
  Player.a; 

  // Allowed - b is public
  Player.b;
}

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

C++ supports multiple inheritance and abstract classes, both of which are covered 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 is one that 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 no longer available:

// Call custom constructor
Vector MyVector{4.f};

// Compilation error
Vector AnotherVector;

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:

struct Vector {
  // A replacement default constructor
  Vector() {
    std::cout << "Constructing!" << std::endl;
  }

  // Initialise all components to the same value
  Vector(float Value) {
    x = Value;
    y = Value;
    z = Value;
  }

  // Construct from another vector and length
  Vector(Vector OtherVector, float Length) {
    // ...
  }

  float x;
  float y;
  float z;
};

Vector A;
Vector B{1.4f};
Vector C{B, 1.f};

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.

It looks like this:

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

Was this lesson useful?

Edit History

  • — First Published

Ryan McCombe
Ryan McCombe
Posted
This lesson is part of the course:

Making Games with SDL

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

  • 25.Making Minesweeper with C++ and SDL2
  • 26.Project Setup
  • 27.GPUs and Rasterization
  • 28.SDL Renderers
DreamShaper_v7_cyberpunk_woman_playing_video_games_modest_clot_0.jpg
This lesson is part of the course:

Making Games with SDL

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

Free, unlimited access!

This course includes:

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

Preprocessor, Build Process, and Header Files

A quick guide to the build process in C++, and how we can use preprocessor directives to modify what gets compiled.
a53.jpg
Contact|Privacy Policy|Terms of Use
Copyright © 2023 - All Rights Reserved