Module 9.3

C++ Capstone Projects

Put your C++ skills to the test by building real-world applications! Create a fully-functional calculator, design a simple game engine, and implement a mini database system from scratch.

60 min read
Intermediate
3 Projects
What You'll Build
  • Calculator with expression parsing
  • Simple game engine architecture
  • Mini database with CRUD operations
  • File I/O and data persistence
  • Object-oriented design patterns
Contents
01

Building a Calculator Application

A calculator is a perfect first project - it combines user input, mathematical operations, error handling, and can be extended to support expression parsing. Let's build one step by step!

Project Overview

We'll build a calculator that evolves from simple operations to parsing mathematical expressions. This project demonstrates operator overloading, class design, and the strategy pattern.

Level 1: Basic Ops

Simple arithmetic with +, -, *, / operations and input validation.

Level 2: Expression Parser

Parse and evaluate expressions like "3 + 4 * 2" with operator precedence.

Level 3: History

Track calculation history and support variables for storing results.

Level 1: Basic Calculator

Let's start with a simple calculator class that performs basic operations. We'll use good OOP practices from the beginning.

// calculator.hpp
#ifndef CALCULATOR_HPP
#define CALCULATOR_HPP

#include <string>
#include <stdexcept>

class Calculator {
public:
    // Basic arithmetic operations
    double add(double a, double b) const { return a + b; }
    double subtract(double a, double b) const { return a - b; }
    double multiply(double a, double b) const { return a * b; }
    
    double divide(double a, double b) const {
        if (b == 0) {
            throw std::invalid_argument("Division by zero!");
        }
        return a / b;
    }

Calculator Class Foundation: We start with a clean header file using include guards (#ifndef). The basic operations are marked const since they don't modify the object's state. Notice how divide() checks for division by zero and throws an exception - always validate inputs! This is defensive programming in action.

    // Perform operation based on operator symbol
    double calculate(double a, char op, double b) const {
        switch (op) {
            case '+': return add(a, b);
            case '-': return subtract(a, b);
            case '*': return multiply(a, b);
            case '/': return divide(a, b);
            default:
                throw std::invalid_argument("Unknown operator: " + std::string(1, op));
        }
    }
};

#endif // CALCULATOR_HPP

Unified Calculate Method: The calculate() method is a "dispatcher" - it takes an operator character and routes to the appropriate operation. The switch statement handles all operators, and the default case catches invalid operators. Note how we convert the char to a string for the error message using std::string(1, op).

// main.cpp
#include <iostream>
#include "calculator.hpp"

int main() {
    Calculator calc;
    double num1, num2;
    char op;
    
    std::cout << "=== Simple Calculator ===" << std::endl;
    std::cout << "Enter expression (e.g., 5 + 3): ";
    
    while (std::cin >> num1 >> op >> num2) {
        try {
            double result = calc.calculate(num1, op, num2);
            std::cout << "Result: " << result << std::endl;
        } catch (const std::exception& e) {
            std::cout << "Error: " << e.what() << std::endl;
        }
        std::cout << "\nEnter expression (or Ctrl+C to exit): ";
    }
    
    return 0;
}

User Interface: The main program creates a Calculator object and runs a read-eval-print loop (REPL). The std::cin >> num1 >> op >> num2 reads space-separated input. The try-catch block handles any errors gracefully, showing the user what went wrong instead of crashing. This pattern is essential for user-facing applications!

Level 2: Expression Parser

Now let's add the ability to parse full mathematical expressions with proper operator precedence. We'll implement a simple recursive descent parser.

// expression_parser.hpp
#ifndef EXPRESSION_PARSER_HPP
#define EXPRESSION_PARSER_HPP

#include <string>
#include <sstream>
#include <cctype>
#include <stdexcept>

class ExpressionParser {
private:
    std::string expr;
    size_t pos = 0;
    
    char peek() const {
        while (pos < expr.size() && isspace(expr[pos])) {
            // Skip but don't modify pos in const method
        }
        return pos < expr.size() ? expr[pos] : '\0';
    }
    
    char get() {
        while (pos < expr.size() && isspace(expr[pos])) pos++;
        return pos < expr.size() ? expr[pos++] : '\0';
    }

Parser Foundation: The expression parser tracks its position in the input string. peek() looks at the next character without consuming it (useful for lookahead), while get() returns the next character and advances the position. Both skip whitespace automatically. The '\0' return signals end-of-input.

    double parseNumber() {
        while (pos < expr.size() && isspace(expr[pos])) pos++;
        
        size_t start = pos;
        bool hasDecimal = false;
        
        // Handle negative numbers
        if (expr[pos] == '-') pos++;
        
        while (pos < expr.size() && (isdigit(expr[pos]) || expr[pos] == '.')) {
            if (expr[pos] == '.') {
                if (hasDecimal) throw std::runtime_error("Invalid number format");
                hasDecimal = true;
            }
            pos++;
        }
        
        if (pos == start || (pos == start + 1 && expr[start] == '-')) {
            throw std::runtime_error("Expected a number");
        }
        
        return std::stod(expr.substr(start, pos - start));
    }

Number Parsing: This method extracts a number from the expression. It handles negative numbers, decimal points (checking for duplicates), and uses std::stod() for conversion. The error checking ensures we don't accept invalid inputs like ".." or just "-". Building robust parsers requires handling edge cases!

    // Handle + and - (lowest precedence)
    double parseExpression() {
        double left = parseTerm();
        
        while (true) {
            char op = peek();
            if (op == '+' || op == '-') {
                get();  // consume operator
                double right = parseTerm();
                left = (op == '+') ? left + right : left - right;
            } else {
                break;
            }
        }
        return left;
    }
    
    // Handle * and / (higher precedence)
    double parseTerm() {
        double left = parseFactor();
        
        while (true) {
            char op = peek();
            if (op == '*' || op == '/') {
                get();  // consume operator
                double right = parseFactor();
                if (op == '/') {
                    if (right == 0) throw std::runtime_error("Division by zero");
                    left = left / right;
                } else {
                    left = left * right;
                }
            } else {
                break;
            }
        }
        return left;
    }

Operator Precedence via Recursion: This is the heart of the recursive descent parser! parseExpression() handles + and -, while parseTerm() handles * and /. By calling parseTerm() from parseExpression(), multiplication and division are evaluated FIRST (higher precedence). The while loops handle chains of same-precedence operators left-to-right.

    // Handle numbers and parentheses (highest precedence)
    double parseFactor() {
        char c = peek();
        
        // Handle parentheses
        if (c == '(') {
            get();  // consume '('
            double result = parseExpression();  // recursive!
            if (get() != ')') {
                throw std::runtime_error("Missing closing parenthesis");
            }
            return result;
        }
        
        // It's a number
        return parseNumber();
    }

public:
    double evaluate(const std::string& expression) {
        expr = expression;
        pos = 0;
        double result = parseExpression();
        
        // Check for leftover characters
        while (pos < expr.size() && isspace(expr[pos])) pos++;
        if (pos != expr.size()) {
            throw std::runtime_error("Unexpected character: " + std::string(1, expr[pos]));
        }
        
        return result;
    }
};

#endif // EXPRESSION_PARSER_HPP

Parentheses and Recursion: parseFactor() handles the highest-precedence elements: numbers and parentheses. When we see '(', we recursively call parseExpression() - this allows nested expressions of any depth! The evaluate() method is the public interface, initializing state and checking for complete consumption of input.

Try It: The expression "3 + 4 * 2" returns 11 (not 14), because * has higher precedence. The expression "(3 + 4) * 2" returns 14 because parentheses override precedence!

Level 3: Adding History

Let's add calculation history using STL containers. Users can view past calculations and even use the "ans" variable to reference the last result.

// calculator_with_history.hpp
#include <vector>
#include <string>
#include <map>
#include "expression_parser.hpp"

class AdvancedCalculator {
private:
    ExpressionParser parser;
    std::vector<std::pair<std::string, double>> history;
    std::map<std::string, double> variables;
    double lastResult = 0;

public:
    double calculate(const std::string& expression) {
        // Replace 'ans' with last result
        std::string processedExpr = expression;
        size_t pos;
        while ((pos = processedExpr.find("ans")) != std::string::npos) {
            processedExpr.replace(pos, 3, std::to_string(lastResult));
        }
        
        // Evaluate and store
        double result = parser.evaluate(processedExpr);
        history.push_back({expression, result});
        lastResult = result;
        
        return result;
    }
    
    void showHistory() const {
        std::cout << "\n=== Calculation History ===" << std::endl;
        for (size_t i = 0; i < history.size(); i++) {
            std::cout << i + 1 << ". " << history[i].first 
                      << " = " << history[i].second << std::endl;
        }
    }
    
    void clearHistory() {
        history.clear();
        std::cout << "History cleared." << std::endl;
    }
};

History with STL Containers: We use std::vector to store history as expression-result pairs, and std::map for future variable support. The "ans" keyword is replaced with the last result before evaluation, allowing chains like "ans * 2". This demonstrates practical use of STL containers in real applications!

02

Creating a Simple Game Engine

Game engines are fascinating applications of OOP principles. We'll build a simple text-based game engine that demonstrates the game loop, entity system, and event handling.

Game Engine Architecture

Our game engine follows the Entity-Component pattern and includes a proper game loop that separates update logic from rendering.

Design Pattern

Game Loop

The game loop is the heartbeat of every game. It continuously cycles through three phases: processing input, updating game state, and rendering the result. This loop runs until the game ends.

The cycle: Input → Update → Render → Repeat

// entity.hpp
#ifndef ENTITY_HPP
#define ENTITY_HPP

#include <string>
#include <iostream>

class Entity {
protected:
    std::string name;
    int x, y;           // Position
    char symbol;        // Visual representation
    bool active = true;

public:
    Entity(const std::string& name, int x, int y, char symbol)
        : name(name), x(x), y(y), symbol(symbol) {}
    
    virtual ~Entity() = default;
    
    // Pure virtual - must be implemented by subclasses
    virtual void update() = 0;
    virtual void render() const {
        std::cout << symbol << " " << name << " at (" << x << ", " << y << ")";
    }
    
    // Getters
    int getX() const { return x; }
    int getY() const { return y; }
    bool isActive() const { return active; }
    const std::string& getName() const { return name; }
    
    void deactivate() { active = false; }
};

#endif // ENTITY_HPP

Entity Base Class: Every object in our game (player, enemies, items) inherits from Entity. It has position (x, y), a visual symbol, and an active state. The update() method is pure virtual, forcing subclasses to define their behavior. Notice virtual ~Entity() = default - this ensures proper cleanup when deleting through base pointers.

// player.hpp
#include "entity.hpp"

class Player : public Entity {
private:
    int health = 100;
    int score = 0;

public:
    Player(const std::string& name, int x, int y)
        : Entity(name, x, y, '@') {}  // '@' is classic roguelike player symbol
    
    void update() override {
        // Player update logic (health regen, etc.)
        if (health < 100) health++;
    }
    
    void render() const override {
        Entity::render();
        std::cout << " | HP: " << health << " | Score: " << score;
    }
    
    void move(int dx, int dy) {
        x += dx;
        y += dy;
    }
    
    void takeDamage(int amount) {
        health -= amount;
        if (health <= 0) {
            health = 0;
            deactivate();
        }
    }
    
    void addScore(int points) { score += points; }
    int getHealth() const { return health; }
    int getScore() const { return score; }
};

Player Entity: The Player class extends Entity with health and score. It overrides both update() (slow health regeneration) and render() (adds stats display). The move() method takes deltas (changes) rather than absolute positions - this makes input handling cleaner. When health reaches 0, the player is deactivated.

// enemy.hpp
#include "entity.hpp"
#include <cstdlib>

class Enemy : public Entity {
private:
    int damage = 10;
    int moveCounter = 0;

public:
    Enemy(const std::string& name, int x, int y)
        : Entity(name, x, y, 'E') {}
    
    void update() override {
        // Simple AI: move randomly every 3 updates
        moveCounter++;
        if (moveCounter >= 3) {
            moveCounter = 0;
            int dx = (rand() % 3) - 1;  // -1, 0, or 1
            int dy = (rand() % 3) - 1;
            x += dx;
            y += dy;
        }
    }
    
    void render() const override {
        Entity::render();
        std::cout << " [Hostile]";
    }
    
    int getDamage() const { return damage; }
};

Enemy with Simple AI: Enemies have their own update logic - here, a simple random movement every 3 frames. This shows how different entities can have completely different behaviors while sharing the same interface. The moveCounter creates a delay, making enemies slower than the player. Real games would have more sophisticated AI!

// game.hpp
#include <vector>
#include <memory>
#include <algorithm>
#include "player.hpp"
#include "enemy.hpp"

class Game {
private:
    std::vector<std::unique_ptr<Entity>> entities;
    Player* player = nullptr;  // Non-owning pointer for quick access
    bool running = true;
    int mapWidth = 20;
    int mapHeight = 10;

public:
    void init() {
        // Create player
        auto playerPtr = std::make_unique<Player>("Hero", 10, 5);
        player = playerPtr.get();  // Store raw pointer before moving
        entities.push_back(std::move(playerPtr));
        
        // Spawn some enemies
        entities.push_back(std::make_unique<Enemy>("Goblin", 5, 3));
        entities.push_back(std::make_unique<Enemy>("Orc", 15, 7));
        entities.push_back(std::make_unique<Enemy>("Troll", 8, 8));
        
        std::cout << "Game initialized with " << entities.size() 
                  << " entities.\n";
    }

Game Manager with Smart Pointers: The Game class owns all entities via unique_ptr. We store a raw pointer to the player for quick access (safe because Game owns the player's lifetime). Using std::make_unique and std::move demonstrates modern C++ memory management. No manual delete needed!

    void processInput() {
        char input;
        std::cout << "\nMove (WASD) or Q to quit: ";
        std::cin >> input;
        
        switch (tolower(input)) {
            case 'w': player->move(0, -1); break;  // Up
            case 's': player->move(0, 1); break;   // Down
            case 'a': player->move(-1, 0); break;  // Left
            case 'd': player->move(1, 0); break;   // Right
            case 'q': running = false; break;
        }
    }
    
    void update() {
        // Update all entities
        for (auto& entity : entities) {
            if (entity->isActive()) {
                entity->update();
            }
        }
        
        // Check collisions (simple version)
        checkCollisions();
        
        // Remove inactive entities
        entities.erase(
            std::remove_if(entities.begin(), entities.end(),
                [](const auto& e) { return !e->isActive(); }),
            entities.end()
        );
        
        // Check game over
        if (!player->isActive()) {
            std::cout << "\n*** GAME OVER ***\n";
            std::cout << "Final Score: " << player->getScore() << std::endl;
            running = false;
        }
    }

Input Processing and Update: processInput() handles WASD movement. update() iterates through all entities, calling their update methods polymorphically. The remove-erase idiom with std::remove_if and a lambda cleans up dead entities. This shows C++ STL algorithms working with smart pointers beautifully!

    void checkCollisions() {
        for (auto& entity : entities) {
            if (entity.get() == player || !entity->isActive()) continue;
            
            // Simple collision: same position
            if (entity->getX() == player->getX() && 
                entity->getY() == player->getY()) {
                
                // It's an enemy
                Enemy* enemy = dynamic_cast<Enemy*>(entity.get());
                if (enemy) {
                    player->takeDamage(enemy->getDamage());
                    player->addScore(50);  // Score for defeating
                    enemy->deactivate();
                    std::cout << "Combat! " << enemy->getName() 
                              << " defeated! (-" << enemy->getDamage() << " HP)\n";
                }
            }
        }
    }
    
    void render() const {
        std::cout << "\n=== Game State ===" << std::endl;
        for (const auto& entity : entities) {
            if (entity->isActive()) {
                entity->render();
                std::cout << std::endl;
            }
        }
    }
    
    void run() {
        init();
        while (running) {
            render();
            processInput();
            update();
        }
    }
};

Collision, Render, and Run: checkCollisions() uses dynamic_cast to safely check if an entity is an Enemy - this is runtime type checking. render() calls each entity's render method polymorphically. Finally, run() implements the classic game loop: render → input → update → repeat. This is the same structure used by professional game engines!

03

Implementing a Mini Database System

Build a simple database that supports Create, Read, Update, Delete (CRUD) operations with file persistence. This project showcases file I/O, serialization, and data structures.

Database Design

Our mini database stores records with multiple fields, supports basic queries, and persists data to disk. It's like a simplified version of SQLite!

// database.hpp
#ifndef DATABASE_HPP
#define DATABASE_HPP

#include <string>
#include <vector>
#include <map>
#include <fstream>
#include <sstream>
#include <iostream>
#include <algorithm>
#include <iomanip>

// A Record is a collection of field-value pairs
using Record = std::map<std::string, std::string>;

class Database {
private:
    std::string name;
    std::vector<std::string> columns;  // Column names
    std::vector<Record> records;       // All records
    int nextId = 1;
    
    static const char DELIMITER = '|';
    static const char FIELD_SEP = ':';

public:
    Database(const std::string& name, const std::vector<std::string>& cols)
        : name(name), columns(cols) {
        // Ensure 'id' is always first column
        if (columns.empty() || columns[0] != "id") {
            columns.insert(columns.begin(), "id");
        }
    }

Database Structure: We use std::map<string, string> for flexible records where each record stores field-value pairs. The columns vector defines the schema. Every record automatically gets an 'id' field. The delimiter constants will be used for file serialization - using constants makes the code easier to maintain.

    // CREATE - Insert a new record
    int insert(const Record& data) {
        Record record = data;
        record["id"] = std::to_string(nextId++);
        
        // Validate: only allow defined columns
        for (const auto& [key, value] : record) {
            if (key != "id" && 
                std::find(columns.begin(), columns.end(), key) == columns.end()) {
                std::cerr << "Warning: Unknown column '" << key << "' ignored.\n";
            }
        }
        
        records.push_back(record);
        return std::stoi(record["id"]);
    }
    
    // READ - Find records matching criteria
    std::vector<Record> select(const std::string& field = "", 
                                const std::string& value = "") const {
        if (field.empty()) {
            return records;  // Return all
        }
        
        std::vector<Record> results;
        for (const auto& record : records) {
            auto it = record.find(field);
            if (it != record.end() && it->second == value) {
                results.push_back(record);
            }
        }
        return results;
    }

Create and Read Operations: insert() automatically assigns an ID and validates columns. The structured binding [key, value] (C++17) makes iteration clean. select() returns all records if no filter is given, or filters by field-value match. Real databases use indexes for speed, but this linear search is fine for small datasets.

    // UPDATE - Modify existing record
    bool update(int id, const Record& newData) {
        std::string idStr = std::to_string(id);
        
        for (auto& record : records) {
            if (record["id"] == idStr) {
                for (const auto& [key, value] : newData) {
                    if (key != "id") {  // Don't allow changing ID
                        record[key] = value;
                    }
                }
                return true;
            }
        }
        return false;  // Record not found
    }
    
    // DELETE - Remove a record
    bool remove(int id) {
        std::string idStr = std::to_string(id);
        
        auto it = std::remove_if(records.begin(), records.end(),
            [&idStr](const Record& r) {
                auto found = r.find("id");
                return found != r.end() && found->second == idStr;
            });
        
        if (it != records.end()) {
            records.erase(it, records.end());
            return true;
        }
        return false;
    }

Update and Delete: update() finds the record by ID and modifies fields (but protects the ID from being changed). remove() uses the erase-remove idiom with a lambda that captures idStr by reference. The lambda checks if each record's ID matches the target. Both return bool to indicate success.

    // Save database to file
    bool save(const std::string& filename) const {
        std::ofstream file(filename);
        if (!file) {
            std::cerr << "Error: Cannot open file for writing.\n";
            return false;
        }
        
        // Write header (column names)
        for (size_t i = 0; i < columns.size(); i++) {
            file << columns[i];
            if (i < columns.size() - 1) file << DELIMITER;
        }
        file << "\n";
        
        // Write records
        for (const auto& record : records) {
            for (size_t i = 0; i < columns.size(); i++) {
                auto it = record.find(columns[i]);
                if (it != record.end()) {
                    file << it->second;
                }
                if (i < columns.size() - 1) file << DELIMITER;
            }
            file << "\n";
        }
        
        file.close();
        std::cout << "Database saved to " << filename << std::endl;
        return true;
    }

Saving to File: We serialize the database in CSV-like format. First line is the header (column names), followed by data rows. Using a delimiter character (|) separates fields. We write values in column order to ensure consistent format. Error checking with if (!file) handles file opening failures gracefully.

    // Load database from file
    bool load(const std::string& filename) {
        std::ifstream file(filename);
        if (!file) {
            std::cerr << "Error: Cannot open file for reading.\n";
            return false;
        }
        
        records.clear();
        std::string line;
        
        // Read header
        if (std::getline(file, line)) {
            columns.clear();
            std::stringstream ss(line);
            std::string col;
            while (std::getline(ss, col, DELIMITER)) {
                columns.push_back(col);
            }
        }
        
        // Read records
        while (std::getline(file, line)) {
            if (line.empty()) continue;
            
            Record record;
            std::stringstream ss(line);
            std::string value;
            size_t colIndex = 0;
            
            while (std::getline(ss, value, DELIMITER) && colIndex < columns.size()) {
                record[columns[colIndex++]] = value;
            }
            
            records.push_back(record);
            
            // Update nextId
            if (record.count("id")) {
                int id = std::stoi(record["id"]);
                if (id >= nextId) nextId = id + 1;
            }
        }
        
        std::cout << "Loaded " << records.size() << " records from " << filename << std::endl;
        return true;
    }

Loading from File: The load function reverses the save process. We read the header first to restore column names, then parse each line into records. std::getline with the delimiter parameter splits fields. Critically, we update nextId to be higher than any loaded ID, preventing ID collisions for new inserts.

    // Pretty print the database
    void display() const {
        if (records.empty()) {
            std::cout << "Database is empty.\n";
            return;
        }
        
        // Calculate column widths
        std::map<std::string, size_t> widths;
        for (const auto& col : columns) {
            widths[col] = col.length();
        }
        for (const auto& record : records) {
            for (const auto& col : columns) {
                auto it = record.find(col);
                if (it != record.end()) {
                    widths[col] = std::max(widths[col], it->second.length());
                }
            }
        }
        
        // Print header
        std::cout << "\n";
        for (const auto& col : columns) {
            std::cout << std::left << std::setw(widths[col] + 2) << col;
        }
        std::cout << "\n" << std::string(60, '-') << "\n";
        
        // Print records
        for (const auto& record : records) {
            for (const auto& col : columns) {
                auto it = record.find(col);
                std::string value = (it != record.end()) ? it->second : "";
                std::cout << std::left << std::setw(widths[col] + 2) << value;
            }
            std::cout << "\n";
        }
        std::cout << "\nTotal: " << records.size() << " records\n";
    }
    
    size_t count() const { return records.size(); }
};

#endif // DATABASE_HPP

Pretty Printing: Good UX matters even for console apps! We first calculate the maximum width needed for each column (checking both header and data), then use std::setw and std::left for aligned output. This creates a clean, readable table format. The <iomanip> header provides these formatting manipulators.

Example Usage

// main.cpp - Database Example
#include "database.hpp"

int main() {
    // Create a student database
    Database db("students", {"name", "age", "grade", "major"});
    
    // Insert some records
    db.insert({{"name", "Alice"}, {"age", "20"}, {"grade", "A"}, {"major", "CS"}});
    db.insert({{"name", "Bob"}, {"age", "22"}, {"grade", "B"}, {"major", "Math"}});
    db.insert({{"name", "Carol"}, {"age", "21"}, {"grade", "A"}, {"major", "CS"}});
    
    std::cout << "=== Initial Database ===" << std::endl;
    db.display();
    
    // Query: Find all CS majors
    std::cout << "\n=== CS Majors ===" << std::endl;
    auto csStudents = db.select("major", "CS");
    for (const auto& student : csStudents) {
        std::cout << student.at("name") << " - Grade: " << student.at("grade") << std::endl;
    }
    
    // Update Bob's grade
    db.update(2, {{"grade", "A"}});
    
    // Save to file
    db.save("students.db");
    
    // Create new database and load
    Database db2("students_copy", {"name", "age", "grade", "major"});
    db2.load("students.db");
    
    std::cout << "\n=== Loaded Database ===" << std::endl;
    db2.display();
    
    return 0;
}
Extend It: Try adding support for multiple data types, indexing for faster queries, SQL-like query syntax parsing, or transaction support with rollback!

Key Takeaways

Modular Design

Split projects into separate header files (.hpp) and implementation files. Use include guards to prevent multiple inclusion.

Error Handling

Use exceptions for error handling, validate all inputs, and provide meaningful error messages to users.

Smart Pointers

Use unique_ptr for ownership and raw pointers for non-owning access. Let RAII manage memory automatically.

OOP Patterns

Use inheritance and polymorphism for extensibility. Virtual functions enable runtime behavior selection.

File Persistence

Use fstream for file I/O. Define clear serialization formats and always check for file operation errors.

STL Mastery

Leverage STL containers (vector, map) and algorithms (remove_if, find). They're tested, optimized, and reliable.

Knowledge Check

Quick Quiz

Test what you've learned about C++ project development

1 Why do we use include guards (#ifndef) in header files?
2 In the expression parser, why do we calculate parseTerm() before combining with + or -?
3 What is the purpose of virtual ~Entity() = default; in the Entity class?
4 Why do we store a raw pointer to the player alongside the unique_ptr in the Game class?
5 What happens if we call db.load("students.db") on an existing database with records?
6 In the database save function, why do we iterate columns in order rather than record keys?
Answer all questions to check your score