C++ classes objects OOP tutorial

C++ Classes & Objects: Complete OOP Guide

Back to C++ RoadmapC++ Programming Course • 65 Lessons

What Are Classes?

A class is a blueprint for creating objects. It bundles data (member variables) and behavior (member functions) into a single unit, with access control to protect internal state. This is the core of object-oriented programming (OOP) in C++.

While structs default to public access, classes default to private — enforcing encapsulation. The outside world interacts with an object through its public interface, while the internal implementation stays hidden. This separation lets you change how a class works internally without breaking code that uses it.

Defining a Class

#include <iostream>
#include <string>
using namespace std;

class Dog {
private:
    string name;
    int age;

public:
    // Constructor
    Dog(string n, int a) : name(n), age(a) {}

    // Member functions
    void bark() const {
        cout << name << " says: Woof!" << endl;
    }

    void info() const {
        cout << name << ", age " << age << endl;
    }
};

int main() {
    Dog rex("Rex", 3);
    Dog bella("Bella", 5);

    rex.bark();    // Rex says: Woof!
    bella.info();  // Bella, age 5
    return 0;
}

Access Specifiers

#include <iostream>
using namespace std;

class Account {
public:     // Accessible from anywhere
    string owner;

    void display() const {
        cout << owner << ": $" << balance << endl;
    }

protected:  // Accessible in this class and subclasses
    string account_type = "Savings";

private:    // Accessible only in this class
    double balance = 0;

public:
    // Public interface to access private data
    void deposit(double amount) {
        if (amount > 0) balance += amount;
    }

    double get_balance() const { return balance; }
};

int main() {
    Account acc;
    acc.owner = "Alice";        // OK — public
    acc.deposit(1000);          // OK — public method
    // acc.balance = 999;       // ERROR — private
    // acc.account_type;        // ERROR — protected
    acc.display();              // Alice: $1000
    return 0;
}

Constructors

#include <iostream>
#include <string>
using namespace std;

class Player {
    string name;
    int health;
    int level;

public:
    // Default constructor
    Player() : name("Unknown"), health(100), level(1) {
        cout << "Default player created" << endl;
    }

    // Parameterized constructor
    Player(string n, int h, int l) : name(n), health(h), level(l) {
        cout << name << " created (L" << level << ")" << endl;
    }

    // Constructor with defaults
    Player(string n) : name(n), health(100), level(1) {
        cout << name << " created with defaults" << endl;
    }

    // Copy constructor
    Player(const Player& other)
        : name(other.name + " (copy)"), health(other.health), level(other.level) {
        cout << name << " copied" << endl;
    }

    void info() const {
        cout << name << " | HP:" << health << " | Lv:" << level << endl;
    }
};

int main() {
    Player p1;                     // Default constructor
    Player p2("Hero", 150, 10);   // Parameterized
    Player p3("Mage");            // Single-arg constructor
    Player p4 = p2;               // Copy constructor

    p1.info();
    p2.info();
    p3.info();
    p4.info();
    return 0;
}

Destructors

A destructor runs when an object goes out of scope or is deleted. It cleans up resources (memory, files, connections).

#include <iostream>
#include <string>
using namespace std;

class Logger {
    string name;
public:
    Logger(string n) : name(n) {
        cout << "[" << name << "] Logger created" << endl;
    }

    ~Logger() {
        cout << "[" << name << "] Logger destroyed" << endl;
    }

    void log(string msg) const {
        cout << "[" << name << "] " << msg << endl;
    }
};

int main() {
    Logger app("App");
    app.log("Starting...");

    {
        Logger db("DB");
        db.log("Connected");
    }  // db destroyed here

    app.log("Continuing...");
    return 0;
}  // app destroyed here
// Output:
// [App] Logger created
// [App] Starting...
// [DB] Logger created
// [DB] Connected
// [DB] Logger destroyed
// [App] Continuing...
// [App] Logger destroyed

Getters and Setters

#include <iostream>
#include <string>
using namespace std;

class Temperature {
    double celsius;

public:
    Temperature(double c = 0) : celsius(c) {}

    // Getter
    double get_celsius() const { return celsius; }
    double get_fahrenheit() const { return celsius * 9.0 / 5.0 + 32; }
    double get_kelvin() const { return celsius + 273.15; }

    // Setter with validation
    void set_celsius(double c) {
        if (c < -273.15) {
            cout << "Error: below absolute zero!" << endl;
            return;
        }
        celsius = c;
    }

    void set_fahrenheit(double f) {
        set_celsius((f - 32) * 5.0 / 9.0);
    }
};

int main() {
    Temperature t(100);
    cout << t.get_celsius() << " C = "
         << t.get_fahrenheit() << " F = "
         << t.get_kelvin() << " K" << endl;

    t.set_fahrenheit(72);
    cout << t.get_celsius() << " C" << endl;

    t.set_celsius(-300);  // Error: below absolute zero!
    return 0;
}

const Member Functions

#include <iostream>
#include <string>
using namespace std;

class ImmutablePerson {
    string name;
    int age;
public:
    ImmutablePerson(string n, int a) : name(n), age(a) {}

    // const methods — cannot modify member variables
    string get_name() const { return name; }
    int get_age() const { return age; }

    // non-const method — can modify
    void birthday() { age++; }
};

void print_person(const ImmutablePerson& p) {
    // Can only call const methods on const references
    cout << p.get_name() << ", age " << p.get_age() << endl;
    // p.birthday();  // ERROR — can't call non-const on const ref
}

int main() {
    ImmutablePerson alice("Alice", 25);
    print_person(alice);
    alice.birthday();
    print_person(alice);
    return 0;
}

The this Pointer

this is a pointer to the current object. It is used when parameter names shadow member names, or for method chaining.

#include <iostream>
#include <string>
using namespace std;

class Builder {
    string name;
    int width = 0, height = 0;
    string color = "white";

public:
    Builder(string name) : name(name) {}

    // Method chaining using *this
    Builder& set_size(int width, int height) {
        this->width = width;    // this-> disambiguates
        this->height = height;
        return *this;
    }

    Builder& set_color(string color) {
        this->color = color;
        return *this;
    }

    void build() const {
        cout << "Built " << name << ": " << width << "x" << height
             << " " << color << endl;
    }
};

int main() {
    Builder("Window")
        .set_size(800, 600)
        .set_color("blue")
        .build();
    // Output: Built Window: 800x600 blue
    return 0;
}

Static Members

Static members belong to the class itself, not to any individual object. They are shared across all instances.

#include <iostream>
using namespace std;

class Counter {
    static int total;  // Shared across all instances
    int id;

public:
    Counter() : id(++total) {
        cout << "Counter #" << id << " created" << endl;
    }

    ~Counter() {
        cout << "Counter #" << id << " destroyed" << endl;
        total--;
    }

    static int get_total() { return total; }
    int get_id() const { return id; }
};

// Must define static members outside the class
int Counter::total = 0;

int main() {
    cout << "Total: " << Counter::get_total() << endl;  // 0

    Counter a, b, c;
    cout << "Total: " << Counter::get_total() << endl;  // 3

    {
        Counter d;
        cout << "Total: " << Counter::get_total() << endl;  // 4
    }  // d destroyed

    cout << "Total: " << Counter::get_total() << endl;  // 3
    return 0;
}

Operator Overloading

#include <iostream>
using namespace std;

class Vec2 {
public:
    double x, y;

    Vec2(double x = 0, double y = 0) : x(x), y(y) {}

    // Arithmetic operators
    Vec2 operator+(const Vec2& other) const {
        return {x + other.x, y + other.y};
    }

    Vec2 operator-(const Vec2& other) const {
        return {x - other.x, y - other.y};
    }

    Vec2 operator*(double scalar) const {
        return {x * scalar, y * scalar};
    }

    // Comparison
    bool operator==(const Vec2& other) const {
        return x == other.x && y == other.y;
    }

    // Stream output
    friend ostream& operator<<(ostream& os, const Vec2& v) {
        os << "(" << v.x << ", " << v.y << ")";
        return os;
    }
};

int main() {
    Vec2 a(3, 4), b(1, 2);
    cout << "a + b = " << (a + b) << endl;    // (4, 6)
    cout << "a - b = " << (a - b) << endl;    // (2, 2)
    cout << "a * 3 = " << (a * 3) << endl;    // (9, 12)
    cout << "a == b: " << (a == b) << endl;   // 0
    return 0;
}

Friend Functions

#include <iostream>
using namespace std;

class Box {
    double width, height;

public:
    Box(double w, double h) : width(w), height(h) {}

    // Friend function — has access to private members
    friend double area(const Box& b);
    friend bool compare(const Box& a, const Box& b);
};

double area(const Box& b) {
    return b.width * b.height;  // Can access private members
}

bool compare(const Box& a, const Box& b) {
    return area(a) > area(b);
}

int main() {
    Box small_box(3, 4);
    Box big_box(10, 8);

    cout << "Area: " << area(small_box) << endl;    // 12
    cout << "Area: " << area(big_box) << endl;      // 80
    cout << "Big is bigger: " << compare(big_box, small_box) << endl;
    return 0;
}

Separating Header and Source

// ===== player.h =====
#ifndef PLAYER_H
#define PLAYER_H

#include <string>

class Player {
    std::string name;
    int health;
public:
    Player(std::string n, int h);
    void take_damage(int amount);
    void heal(int amount);
    void print() const;
    bool is_alive() const;
};

#endif

// ===== player.cpp =====
#include "player.h"
#include <iostream>
using namespace std;

Player::Player(string n, int h) : name(n), health(h) {}

void Player::take_damage(int amount) {
    health -= amount;
    if (health < 0) health = 0;
}

void Player::heal(int amount) {
    health += amount;
    if (health > 100) health = 100;
}

void Player::print() const {
    cout << name << " [HP: " << health << "]" << endl;
}

bool Player::is_alive() const { return health > 0; }

// ===== main.cpp =====
// #include "player.h"
// int main() {
//     Player hero("Knight", 100);
//     hero.take_damage(30);
//     hero.print();  // Knight [HP: 70]
//     return 0;
// }

Real-World Example

#include <iostream>
#include <vector>
#include <string>
using namespace std;

class TodoList {
    struct Task {
        string text;
        bool done;
    };

    string name;
    vector<Task> tasks;

public:
    TodoList(string name) : name(name) {}

    void add(string task) {
        tasks.push_back({task, false});
    }

    void complete(int index) {
        if (index >= 0 && index < tasks.size()) {
            tasks[index].done = true;
        }
    }

    void print() const {
        cout << "=== " << name << " ===" << endl;
        for (int i = 0; i < tasks.size(); i++) {
            cout << (tasks[i].done ? "[x] " : "[ ] ")
                 << i << ". " << tasks[i].text << endl;
        }
        int done = 0;
        for (const auto& t : tasks) if (t.done) done++;
        cout << done << "/" << tasks.size() << " completed" << endl;
    }
};

int main() {
    TodoList todo("Today");
    todo.add("Learn C++ classes");
    todo.add("Write practice code");
    todo.add("Review lesson");

    todo.complete(0);
    todo.print();
    return 0;
}

Practice Exercises

Exercise 1: Create a BankAccount class with private balance, deposit/withdraw methods with validation, and a transaction history.

Exercise 2: Create a Matrix class with operator overloading for +, *, and <<.

Exercise 3: Create a String class that wraps a char* with a constructor, copy constructor, destructor, and operator+.

Exercise 4: Build a simple LinkedList class with push_front, push_back, print, and destructor.

Summary

Classes are the foundation of object-oriented C++. You learned how to define classes with access specifiers, write constructors and destructors, use getters/setters, overload operators, work with static members and friend functions, and separate declarations from definitions. The key principle is encapsulation: hide internal state behind a public interface. In the next lessons, you will build on this foundation with inheritance, polymorphism, and the Rule of Three/Five.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *