Delving into C++

·6 min read·Graham Mace
Delving into C++

As mentioned previously, a prerequisite of the CG Spectrum Game Programming Foundations course is a basic understanding of the C++ programming language.

My programming experience has mostly been with higher-level languages. I started with Visual Basic before moving on to Java and then C#.

Of those, I’ve always found C# particularly intuitive, but I’ve also wanted to learn C++ for a long time.

Since I already had a solid understanding of the core principles, I saw little value in taking extensive notes on concepts I was already familiar with, and instead committed code to:

sawnoff-studios/game-programming-foundations

This also gives me a practical reference to return to, while letting me track my progress through the course.

Topics

The topics studied over those 12 weeks were:

  • Game Programming Concepts
  • Variables and Operators
  • Conditionals
  • Loops
  • Functions
  • Classes and Objects
  • Pointers, References & Dynamic Memory
  • Arrays
  • Inheritance & Polymorphism
  • Templates
  • Game Loop
  • Putting It All Together

This was followed by using the PlayBuffer library to practise implementing the concepts learned, committed to sawnoff-studios/playbuffer.

Through this I gained a deeper understanding of how to use C++ to create games, with cppreference.com and GeeksforGeeks proving invaluable.

Differences

While primitive types such as int, float and bool are similar to their C# counterparts, there are key differences in the standard library data structures – for me mostly in the naming rather than the implementation:

C++C# equivalentDescription
std::vectorList<T>Dynamic, resizable array
std::listLinkedList<T>Doubly linked list
std::stringStringString class
std::mapDictionary<TKey, TValue>Stores key/value pairs
std::deque(no direct equivalent)Double-ended queue

References vs Pointers

One of the main differences between references and pointers in C++ is how they handle nullability, reassignment, syntax, memory management, and flexibility.

In managed languages such as C# and Java, garbage collection handles memory automatically; in C++, memory management is manual and requires the new and delete operators.

FeatureReferencesPointers
NullabilityCannot be nullCan be null (nullptr)
ReassignmentCannot be reassignedCan be reassigned
SyntaxSimple, no dereferencingRequires * or ->
Memory managementNo manual managementRequires new/delete for heap data
FlexibilityTied to one object for its lifetimeCan point to different objects

Recap Thoughts

Code is spread out a lot across files.

Coming from higher-level languages, it felt like a pain switching between the .h file to define class members and the .cpp file to implement them. I started seeing .h files as the ‘interface’ to a class, which is different to what I’m used to.

It’s a bit of a hassle, but a necessary evil – otherwise, I found C++ remarkably similar to C#.

Alternate constructor syntax is weird.

In C#, constructors look like this:

public class Person
{
    private string _name;
    private int _age;

    public Person(string name, int age)
    {
        _name = name;
        _age = age;
    }
}

Whereas C++ initialises members in an initialiser list - with the members defined in the .h file, then assigned via what looks like a method call in the .cpp:

// Person.h
class Person
{
    std::string m_name;
    int m_age;

public:
    Person();
    Person(std::string name, int age);
};
// Person.cpp
#include "Person.h"

Person::Person()
    : m_name("Unnamed")
    , m_age(0)
{
}

Person::Person(std::string name, int age)
    : m_name(name)
    , m_age(age)
{
}

Friend is a scary concept.

The friend keyword grants a class access to another class’s private and protected members, breaking encapsulation.

In the course example, two classes were defined - Spiderman and Venom - with Venom declared a friend of Spiderman, allowing Venom::AccessSpidermanHealth() to modify Spiderman’s health without inheritance.

I found it a bit of a scary concept – no such thing exists in C# without compiler workarounds.

// Spiderman.h
#pragma once
#include <string>

class Venom; // Forward declaration

class Spiderman
{
    friend class Venom;

public:
    Spiderman();
    Spiderman(int inHealth);

private:
    std::string name;
    int health;
};
// Venom.h
#pragma once
#include "Spiderman.h"

class Venom
{
public:
    Venom();
    Venom(int inHealth);

    void AccessSpidermanHealth();

private:
    Spiderman spiderman;
};
// Venom.cpp
#include <iostream>
#include "Venom.h"

Venom::Venom() : Venom(0)
{
}

Venom::Venom(int inHealth)
{
    spiderman.health = inHealth;  // Legal only because of the friend declaration
}

void Venom::AccessSpidermanHealth()
{
    std::cout << "Accessing Spiderman health " << spiderman.health << '\n';
}

Forward declarations are interesting.

In higher-level languages, you define a class and it can instantly be referenced elsewhere.

In C++, forward declarations let you declare a type’s existence ‘ahead of time’ for the compiler:

// Player.h
class Enemy; // Forward declaration

class Player
{
public:
    static void PlayerTurn(Player& player, std::vector<Enemy>& enemies);
    static void PlayerAttack(Player& player, Enemy& enemy);
};
// Enemy.h
#include "Player.h"

class Enemy
{
public:
    static void EnemyTurn(Player& player, Enemy& enemy);
    static void EnemyAttack(Player& player, Enemy& enemy);
};

Here, Player.h references Enemy, but adding #include "Enemy.h" to Player.h would create a circular reference – each header including the other, a chicken-and-egg situation. Forward-declaring Enemy in Player.h and including the full header only where needed breaks the cycle.

The second benefit is compile times: an #include in a .h file pulls in everything from that file, including its own includes - pages and pages of code that need compiling. Forward declarations only reference what’s required.

Operator overloading can be useful.

Another scary-seeming concept: overloading existing operators such as +, == and = for custom types. It makes for intuitive, readable code, but can cause confusion if misused:

#pragma once

class Position
{
public:
    Position();
    Position(float inX, float inY);
    ~Position();

    Position operator+(const Position& other) const
    {
        return Position(X + other.X, Y + other.Y);
    }

    bool operator==(const Position& other) const
    {
        return ((X == other.X) && (Y == other.Y));
    }

    Position& operator=(const Position& other)
    {
        X = other.X;
        Y = other.Y;
        return *this;
    }

private:
    float X;
    float Y;
};

Three overloaded operators - +, == and = - allowing Positions to be added, compared, and assigned:

Position pos1(1.0f, 2.0f);
Position pos2(3.0f, 4.0f);

Position pos3 = pos1 + pos2;      // pos3.X = 4.0f, pos3.Y = 6.0f
bool areEqual = (pos1 == pos2);   // false
pos1 = pos2;                      // pos1.X = 3.0f, pos1.Y = 4.0f

Pointers aren’t that scary.

Pointers aren’t really a concept in higher-level languages, but they’re fundamental to C++. I’d always been a bit scared of them – I’d heard they can have unintended consequences – but they’re simple once you get the hang of them.

In the course example, a Car struct is allocated on the heap via a pointer, and its members accessed through the dereference operator (->):

#include <iostream>
using namespace std;

int main()
{
    struct Car
    {
        string make;
        string model;
        int year;
    };

    Car* car = new Car();

    cout << "Enter the car's make\n";
    cin >> car->make;

    cout << "Enter the car's model\n";
    cin >> car->model;

    cout << "Enter the car's year\n";
    cin >> car->year;

    cout << "You entered " << car->year << " " << car->make << " " << car->model;

    delete car;  // Don't leak!
    return 0;
}

References are interesting.

Following on from pointers, references were equally interesting – they act as an alias for an existing variable:

#include <iostream>
using namespace std;

int main()
{
    int x = 10;

    int& ref = x;  // ref is a reference (alias) to x

    cout << ref << endl;  // Prints 10

    ref = 22;             // Writing through ref modifies x itself

    cout << ref << endl;  // Prints 22
    cout << x << endl;    // Prints 22 - x and ref are the same object

    return 0;
}

Certificate

After three months of study, I officially passed this module and received my certificate on 26th June 2025.

Many thanks to Cameron Cintron for your support and encouragement throughout this part of my journey!

Onwards towards learning Unreal Engine!

Game Programming Essentials certificate