Skip to content
C++ Better Explained
Go back

Object-Oriented Programming in C++: Classes, Objects, and Constructors Explained

Edit page

Object-Oriented Programming in C++: Classes, Objects, and Constructors Explained

Introduction: Why OOP Exists—The Problem It Solves

Imagine you’re building a large program without objects. You’d have functions everywhere:

// Without OOP: scattered functions and data
float balance1 = 1000;
float balance2 = 5000;

void deposit1(float amount) { balance1 += amount; }
void deposit2(float amount) { balance2 += amount; }

void withdraw1(float amount) { balance1 -= amount; }
void withdraw2(float amount) { balance2 -= amount; }

This gets messy fast. What if you have 100 bank accounts? Or what if you need to add interest calculation, transaction history, and fraud detection? Your code becomes a tangled mess.

Object-Oriented Programming (OOP) solves this problem by grouping related data and functions together.

Instead of scattered functions and variables, you create a class that represents a bank account. The class bundles data (balance, account number) and functions (deposit, withdraw) into one organized unit. Then you create objects from that class—one for each actual bank account.

This is how real software is built. Understanding OOP is not optional in C++; it’s essential. And the good news? Once you understand the core concepts—classes, objects, constructors, and encapsulation—everything else clicks into place.


What is a Class? The Blueprint Metaphor

A class is a blueprint. It defines what something is and what it can do.

Think of a cookie cutter. The cookie cutter is like a class—it specifies the shape. You can use the same cookie cutter to make many cookies. Each cookie is like an object (an instance of the class).

Here’s the simplest possible class:

class Dog {
    // Class definition goes here
};

This is an empty blueprint. Let’s make it useful by adding member variables (data that belongs to the class):

class Dog {
public:
    string name;
    int age;
    string breed;
};

Now our Dog blueprint has three pieces of information: name, age, and breed.


What is an Object? An Instance of a Blueprint

An object is a specific thing created from the blueprint. If a class is a blueprint, an object is the actual house built from that blueprint.

Here’s how you create objects:

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

class Dog {
public:
    string name;
    int age;
    string breed;
};

int main() {
    // Create an object of type Dog
    Dog dog1;
    dog1.name = "Buddy";
    dog1.age = 3;
    dog1.breed = "Golden Retriever";

    // Create another object of type Dog
    Dog dog2;
    dog2.name = "Max";
    dog2.age = 5;
    dog2.breed = "Labrador";

    cout << dog1.name << " is " << dog1.age << " years old" << endl;
    cout << dog2.name << " is " << dog2.age << " years old" << endl;

    return 0;
}

Output:

Buddy is 3 years old
Max is 5 years old

Each object has its own separate data. dog1.name is different from dog2.name. They’re separate things, just created from the same blueprint.


Declaring and Defining a Class in C++

Classes can be declared and defined in different ways. Let’s see the most common approach:

Method 1: Definition in a Header File

// dog.h
#ifndef DOG_H
#define DOG_H

#include <string>
using namespace std;

class Dog {
public:
    string name;
    int age;
    string breed;

    void bark();
    void printInfo();
};

#endif

Then you implement the functions in a .cpp file:

// dog.cpp
#include "dog.h"
#include <iostream>

void Dog::bark() {
    cout << name << " says: Woof! Woof!" << endl;
}

void Dog::printInfo() {
    cout << "Name: " << name << ", Age: " << age << ", Breed: " << breed << endl;
}

The :: operator is the scope resolution operator. It means “the bark() function that belongs to the Dog class.”

Using the Class

// main.cpp
#include <iostream>
#include "dog.h"
using namespace std;

int main() {
    Dog dog1;
    dog1.name = "Buddy";
    dog1.age = 3;
    dog1.breed = "Golden Retriever";

    dog1.bark();
    dog1.printInfo();

    return 0;
}

Member Variables and Member Functions

Member variables are data that belongs to an object:

class Car {
public:
    string color;      // member variable
    string brand;      // member variable
    int speed;         // member variable
};

Member functions (also called methods) are functions that belong to an object:

class Car {
public:
    string color;
    string brand;
    int speed;

    void accelerate() {        // member function
        speed += 10;
    }

    void printStatus() {       // member function
        cout << "Speed: " << speed << " mph" << endl;
    }
};

When you call a member function, it operates on that specific object’s data:

Car car1, car2;
car1.speed = 50;
car2.speed = 100;

car1.accelerate();  // only car1.speed increases
cout << car1.speed << endl;  // 60
cout << car2.speed << endl;  // 100 (unchanged)

Access Specifiers: public, private, protected—And Why They Matter

C++ lets you control who can access member variables and functions using access specifiers:

Here’s why this matters. Imagine a bank account:

// BAD: everything is public
class BankAccount {
public:
    string accountNumber;
    float balance;
};

BankAccount account;
account.balance = 1000000;  // Oops, anyone can set any balance!

This is dangerous. Anyone can modify the balance directly. The proper way is to make the data private and provide controlled access:

// GOOD: balance is protected
class BankAccount {
private:
    float balance;

public:
    float getBalance() const {
        return balance;
    }

    void deposit(float amount) {
        if (amount > 0) {
            balance += amount;
        }
    }

    void withdraw(float amount) {
        if (amount > 0 && amount <= balance) {
            balance -= amount;
        }
    }
};

BankAccount account;
// account.balance = 1000000;  // ERROR: balance is private
account.deposit(500);           // OK: controlled access

Now the bank can enforce rules. You can’t withdraw more than you have, and you can’t set a negative balance.


Constructors: Initialization That Actually Works

When you create an object, you often want to initialize it. That’s what constructors do.

A constructor is a special function that runs automatically when an object is created. It has the same name as the class and no return type:

class Dog {
public:
    string name;
    int age;

    Dog() {              // Constructor (no return type)
        cout << "A dog was created!" << endl;
    }
};

int main() {
    Dog dog1;            // Constructor runs automatically here
    return 0;
}

Output:

A dog was created!

Parameterized Constructors

Constructors can take parameters:

class Dog {
public:
    string name;
    int age;

    Dog(string n, int a) {    // Parameterized constructor
        name = n;
        age = a;
    }

    void printInfo() {
        cout << "Name: " << name << ", Age: " << age << endl;
    }
};

int main() {
    Dog dog1("Buddy", 3);
    dog1.printInfo();          // Name: Buddy, Age: 3

    Dog dog2("Max", 5);
    dog2.printInfo();          // Name: Max, Age: 5

    return 0;
}

This is much cleaner than setting member variables manually after creation.

Default Constructor

If you don’t define any constructor, C++ provides a default one that does nothing:

class Dog {
public:
    string name;
    int age;
    // No constructor defined
};

int main() {
    Dog dog1;  // Default constructor runs (does nothing)
    // dog1.name and dog1.age are uninitialized
    return 0;
}

Constructor Initializer Lists

There’s a more efficient way to initialize member variables: the initializer list:

class Dog {
public:
    string name;
    int age;

    // Using initializer list
    Dog(string n, int a) : name(n), age(a) {
        // Constructor body (usually empty if using initializer list)
    }
};

The : name(n), age(a) part initializes the member variables before the constructor body runs.

Why use an initializer list? It’s more efficient, especially for complex objects like strings:

// WITHOUT initializer list
Dog(string n, int a) {
    name = n;      // Create temporary, then assign
    age = a;
}

// WITH initializer list (more efficient)
Dog(string n, int a) : name(n), age(a) {}

The initializer list initializes directly, avoiding unnecessary temporary objects.


Destructors: Cleanup When Objects Die

A destructor is the opposite of a constructor. It runs automatically when an object is destroyed (when it goes out of scope or is deleted). Destructors are useful for cleanup: closing files, freeing memory, releasing resources.

class Dog {
public:
    string name;

    Dog(string n) : name(n) {
        cout << name << " was born!" << endl;
    }

    ~Dog() {                            // Destructor (~ symbol)
        cout << name << " has left us..." << endl;
    }
};

int main() {
    {
        Dog dog1("Buddy");
        cout << "Buddy is alive" << endl;
    }  // dog1 goes out of scope, destructor runs here

    return 0;
}

Output:

Buddy was born!
Buddy is alive
Buddy has left us...

Destructors are critical for resource management. For example:

class FileHandler {
private:
    ifstream file;

public:
    FileHandler(string filename) {
        file.open(filename);      // Open file in constructor
    }

    ~FileHandler() {
        file.close();             // Close file in destructor
    }
};

When the object is destroyed, the file is automatically closed. No need to remember to close it manually.


The “this” Pointer Explained

Inside a member function, there’s a special pointer called this that points to the current object:

class Dog {
public:
    string name;

    void printName() {
        cout << this->name << endl;    // this->name is the same as name
    }

    Dog* getPointer() {
        return this;                   // Return a pointer to this object
    }
};

You rarely need to use this explicitly, but it’s useful in certain situations:

class Dog {
public:
    string name;

    void setName(string name) {
        this->name = name;   // Distinguish member variable from parameter
    }
};

Encapsulation in Practice: Getters and Setters

Encapsulation means bundling data and functions together, and controlling how the data is accessed. The typical pattern is to make member variables private and provide public getter and setter functions:

class BankAccount {
private:
    float balance;
    string accountNumber;

public:
    BankAccount(string accNum) : accountNumber(accNum), balance(0) {}

    // Getter
    float getBalance() const {
        return balance;
    }

    // Setter with validation
    void setBalance(float newBalance) {
        if (newBalance >= 0) {
            balance = newBalance;
        }
    }

    void deposit(float amount) {
        if (amount > 0) {
            balance += amount;
            cout << "Deposited: $" << amount << endl;
        }
    }

    void withdraw(float amount) {
        if (amount > 0 && amount <= balance) {
            balance -= amount;
            cout << "Withdrew: $" << amount << endl;
        } else {
            cout << "Withdrawal failed!" << endl;
        }
    }
};

The key benefit: you control how data changes. In setBalance(), you can validate that the balance is never negative. If you just had public float balance;, anyone could set it to -999999.


A Complete Practical Example: A BankAccount Class

Let’s put it all together with a realistic example:

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

class BankAccount {
private:
    string accountNumber;
    string accountHolder;
    float balance;

public:
    // Constructor
    BankAccount(string accNum, string holder, float initialBalance)
        : accountNumber(accNum), accountHolder(holder), balance(initialBalance) {
        cout << "Account created for " << accountHolder << endl;
    }

    // Destructor
    ~BankAccount() {
        cout << "Account for " << accountHolder << " closed" << endl;
    }

    // Getters
    string getAccountNumber() const {
        return accountNumber;
    }

    string getAccountHolder() const {
        return accountHolder;
    }

    float getBalance() const {
        return balance;
    }

    // Core functionality
    void deposit(float amount) {
        if (amount > 0) {
            balance += amount;
            cout << accountHolder << " deposited $" << amount << endl;
            cout << "New balance: $" << balance << endl;
        } else {
            cout << "Invalid deposit amount!" << endl;
        }
    }

    void withdraw(float amount) {
        if (amount > 0 && amount <= balance) {
            balance -= amount;
            cout << accountHolder << " withdrew $" << amount << endl;
            cout << "New balance: $" << balance << endl;
        } else {
            cout << "Withdrawal failed! (Insufficient funds or invalid amount)" << endl;
        }
    }

    void printStatement() {
        cout << "\n--- Account Statement ---" << endl;
        cout << "Account Number: " << accountNumber << endl;
        cout << "Account Holder: " << accountHolder << endl;
        cout << "Balance: $" << balance << endl;
        cout << "------------------------\n" << endl;
    }
};

int main() {
    BankAccount account("123456", "Alice", 1000);

    account.deposit(500);
    account.withdraw(200);
    account.printStatement();

    account.withdraw(2000);  // Should fail
    account.printStatement();

    return 0;
}

Output:

Account created for Alice
Alice deposited $500
New balance: $1500
Alice withdrew $200
New balance: $1300

--- Account Statement ---
Account Number: 123456
Account Holder: Alice
Balance: $1300
------------------------

Withdrawal failed! (Insufficient funds or invalid amount)

--- Account Statement ---
Account Number: 123456
Account Holder: Alice
Balance: $1300
------------------------

Account for Alice closed

Structs vs Classes in C++

In C++, structs and classes are almost identical, with one key difference:

class Dog {
    string name;    // private by default
};

struct Dog {
    string name;    // public by default
};

That’s it. In practice, use classes when you want to hide implementation details and control access, and structs for simple data containers.

Modern C++ rarely uses structs for anything but simple data grouping. For real objects, use classes.


Inheritance Overview (With Link to Deeper Article)

Inheritance lets you create new classes based on existing ones. For example, a Dog class might inherit from an Animal class:

class Animal {
public:
    string name;
    void eat() { cout << name << " is eating" << endl; }
};

class Dog : public Animal {
public:
    void bark() { cout << name << " says: Woof!" << endl; }
};

int main() {
    Dog dog;
    dog.name = "Buddy";
    dog.eat();   // Inherited from Animal
    dog.bark();  // Specific to Dog
    return 0;
}

We’ll dive deep into inheritance in a follow-up article. For now, just know that it exists and is one of the pillars of OOP.


Conclusion: OOP Makes Complex Programs Manageable

OOP might seem like a lot of syntax and concepts at first, but it solves a real problem: how to organize large programs so they’re easy to understand and modify.

When you group related data and functions into classes, you create a mental model that matches the real world. A BankAccount object represents an actual bank account. A Dog object represents an actual dog. This makes code intuitive.

The key concepts you’ve learned:

Master these, and you’re ready to build real C++ programs.


Ready to go deeper? Check out our guide to inheritance and polymorphism to extend your OOP knowledge. Or explore the principles of design patterns to see how experienced C++ programmers organize large codebases.


Take Your C++ Further

If you’re looking to go deeper with C++, the C++ Better Explained Ebook is perfect for you — whether you’re a complete beginner or looking to solidify your understanding. Just $19.

👉 Get the C++ Better Explained Ebook — $19



Edit page
Share this post on:

Previous Post
Multithreading in C++: Threads, Mutexes, and Writing Thread-Safe Code
Next Post
How to Use Pointers in C++: A Complete Beginner's Guide