C++ OOP Introduction

Object-oriented programming (OOP) is a way of structuring programs around objects — self-contained units that bundle data together with the functions that operate on that data. Instead of writing a pile of loose functions that pass structs around, you model real-world (or abstract) things as class types: a Car, a BankAccount, a Player. C++ was built with OOP as a first-class feature, and almost every non-trivial C++ program — from game engines to the standard library itself — is organized this way. This lesson introduces classes and objects, how they’re laid out in memory, and the encapsulation habits that separate working OOP code from buggy OOP code.

Overview: What Is Object-Oriented Programming?

In procedural programming, you write functions that operate on data that lives somewhere else — global variables, structs passed by pointer, arrays. As programs grow, it becomes hard to know which functions are allowed to touch which data, and in what order. OOP fixes this by attaching data and behavior together in one unit called a class.

A class is a blueprint. It doesn’t exist in memory by itself — it’s a description the compiler uses. An object is a concrete instance of that blueprint, created at runtime, with its own memory and its own copy of the class’s data members. If Car is the class, then myHonda and yourToyota are two separate objects, each with their own color, speed, and fuelLevel, but both sharing the same set of behaviors (functions) defined once in the class.

Full OOP in C++ rests on four ideas, and this lesson focuses on the first one in depth (the others get dedicated lessons later in this section):

  • Encapsulation — bundling data with the functions that operate on it, and hiding internal details behind a controlled public interface.
  • Abstraction — exposing only what a user of the class needs to know, hiding the messy implementation.
  • Inheritance — building new classes on top of existing ones, reusing and extending behavior.
  • Polymorphism — letting different classes respond to the same function call in different ways.

The key mental model for this lesson: a class is a type, and an object is a variable of that type. Just like int x; creates an integer variable, Car myHonda; creates a Car object. Everything else in OOP builds on that simple idea.

Syntax

Here is the general shape of a class definition and how you create objects from it:

class ClassName {
private:
    // data members only this class can access directly
    DataType privateMember;

public:
    // constructor: runs automatically when an object is created
    ClassName(DataType value) {
        privateMember = value;
    }

    // member function: behavior the object exposes
    ReturnType functionName(Parameters) {
        // function body, can freely use privateMember
    }
}; // <-- semicolon is required here

int main() {
    ClassName obj(someValue); // create an object, calls the constructor
    obj.functionName();       // call a member function with the dot operator
}
Part Meaning
class ClassName Declares a new type named ClassName.
private: Everything below this label is only accessible from inside the class's own member functions.
public: Everything below this label is accessible from outside the class, e.g. from main().
Data members Variables declared inside the class; each object gets its own copy.
Constructor A function with the same name as the class, no return type, called automatically when an object is created.
Member function A function defined inside the class that can access the object's data members directly.
}; The closing brace of a class needs a trailing semicolon — unlike a function body.
obj.functionName() The dot operator calls a member function on a specific object.

Examples

Example 1: A Basic Class

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

class Dog {
public:
    string name;
    int age;

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

    void describe() {
        cout << name << " is " << age << " years old." << endl;
    }
};

int main() {
    Dog myDog;
    myDog.name = "Rex";
    myDog.age = 3;

    myDog.describe();
    myDog.bark();

    return 0;
}

Output:

Rex is 3 years old.
Rex says: Woof woof!

Here Dog is the class, and myDog is a single object of that class. The dot operator (myDog.name, myDog.bark()) is used to access data members and call member functions on that specific object. If you created a second Dog, it would have its own independent name and age, but it would use the exact same compiled bark() and describe() code.

Example 2: Encapsulation with Private Data

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

class BankAccount {
private:
    string owner;
    double balance;

public:
    BankAccount(string ownerName, double startingBalance) {
        owner = ownerName;
        balance = startingBalance;
    }

    void deposit(double amount) {
        if (amount <= 0) {
            cout << "Deposit amount must be positive." << endl;
            return;
        }
        balance += amount;
        cout << "Deposited $" << amount << ". New balance: $" << balance << endl;
    }

    void withdraw(double amount) {
        if (amount > balance) {
            cout << "Insufficient funds." << endl;
            return;
        }
        balance -= amount;
        cout << "Withdrew $" << amount << ". New balance: $" << balance << endl;
    }

    double getBalance() {
        return balance;
    }
};

int main() {
    BankAccount account("Alice", 100.0);

    account.deposit(50.0);
    account.withdraw(30.0);
    account.withdraw(1000.0);

    cout << "Final balance: $" << account.getBalance() << endl;

    return 0;
}

Output:

Deposited $50. New balance: $150
Withdrew $30. New balance: $120
Insufficient funds.
Final balance: $120

Notice that balance is private — code outside the class cannot write account.balance = -9999; directly. Every change to balance has to go through deposit() or withdraw(), which validate the amount first. This is encapsulation in action: the class controls how its own data can change, rather than trusting every caller to do the right thing.

Example 3: Multiple Objects Interacting

#include <iostream>
using namespace std;

class Rectangle {
private:
    double width;
    double height;

public:
    Rectangle(double w, double h) {
        width = w;
        height = h;
    }

    double area() {
        return width * height;
    }

    double perimeter() {
        return 2 * (width + height);
    }

    bool isLargerThan(Rectangle other) {
        return area() > other.area();
    }
};

int main() {
    Rectangle rect1(5.0, 3.0);
    Rectangle rect2(4.0, 4.0);

    cout << "Rectangle 1 area: " << rect1.area() << endl;
    cout << "Rectangle 1 perimeter: " << rect1.perimeter() << endl;
    cout << "Rectangle 2 area: " << rect2.area() << endl;

    if (rect1.isLargerThan(rect2)) {
        cout << "Rectangle 1 is larger than Rectangle 2." << endl;
    } else {
        cout << "Rectangle 2 is larger than or equal to Rectangle 1." << endl;
    }

    return 0;
}

Output:

Rectangle 1 area: 15
Rectangle 1 perimeter: 16
Rectangle 2 area: 16
Rectangle 2 is larger than or equal to Rectangle 1.

This example shows two independent objects, rect1 and rect2, and a member function (isLargerThan) that takes another object of the same class as a parameter. Inside isLargerThan, calling area() with no object prefix refers to the current object's own area, while other.area() explicitly refers to the parameter object's area — this is a common pattern once you start comparing or combining objects.

Under the Hood: How Classes and Objects Really Work

Understanding what the compiler actually does removes a lot of the mystery around OOP:

  • A class declaration allocates nothing. Writing class Dog { ... }; produces no memory usage at all — it's a compile-time blueprint, similar to a typedef or a struct definition.
  • An object allocates memory for its data members only. When you write Dog myDog; on the stack, the compiler reserves space for exactly the data members (name and age in Example 1) — nothing is allocated for the member functions themselves.
  • Member functions are compiled once and shared. Every Dog object uses the same machine code for bark(). What makes myDog.bark() know which dog's name to print is a hidden pointer, conventionally called this, that the compiler silently passes into every non-static member function call. Conceptually, myDog.bark() is compiled roughly like bark(&myDog), and inside bark(), name really means this->name.
  • sizeof(ClassName) reflects data members, not functions. sizeof(Dog) is (roughly) the combined size of string name and int age, plus any padding the compiler adds for alignment — regardless of how many member functions the class has.
  • Access specifiers are enforced by the compiler, not at runtime. private and public have zero cost when the program runs; they're purely a compile-time check that rejects illegal access with an error message. There is no hidden runtime lock or memory tag marking a member as private.
  • Constructors run automatically. When an object is created, its constructor executes before any of your other code can touch that object, guaranteeing it starts in a valid state. If you don't write any constructor, the compiler generates an implicit default constructor that does nothing beyond default-initializing members.
  • Objects can live on the stack or the heap. Dog myDog; lives on the stack and is destroyed automatically when it goes out of scope. Dog* p = new Dog(); lives on the heap and stays alive until you call delete p; — forgetting to do so leaks memory, just like with any other heap allocation.

Common Mistakes

Mistake 1: Forgetting the Semicolon After a Class

Unlike a function body, a class definition must end with a semicolon after the closing brace. Leaving it out is one of the most common beginner errors and produces a confusing compiler error:

class Point {
public:
    int x;
    int y;
}

int main() {
    Point p;
    p.x = 1;
    p.y = 2;
    return 0;
}

This fails to compile because the compiler treats everything after the missing semicolon as part of the class declaration, so int main() gets swallowed into an invalid syntax. The fix is simply to add the semicolon:

#include <iostream>
using namespace std;

class Point {
public:
    int x;
    int y;
};

int main() {
    Point p;
    p.x = 1;
    p.y = 2;
    cout << "Point: (" << p.x << ", " << p.y << ")" << endl;
    return 0;
}

Output:

Point: (1, 2)

Mistake 2: Making Everything Public (No Encapsulation)

A very common beginner habit is to make all data members public just to avoid writing getters and setters. This compiles fine, but it defeats the entire point of encapsulation because any code, anywhere, can put the object into an invalid state:

#include <iostream>
using namespace std;

class BankAccountUnsafe {
public:
    double balance;
};

int main() {
    BankAccountUnsafe account;
    account.balance = 100.0;

    account.balance = -500.0; // nothing stops this

    cout << "Balance: $" << account.balance << endl;

    return 0;
}

Output:

Balance: $-500

A real bank account should never hold a negative balance without an overdraft rule allowing it. Because balance is public, nothing enforces that. The fix is to make the data private and only allow changes through a function that validates the input:

#include <iostream>
using namespace std;

class BankAccountSafe {
private:
    double balance;

public:
    BankAccountSafe() {
        balance = 0.0;
    }

    void setBalance(double amount) {
        if (amount < 0) {
            cout << "Error: balance cannot be negative." << endl;
            return;
        }
        balance = amount;
    }

    double getBalance() {
        return balance;
    }
};

int main() {
    BankAccountSafe account;
    account.setBalance(100.0);
    account.setBalance(-500.0);

    cout << "Balance: $" << account.getBalance() << endl;

    return 0;
}

Output:

Error: balance cannot be negative.
Balance: $100

Best Practices

  • Default to making data members private, and only expose what callers genuinely need through public member functions.
  • Give every class a constructor that leaves the object in a valid, fully-initialized state — never leave data members uninitialized.
  • Name classes with a capitalized noun (BankAccount, Rectangle) to distinguish types from variables and functions at a glance.
  • Keep each class focused on one responsibility; if a class is doing several unrelated jobs, consider splitting it.
  • Validate input inside setter-style member functions rather than trusting the caller to pass sane values.
  • Prefer passing objects by const reference (const Rectangle&) to functions when you don't need a copy, to avoid unnecessary copying — this becomes more important as classes grow larger.
  • Remember the semicolon after a class's closing brace — it's easy to forget and produces confusing error messages.

Practice Exercises

  • Exercise 1: Write a Car class with private string make, string model, and int year data members, a constructor that sets all three, and a displayInfo() member function that prints them. Create two Car objects in main() and display both.
  • Exercise 2: Write a Circle class with a private double radius, a setRadius(double r) function that rejects negative values with an error message, and area() and circumference() functions. Test it by trying to set a negative radius and confirming it's rejected.
  • Exercise 3: Write a Student class with a private string name and a private array or vector<int> of grades, a function to add a grade, and a function average() that returns the mean of all grades. Print the average for a student with at least four grades.

Summary

  • A class is a blueprint (a type); an object is a specific instance of that type with its own memory.
  • Each object gets its own copy of the class's data members, but all objects share the same compiled member functions.
  • A hidden this pointer lets a shared member function know which object's data to operate on.
  • Encapsulation means hiding data behind private and exposing a controlled public interface, so invalid states are prevented rather than merely possible to avoid.
  • public/private are enforced only at compile time — they cost nothing at runtime.
  • A class definition must end with a semicolon after its closing brace.
  • Constructors run automatically on object creation and should leave every object in a valid state.