C++ Object-Oriented Programming

7 minute read

Published:

Object-Oriented Programming (OOP) is more than just a programming style; it’s a powerful paradigm that has shaped modern software development. For C++ developers, a solid grasp of OOP principles is not just beneficial—it’s essential. It allows us to build complex systems that are modular, flexible, and easier to maintain.

This post will guide you through the core concepts of OOP, moving from the “why” to the “how,” with practical C++ examples to solidify your understanding.


From Old School to New School: Procedural vs. Object-Oriented

Before OOP, the dominant paradigm was Procedural Programming, as seen in languages like C. This approach organizes a program around procedures, or functions, that operate on data.

// Procedural approach in C
#include <stdio.h>

// Global data
float account_balance = 1000.0;

void deposit(float amount) {
    account_balance += amount;
}

void withdraw(float amount) {
    if (account_balance >= amount) {
        account_balance -= amount;
    } else {
        printf("Insufficient funds
");
    }
}

int main() {
    deposit(500.0);
    withdraw(200.0);
    printf("Final balance: %.2f
", account_balance);
    return 0;
}

This works for simple programs, but it has significant drawbacks as projects grow:

  • Loose Data-Function Coupling: Data (like account_balance) and the functions that manipulate it (deposit, withdraw) are separate entities. It’s easy to lose track of which functions affect which data.
  • Global Data Risks: The use of global variables means any function anywhere in the program can modify the data, leading to unpredictable behavior and bugs that are difficult to trace.
  • Maintenance Headaches: Changing the data structure requires hunting down and modifying every single function that touches it. This makes the code brittle and hard to extend.

OOP was born from the need to solve these problems by bundling data and the functions that operate on that data into a single unit.


The Core of OOP: Objects

Think about the real world. It’s filled with objects: cars, dogs, computers, people. Each object has attributes (properties or state) and behaviors (actions it can perform).

  • A Car object has attributes like color, brand, and currentSpeed. Its behaviors include startEngine(), accelerate(), and brake().
  • A User object in an application has attributes like username, email, and password. Its behaviors could be login(), logout(), and updateProfile().

In OOP, we model software in a similar way. An object is a self-contained unit that bundles together data (attributes) and the functions that work on that data (behaviors).


Blueprints for Objects: Classes

If an object is a specific thing (like my car), a class is the blueprint that defines how to make that thing. It’s a template that describes the attributes and behaviors that all objects of a certain type will have.

In C++, we use the class keyword to define this blueprint. Let’s create a blueprint for a BankAccount.

#include <iostream>
#include <string>

class BankAccount {
public:
    // Attributes (Member Variables)
    std::string accountHolderName;
    double balance;

    // Behaviors (Member Functions / Methods)
    void deposit(double amount) {
        balance += amount;
    }

    void withdraw(double amount) {
        if (balance >= amount) {
            balance -= amount;
        }
    }

    void displayBalance() {
        std::cout << "Account Holder: " << accountHolderName << ", Balance: " << balance << std::endl;
    }
};

int main() {
    BankAccount myAccount;
    myAccount.accountHolderName = "Jane Doe";
    myAccount.balance = 1000.0;
    myAccount.displayBalance();
    myAccount.deposit(500.0);
    myAccount.displayBalance();
    return 0;
}

This BankAccount class defines that any object created from it will have a accountHolderName, a balance, and the ability to deposit, withdraw, and displayBalance.


The Principle of Encapsulation: Protecting Your Data

The class above has a major flaw: anyone can directly access and modify its member variables.

BankAccount myAccount;
myAccount.balance = -1000000; // This should not be allowed!

This is where encapsulation comes in. It’s the practice of bundling data and methods together and restricting direct access to an object’s internal data. We achieve this in C++ using access specifiers:

  • public: Members are accessible from outside the class. This is the public “interface” of your object.
  • private: Members can only be accessed by other member functions inside the same class. This is used to hide and protect the object’s internal state.

Let’s refactor our BankAccount class to be properly encapsulated:

#include <iostream>
#include <string>

class BankAccount {
private:
    // Data is hidden and protected
    std::string accountHolderName;
    double balance;

public:
    // Constructor: A special method for creating and initializing objects
    BankAccount(std::string name, double initialDeposit) {
        accountHolderName = name;
        balance = initialDeposit;
    }

    // Public methods provide controlled access to the data
    void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
            std::cout << "Deposited: $" << amount << std::endl;
        }
    }

    void withdraw(double amount) {
        if (amount > 0 && balance >= amount) {
            balance -= amount;
            std::cout << "Withdrew: $" << amount << std::endl;
        } else {
            std::cout << "Withdrawal failed. Insufficient funds or invalid amount." << std::endl;
        }
    }

    void displayBalance() {
        std::cout << "Account Holder: " << accountHolderName << std::endl;
        std::cout << "Current Balance: $" << balance << std::endl;
    }
    
    // "Getter" method to safely retrieve a private variable
    std::string getName() {
        return accountHolderName;
    }
};

Now, the balance can only be modified through the deposit and withdraw methods, which contain logic to prevent invalid operations. This makes our code safer, more predictable, and easier to debug.


Bringing Classes to Life: Creating and Using Objects

Defining a class doesn’t create any objects; it just defines the template. To create an actual object, we instantiate the class.

Here’s a full main function showing how to create and interact with BankAccount objects:

int main() {
    // Create an instance of the BankAccount class for John Doe
    BankAccount johnsAccount("John Doe", 1500.00);

    // Create another instance for Jane Smith
    BankAccount janesAccount("Jane Smith", 2000.00);

    std::cout << "--- Initial Balances ---" << std::endl;
    johnsAccount.displayBalance();
    std::cout << std::endl;
    janesAccount.displayBalance();
    
    std::cout << "
--- Performing Transactions ---" << std::endl;
    
    // Interact with johnsAccount
    johnsAccount.deposit(500.00);
    johnsAccount.withdraw(200.00);
    johnsAccount.withdraw(2000.00); // This will fail

    std::cout << std::endl;

    // Interact with janesAccount
    janesAccount.withdraw(150.50);

    std::cout << "
--- Final Balances ---" << std::endl;
    johnsAccount.displayBalance();
    std::cout << std::endl;
    janesAccount.displayBalance();

    return 0;
}

Key Takeaways:

  • johnsAccount and janesAccount are two separate instances of the BankAccount class.
  • Each object has its own distinct set of member variables (accountHolderName and balance).
  • We interact with the objects using their public member functions (the “interface”). We cannot directly write johnsAccount.balance = 50;.

Beyond the Basics: Other Pillars of OOP

Classes and Objects are just the beginning. OOP is supported by three other major concepts:

  1. Inheritance: Allows a new class (derived/child) to inherit attributes and methods from an existing class (base/parent). This promotes code reuse and establishes an “is-a” relationship (e.g., a SavingsAccount is a BankAccount).
  2. Polymorphism: Allows objects of different classes to be treated as objects of a common parent class. The most common use is overriding a parent method in a child class to provide a more specific implementation. This enables flexibility and dynamic behavior.
  3. Abstraction: Hides complex implementation details and shows only the essential features of the object. In our example, the user of the BankAccount class doesn’t need to know how the withdraw method works internally, only that it can be called.

Conclusion

Object-Oriented Programming in C++ is a fundamental shift from procedural thinking. By organizing your code into classes that model real-world or conceptual objects, you create software that is more intuitive, reusable, and scalable. Mastering encapsulation by default and building clear public interfaces for your objects will dramatically improve the quality and longevity of your code.