Skip to content

Classes and Objects

Classes are the core construct for object-oriented programming (OOP) in C++. They allow you to bundle state (member variables) and behavior (member functions) into self-contained, user-defined types that enforce system invariants and encapsulation.


1. Classes vs. Structs

In C++, class and struct are virtually identical under the hood. The only differences are: - class: Members and base class inheritance are private by default. - struct: Members and base class inheritance are public by default.

struct Point {
    double x; // public by default
    double y;
};

class BankAccount {
    double balance; // private by default
public:
    void deposit(double amount) { balance += amount; }
};

Guideline:

  • Use struct for passive data containers without internal logic or invariants (often called Plain Old Data, or POD).
  • Use class when the object needs to control access to its internal state to prevent invalid modifications (enforcing invariants).

2. Access Specifiers and Encapsulation

Encapsulation hides internal implementation details and protects the integrity of an object's state. C++ provides three access specifiers: - public: The member is accessible from outside the class. Represents the class's public API interface. - private: The member is only accessible from within functions belonging to the same class. Used for state variables and helper functions. - protected: The member is accessible within the class itself and any child classes inheriting from it.


3. Object Lifetime: Constructors and Destructors

Constructors

Constructors initialize the object when it is allocated in memory. They have the same name as the class and do not specify a return type. - Default Constructor: Takes no arguments. - Parameterized Constructor: Takes arguments to initialize custom values. - Delegating Constructor (C++11): A constructor can call another constructor of the same class to avoid duplicate code.

In-Class Initializers

You can specify default values directly in the variable declarations within the class header. These are overridden if a constructor initializes the variable.

class Servo {
    int pin = 18;       // In-class default initializer
    int min_pulse = 500;
    int max_pulse = 2500;
public:
    // Default constructor uses default values
    Servo() = default; 

    // Parameterized constructor delegating to default values
    Servo(int p) : pin(p) {} 

    // Delegating constructor
    Servo(int p, int min, int max) : pin(p), min_pulse(min), max_pulse(max) {}
};

Member Initializer Lists

Always use member initializer lists instead of assigning values inside the constructor body.

#include <string>

// 1. Efficient: Direct initialization of member variable
class User {
    std::string name;
public:
    User(const std::string& username) : name(username) {}
};

// 2. Inefficient: Constructs empty 'name' string first, then overwrites it via assignment
class User2 {
    std::string name;
public:
    User2(const std::string& username) {
        name = username; 
    }
};

[!WARNING] Initialization Order Warning: Member variables are initialized in the order they are declared in the class header, not the order they appear in the member initializer list. To avoid subtle bugs, always write your initializer list in the same order as the declarations.


4. Constant Member Functions

If a member function only reads data but does not modify the object's state, mark it as const. The compiler will block any modifications inside the function, and it allows the function to be called on const objects.

1
2
3
4
5
6
7
8
9
class Sensor {
    double reading;
public:
    // Read-only method
    double get_reading() const {
        // reading = 0.0; // Compile Error! Cannot modify inside const method.
        return reading;
    }
};

5. Static Members

static members are shared across all instances of a class. They belong to the class namespace itself, rather than any individual instance object.

Static Member Variables

Since they are shared, they must be defined in a single source file outside the class declaration, or declared as inline static in C++17 to initialize them directly in the header.

1
2
3
4
5
6
7
class Logger {
public:
    // C++17 inline static: initialized inside the header file
    inline static int log_count = 0; 

    Logger() { log_count++; }
};

Static Member Functions

These functions do not receive the implicit this pointer and can only access other static variables or functions. They can be called without instantiating the class.

1
2
3
4
5
6
7
8
class Math {
public:
    static double calculate_pi() { return 3.14159265; }
};

int main() {
    double pi = Math::calculate_pi(); // No object needed
}

6. Managing Resources: The Rule of Three and Five

If your class allocates raw heap memory or holds system resources directly, you must manage how it is copied or moved.

The Rule of Three (Legacy C++98)

If you define a custom Destructor, Copy Constructor, or Copy Assignment Operator, you almost certainly need to define all three to manage deep copies.

#include <algorithm>

class Buffer {
    size_t size;
    int* data;
public:
    Buffer(size_t s) : size(s), data(new int[s]) {}

    // 1. Destructor (Releases memory)
    ~Buffer() { delete[] data; }

    // 2. Copy Constructor (Deep copy logic)
    Buffer(const Buffer& other) : size(other.size), data(new int[other.size]) {
        std::copy(other.data, other.data + size, data);
    }

    // 3. Copy Assignment Operator (Protects against self-assignment)
    Buffer& operator=(const Buffer& other) {
        if (this != &other) {
            delete[] data;
            size = other.size;
            data = new int[size];
            std::copy(other.data, other.data + size, data);
        }
        return *this;
    }
};

The Rule of Five (Modern C++11)

With the introduction of move semantics, the Rule of Three is expanded to the Rule of Five. To prevent expensive copy operations, you should also implement the Move Constructor and Move Assignment Operator.

#include <algorithm>

class Buffer {
    size_t size;
    int* data;
public:
    Buffer(size_t s) : size(s), data(new int[s]) {}
    ~Buffer() { delete[] data; }
    Buffer(const Buffer& other) : size(other.size), data(new int[other.size]) {
        std::copy(other.data, other.data + size, data);
    }
    Buffer& operator=(const Buffer& other) {
        if (this != &other) {
            delete[] data;
            size = other.size;
            data = new int[size];
            std::copy(other.data, other.data + size, data);
        }
        return *this;
    }

    // 4. Move Constructor (Transfer pointer, leave source empty)
    Buffer(Buffer&& other) noexcept : size(other.size), data(other.data) {
        other.size = 0;
        other.data = nullptr; // Reset source object
    }

    // 5. Move Assignment Operator
    Buffer& operator=(Buffer&& other) noexcept {
        if (this != &other) {
            delete[] data; // Free existing resource

            size = other.size;
            data = other.data; // Take over resource

            other.size = 0;
            other.data = nullptr; // Reset source object
        }
        return *this;
    }
};

The Rule of Zero

In Modern C++, you should design your classes so that they do not handle raw resources directly. By using smart pointers (std::unique_ptr) and standard library containers (std::vector, std::string), the compiler automatically generates the correct copy/move constructors for you, eliminating the need to write them yourself.