No history yet

Classes and Objects

From Scripts to Blueprints

In procedural programming, data and the functions that operate on it often live separately. This works for simple scripts, but as programs grow, it can become messy. Object-oriented programming (OOP) offers a way to bundle data and functionality together into a neat package called an object.

The blueprint for creating these objects is a class. Think of a class as a cookie cutter. The cutter itself isn't a cookie, but it defines the shape and properties of all the cookies you'll make. Each cookie you stamp out is an object—an instance of that class.

// This is a class declaration. It's the blueprint.
class Dog {
    // Member variables and functions go here
};

int main() {
    Dog myDog; // myDog is an object, or an instance of the Dog class.
    Dog neighborsDog; // This is another, separate object.
}

A class defines a new type in your program. Just as you can create variables of type int or std::string, you can create variables of your new Dog type. Each Dog object will have its own set of data, completely independent of other Dog objects.

Guarding Your Data

A core principle of good class design is protecting an object's internal data from accidental or unwanted changes. We do this using access specifiers: public and private.

  • private members can only be accessed by other member functions inside the same class. This is the default for classes and is used to hide the internal complexity.
  • public members can be accessed from anywhere outside the class. This forms the object's public interface—the controls we expose to the world.

This practice of bundling data and controlling access to it is called . It’s like a car: the steering wheel and pedals are the public interface, but the engine's timing belts and fuel injectors are private. You don't need to know how they work to drive the car, and the manufacturer doesn't want you messing with them directly.

class BankAccount {
private:
    // These are hidden from the outside world.
    double balance;

public:
    // This is the public interface.
    void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
        }
    }

    double getBalance() {
        return balance;
    }
};

Notice how the deposit function can contain logic to validate the input. This prevents code outside the class from setting the balance to a negative number, ensuring the object's state remains valid.

Birth and Death of an Object

Objects need to be set up correctly when they are created and cleaned up when they are no longer needed. C++ automates this with two special member functions: constructors and destructors.

A constructor is a function that's called automatically when an object of a class is created. It has the same name as the class and has no return type. Its job is to initialize the object's member variables to a valid starting state.

A destructor is called automatically when an object is destroyed. Its name is the class name preceded by a tilde (~), and it also has no return type. Its primary role is to free up resources that the object might have acquired during its lifetime, like dynamically allocated memory or open files.

#include <iostream>

class Tracer {
public:
    // Constructor
    Tracer() {
        std::cout << "An object is born!\n";
    }

    // Destructor
    ~Tracer() {
        std::cout << "An object says goodbye.\n";
    }
};

int main() {
    Tracer t1; // Constructor called here.
    // t1 goes out of scope at the end of main.
    // Destructor is called right before the program ends.
    return 0;
}

If you don't provide a constructor or destructor, the compiler generates a default one for you. The default constructor does nothing, and the default destructor does nothing. This is fine for simple classes, but for classes that manage resources, writing your own is essential.

Better Initialization

To give our objects starting values, we can pass arguments to the constructor. The preferred way to set member variables from these arguments is using a member initializer list. This is a more efficient way to initialize members than assigning values inside the constructor's body.

An initializer list appears between the constructor's parameter list and its body, starting with a colon.

#include <string>

class Player {
public:
    std::string username;
    int level;

    // Constructor with a member initializer list
    Player(std::string name, int startLevel) 
        : username(name), level(startLevel) 
    {
        // The body can be empty or contain other setup logic.
    }
};

int main() {
    Player p1("Gandalf", 99);
}

For some types, like const members or reference members, you must use an initializer list. It's a good habit to use it for all member initialization.

But what if a constructor parameter has the same name as a member variable? How does the compiler know which is which? This is where the comes in. The this pointer is a special pointer available inside every member function that points to the current object instance itself.

class Player {
public:
    std::string username;
    int level;

    Player(std::string username, int level) {
        // 'this->username' refers to the member variable.
        // 'username' refers to the function parameter.
        this->username = username;
        this->level = level;
    }
};

Using this resolves the ambiguity. While naming parameters differently can avoid this, explicitly using this can make code clearer, especially in complex constructors.

Quiz Questions 1/6

What is the primary advantage of object-oriented programming (OOP) compared to procedural programming?

Quiz Questions 2/6

In C++, the blueprint for creating an object is called a(n) __________, and an instance of that blueprint is called a(n) __________.

Classes and objects are the core architectural units of C++. By mastering them, you move from writing simple scripts to designing modular, maintainable, and scalable applications.