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 atypedefor 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 (nameandagein Example 1) — nothing is allocated for the member functions themselves. - Member functions are compiled once and shared. Every
Dogobject uses the same machine code forbark(). What makesmyDog.bark()know which dog'snameto print is a hidden pointer, conventionally calledthis, that the compiler silently passes into every non-static member function call. Conceptually,myDog.bark()is compiled roughly likebark(&myDog), and insidebark(),namereally meansthis->name. sizeof(ClassName)reflects data members, not functions.sizeof(Dog)is (roughly) the combined size ofstring nameandint 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.
privateandpublichave 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 calldelete 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
constreference (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
Carclass with privatestring make,string model, andint yeardata members, a constructor that sets all three, and adisplayInfo()member function that prints them. Create twoCarobjects inmain()and display both. - Exercise 2: Write a
Circleclass with a privatedouble radius, asetRadius(double r)function that rejects negative values with an error message, andarea()andcircumference()functions. Test it by trying to set a negative radius and confirming it's rejected. - Exercise 3: Write a
Studentclass with a privatestring nameand a private array orvector<int>of grades, a function to add a grade, and a functionaverage()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
thispointer lets a shared member function know which object's data to operate on. - Encapsulation means hiding data behind
privateand exposing a controlledpublicinterface, so invalid states are prevented rather than merely possible to avoid. public/privateare 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.
