📝 Lesson 9: Classes and Objects
Building your own types with classes, objects, constructors, destructors, and encapsulation — the foundation of object-oriented C++.
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Explain what object-oriented programming is and why classes organize related data and behavior together.
- Define a class with private data members and a public interface, and instantiate objects from it.
- Write constructors (default, parameterized, copy) and a destructor, and explain when each runs.
- Apply encapsulation with access specifiers, and use the
thispointer to disambiguate members. - Build objects out of other objects with composition, and share state across objects with
staticmembers. - Overload operators and grant
friendaccess so custom types behave like built-in ones.
Estimated Time: 90–120 minutes
Project: Design a small banking system (accounts, transactions, a customer with multiple accounts) using classes, constructors/destructors, and static bank-wide statistics.
In This Lesson
What is Object-Oriented Programming?
Imagine you're organizing a zoo. Instead of tracking "animal name 1", "animal age 1", "animal type 1", etc., you create an "Animal" blueprint that combines all related information and behaviors. That's OOP - organizing code around objects that represent real things!
Classes: Blueprints for Objects
A class is like an architect's blueprint - it defines what something should look like and what it can do. An object is the actual house built from that blueprint!
Creating Your First Class
Complete Rectangle Class Example
class Rectangle {
private:
double width;
double height;
public:
// Constructor - called when object is created
Rectangle(double w, double h) {
width = w;
height = h;
}
// Default constructor
Rectangle() {
width = 0;
height = 0;
}
// Methods (member functions) - const: they only read, never modify
double getArea() const {
return width * height;
}
double getPerimeter() const {
return 2 * (width + height);
}
// Setters - modify private data
void setWidth(double w) {
if (w > 0) {
width = w;
}
}
void setHeight(double h) {
if (h > 0) {
height = h;
}
}
// Getters - access private data
double getWidth() const {
return width;
}
double getHeight() const {
return height;
}
};
// Using the class
int main() {
Rectangle rect1(5, 3); // Create object with constructor
Rectangle rect2; // Create with default constructor
cout << "Area: " << rect1.getArea() << endl; // 15
cout << "Perimeter: " << rect1.getPerimeter() << endl; // 16
rect2.setWidth(10);
rect2.setHeight(4);
cout << "Rect2 area: " << rect2.getArea() << endl; // 40
return 0;
}
Encapsulation: Protecting Your Data
Encapsulation is like a car - you don't need to understand the engine to drive. You just use the steering wheel and pedals (public interface) while the complex mechanics (private data) are hidden!
Constructors and Destructors
Constructors are like a welcome party when an object is born, and destructors are the cleanup crew when it's time to go!
Constructor and Destructor Examples
class Student {
private:
string name;
int* grades;
int numGrades;
public:
// Default constructor
Student() {
cout << "Default constructor called!" << endl;
name = "Unknown";
numGrades = 0;
grades = nullptr;
}
// Parameterized constructor
Student(string n, int num) {
cout << "Parameterized constructor for " << n << endl;
name = n;
numGrades = num;
grades = new int[numGrades]; // Dynamic allocation
// Initialize grades to 0
for (int i = 0; i < numGrades; i++) {
grades[i] = 0;
}
}
// Copy constructor (important for dynamic memory!)
Student(const Student& other) {
cout << "Copy constructor called!" << endl;
name = other.name;
numGrades = other.numGrades;
// Deep copy of array
grades = new int[numGrades];
for (int i = 0; i < numGrades; i++) {
grades[i] = other.grades[i];
}
}
// Copy assignment operator (Rule of 3: if you write one of
// destructor / copy constructor / copy assignment, write all three)
Student& operator=(const Student& other) {
if (this == &other) return *this; // Guard against self-assignment
delete[] grades; // Release what we already own
name = other.name;
numGrades = other.numGrades;
// Deep copy of array
grades = new int[numGrades];
for (int i = 0; i < numGrades; i++) {
grades[i] = other.grades[i];
}
return *this;
}
// Destructor - cleanup!
~Student() {
cout << "Destructor for " << name << endl;
delete[] grades; // Free dynamic memory
}
void setGrade(int index, int grade) {
if (index >= 0 && index < numGrades) {
grades[index] = grade;
}
}
double getAverage() const {
if (numGrades == 0) return 0;
int sum = 0;
for (int i = 0; i < numGrades; i++) {
sum += grades[i];
}
return static_cast<double>(sum) / numGrades;
}
};
The this Pointer
The 'this' pointer is like saying "myself" - it's how an object refers to itself!
Class Composition: Objects Within Objects
Composition is like building with LEGO blocks - you create complex objects by combining simpler ones!
Composition Example
class Engine {
private:
int horsepower;
double displacement;
public:
Engine(int hp, double disp) : horsepower(hp), displacement(disp) {}
void start() {
cout << "Engine starting... Vroom!" << endl;
}
int getHorsepower() const { return horsepower; }
};
class Wheel {
private:
double diameter = 0.0;
string type;
public:
Wheel() = default; // Needed so an array of Wheels can be default-constructed
Wheel(double d, string t) : diameter(d), type(t) {}
void rotate() {
cout << "Wheel rotating..." << endl;
}
};
class Car {
private:
string make;
string model;
Engine engine; // Composition: Car "has an" Engine
Wheel wheels[4]; // Composition: Car "has" Wheels
public:
// Constructor initialization list
Car(string mk, string md, int hp)
: make(mk), model(md), engine(hp, 2.0) {
// Initialize all wheels
for (int i = 0; i < 4; i++) {
wheels[i] = Wheel(17.0, "All-Season");
}
}
void start() {
cout << "Starting " << make << " " << model << endl;
engine.start();
cout << "Ready to drive!" << endl;
}
void drive() {
cout << "Driving..." << endl;
for (int i = 0; i < 4; i++) {
wheels[i].rotate();
}
}
};
Static Members: Shared Among All Objects
Static members are like a shared bulletin board in an office - all employees (objects) can see and use the same information!
Static Members Example
class BankAccount {
private:
string accountNumber;
double balance;
static int totalAccounts; // Shared by all accounts
static double totalBalance; // Track total money in bank
public:
BankAccount(string accNum, double initialBalance) {
accountNumber = accNum;
balance = initialBalance;
totalAccounts++; // Increment shared counter
totalBalance += balance; // Add to total
}
~BankAccount() {
totalAccounts--; // Decrement when destroyed
totalBalance -= balance;
}
void deposit(double amount) {
balance += amount;
totalBalance += amount; // Update total
}
void withdraw(double amount) {
if (amount <= balance) {
balance -= amount;
totalBalance -= amount;
}
}
// Static method - can be called without object
static int getTotalAccounts() {
return totalAccounts;
}
static double getTotalBalance() {
return totalBalance;
}
};
// Initialize static members outside class
int BankAccount::totalAccounts = 0;
double BankAccount::totalBalance = 0;
// Usage
int main() {
cout << "Accounts: " << BankAccount::getTotalAccounts() << endl; // 0
BankAccount acc1("123", 1000);
BankAccount acc2("456", 2000);
cout << "Accounts: " << BankAccount::getTotalAccounts() << endl; // 2
cout << "Total balance: $" << BankAccount::getTotalBalance() << endl; // 3000
return 0;
}
Operator Overloading: Making Objects Natural
Operator overloading lets your objects work with operators like +, -, ==, just like built-in types!
🔭 A first taste — full treatment in Lesson 13
This is a preview so the Vector2D example below reads naturally. Operator overloading gets its own dedicated lesson — Lesson 13: Operator Overloading — where you'll learn the full rules (member vs. friend functions, return types, and which operators to overload versus leave alone). For now, just notice that a class can teach the compiler what + or == means for its own type.
class Vector2D {
private:
double x, y;
public:
Vector2D(double x = 0, double y = 0) : x(x), y(y) {}
// Addition operator
Vector2D operator+(const Vector2D& other) const {
return Vector2D(x + other.x, y + other.y);
}
// Subtraction operator
Vector2D operator-(const Vector2D& other) const {
return Vector2D(x - other.x, y - other.y);
}
// Scalar multiplication
Vector2D operator*(double scalar) const {
return Vector2D(x * scalar, y * scalar);
}
// Equality operator
bool operator==(const Vector2D& other) const {
return (x == other.x && y == other.y);
}
// Stream insertion operator (friend function)
friend ostream& operator<<(ostream& out, const Vector2D& v) {
out << "(" << v.x << ", " << v.y << ")";
return out;
}
double magnitude() const {
return sqrt(x * x + y * y);
}
};
// Usage
int main() {
Vector2D v1(3, 4);
Vector2D v2(1, 2);
Vector2D v3 = v1 + v2; // (4, 6)
Vector2D v4 = v1 - v2; // (2, 2)
Vector2D v5 = v1 * 2; // (6, 8)
cout << "v1 = " << v1 << endl;
cout << "v3 = " << v3 << endl;
cout << "Magnitude of v1: " << v1.magnitude() << endl; // 5
return 0;
}
Practice Exercise: Bank System
🏋️ Build a Banking System
Create a complete banking system with the following features:
- Account class with deposit, withdraw, transfer
- Customer class that can have multiple accounts
- Transaction history
- Static member to track total bank assets
class Transaction {
private:
string type; // "Deposit", "Withdrawal", "Transfer"
double amount;
string date;
public:
// TODO: Constructor and display method
};
class Account {
private:
string accountNumber;
double balance;
vector<Transaction> history;
public:
// TODO: Constructor, deposit, withdraw, getBalance
// TODO: addTransaction, showHistory
};
class Customer {
private:
string name;
string customerId;
vector<Account*> accounts;
public:
// TODO: Constructor, addAccount, getTotalBalance
// TODO: transfer between accounts
};
💡 Hint
- Use dynamic allocation for accounts
- Implement proper copy constructor for Customer
- Add validation for withdrawals
- Use static members for bank-wide statistics
Friend Functions and Classes
Friends are like trusted individuals who have keys to your house - they can access private members!
Challenge Exercise: Game Development
🏋️ Create a Simple Game Framework
Design classes for a simple 2D game:
- GameObject base class with position and velocity
- Player class with health and score
- Enemy class with AI behavior
- Collision detection between objects
- Game class to manage all objects
✅ Solution
class GameObject {
protected:
double x, y; // Position
double vx, vy; // Velocity
double width, height; // Size
public:
virtual ~GameObject() = default; // Virtual: safe delete through a base pointer
virtual void update(double deltaTime);
virtual void render();
bool collidesWith(const GameObject& other) const;
};
class Player : public GameObject {
private:
int health;
int score;
public:
void handleInput();
void takeDamage(int damage);
};
🎯 Quick Quiz
Question 1: In the Student class example, why must Student have a custom copy constructor that performs a deep copy of grades?
Question 2: In the BankAccount example, what does declaring totalAccounts as static mean?
Question 3: In the Vector2D example, why is operator<< implemented as a friend function instead of a member function?
Summary
🎉 Key Takeaways
- Classes are blueprints for creating objects
- Encapsulation hides implementation details behind a public interface
- Constructors initialize objects, destructors clean up
- The copy constructor must deep-copy any owned pointers, or two objects end up sharing (and double-freeing) the same memory
- The this pointer refers to the current object
- Composition builds complex objects from simpler ones
- Static members are shared among all objects of a class
- Operator overloading makes custom types feel natural
- Friend functions can access private members when needed
📚 Additional Resources
- cppreference — Classes
- cppreference — Constructors and member initializer lists
- LearnCpp — Introduction to object-oriented programming
🚀 What's Next?
You can now design your own types from scratch — bundling data with the behavior that operates on it. Next, in Lesson 10: Smart Pointers, you'll let those objects manage their own memory automatically — using unique_ptr, shared_ptr, and weak_ptr so ownership is handled for you through RAII instead of manual new/delete.
🎉 You're thinking in objects now!
Classes are the turning point where C++ stops being "just functions and data" and starts being real software design. Nice work building your first ones.