๐๏ธ Classes & Objects โ The Big Idea
Everything so far has kept data and the functions that work on it separate. This phase introduces the idea of bundling them together โ the foundation of essentially all real-world C++ code you'll encounter.
Every type you've used so far โ int, std::string โ was built into C++ already. A class lets YOU define a brand new type, bundling together both data (variables) and behavior (functions) that belong together conceptually.
class Dog { public: std::string name; int age; void bark() { std::cout << name << " says woof!" << std::endl; } };
A cookie cutter defines a SHAPE โ it isn't itself a cookie you can eat. From that one cutter, you can stamp out as many actual cookies as you want, each a real, separate cookie following that shape. A class is the cutter (the blueprint); an object is an actual instance made from it โ a real thing in memory, following the class's shape.
Dog myDog; myDog.name = "Rex"; myDog.age = 3; myDog.bark(); // prints "Rex says woof!" Dog anotherDog; anotherDog.name = "Bella"; anotherDog.bark(); // prints "Bella says woof!"
myDog and anotherDog are both objects โ separate instances of the Dog class, each with their OWN name and age, but sharing the same bark() behavior defined once in the class itself.
๐ Member Variables & Access Control
The last module's Dog class left its data wide open โ anyone could set an impossible age like -50. This module covers how to actually protect a class's internal data.
The last module's Dog class used public, meaning any code outside the class can freely read or change name and age directly โ exactly what let something like myDog.age = -50; happen without any resistance.
class Dog { private: int age; // can ONLY be accessed by Dog's own member functions public: std::string name; }; Dog myDog; myDog.age = 3; // COMPILER ERROR โ age is private!
Marking age as private means outside code genuinely cannot touch it directly at all โ the compiler enforces this, refusing to compile any code that tries. This is a deliberate design choice, not a limitation to work around: it's the foundation of Module 6's encapsulation.
The common convention in real C++ code: keep member variables private by default, and only make things public when there's a genuine reason external code needs direct access โ usually functions, not raw data. You'll see exactly why in Module 6.
๐๏ธ Constructors
Creating a Dog object and then setting its fields one by one, line by line, is tedious and easy to forget a step on. A constructor fixes this.
class Dog { public: std::string name; int age; Dog(std::string dogName, int dogAge) { // the constructor name = dogName; age = dogAge; } }; Dog myDog("Rex", 3); // name and age set immediately, in one line
A constructor is a special function โ sharing the class's exact name, with no return type at all, not even void โ that runs automatically the moment an object is created. It's the natural place to guarantee an object starts out in a valid, fully-initialized state.
Dog(std::string dogName, int dogAge) : name(dogName), age(dogAge) { // body can be empty โ the : list already did the initialization }
The : name(dogName), age(dogAge) syntax is called a member initializer list โ it's the preferred, more efficient modern C++ style for simple field-setting constructors, though both approaches produce a working result.
If you write NO constructor at all, C++ automatically provides an empty one for you (a "default constructor") that does nothing beyond creating the object. But the moment you write ANY constructor yourself, that automatic one disappears โ if you still want a no-argument way to create the object, you need to write it explicitly.
๐งน Destructors
The mirror image of a constructor โ cleanup that happens automatically, right before an object ceases to exist. This connects directly back to Phase 4's memory management.
class Dog { public: std::string name; Dog(std::string dogName) : name(dogName) { std::cout << name << " was created." << std::endl; } ~Dog() { // the destructor โ note the ~ std::cout << name << " is being destroyed." << std::endl; } };
The destructor โ the class name prefixed with ~ โ runs automatically right before an object is destroyed: when a stack (local) object goes out of scope, or explicitly via delete for a heap object.
A destructor is exactly the right place to delete any heap memory the object itself allocated with new during its lifetime โ guaranteeing that cleanup happens automatically, every single time an object is destroyed, without you needing to remember to call it manually everywhere the object might go out of scope. This is a genuinely powerful pattern (formally called RAII) that Phase 8's smart pointers build on directly.
๐ Member Functions & the this Pointer
How does a member function like bark() know WHICH Dog object it's actually working on, when many Dog objects might exist at once? This module answers that directly.
Dog rex("Rex", 3); Dog bella("Bella", 2); rex.bark(); // prints "Rex says woof!" bella.bark(); // prints "Bella says woof!"
Both calls run the exact same bark() function code โ there's only ONE copy of that function in memory, shared by every Dog object. So how does it know to use rex's name in one call and bella's in the other?
Every non-static member function secretly receives an extra, invisible parameter: this โ a pointer to the exact object the function was called on. When you write rex.bark(), C++ effectively passes &rex in as this behind the scenes, and name inside the function body actually means this->name โ "the name belonging to whichever object I was called on."
void bark() { std::cout << this->name << " says woof!" << std::endl; // "this->name" and just "name" mean the exact same thing here }
Most of the time, you never need to type this yourself โ C++ assumes it automatically. It becomes genuinely necessary mainly when a parameter name shadows (matches) a member variable's name, and you need to distinguish them: this->age = age; unambiguously means "set the member variable to the parameter's value," resolving what would otherwise be confusing.
๐ก๏ธ Encapsulation โ Getters & Setters
Module 2 made age private, but that alone means NO code โ not even legitimate code โ can read or change it. This module shows how to expose controlled, validated access instead.
class Dog { private: int age; public: int getAge() { return age; } };
A getter is a public function whose only job is returning a private member's value โ outside code can now READ age through getAge(), even though it can't touch the variable directly.
void setAge(int newAge) { if (newAge >= 0 && newAge <= 30) { // validation! age = newAge; } else { std::cout << "Invalid age ignored." << std::endl; } }
This is the genuine payoff of encapsulation: a setter can validate a new value BEFORE accepting it โ rejecting myDog.setAge(-50) outright, something that was completely impossible to prevent back when age was simply public in Module 1.
Encapsulation means hiding an object's internal data and only exposing it through controlled, well-defined functions. The genuine benefit isn't the getter/setter syntax itself โ it's that the class can enforce its own rules about what counts as a valid state, and even change HOW data is stored internally later, without breaking any code that uses the class through its public functions.
๐ญ Constructor Overloading & Default Parameters
Phase 3's function overloading applies directly to constructors too โ letting an object be created in more than one way, depending on what information is available.
class Dog { public: std::string name; int age; Dog(std::string dogName, int dogAge) : name(dogName), age(dogAge) {} Dog(std::string dogName) : name(dogName), age(0) {} // age defaults to 0 }; Dog rex("Rex", 3); // uses the first constructor Dog puppy("Puppy"); // uses the second โ age becomes 0
Just like Phase 3's overloaded functions, C++ picks the matching constructor based on how many arguments you provide and their types โ exactly the same overload-resolution rule you already know.
Dog(std::string dogName, int dogAge = 0) : name(dogName), age(dogAge) {} Dog rex("Rex", 3); // age is 3 Dog puppy("Puppy"); // age defaults to 0 โ ONE constructor handles both
int dogAge = 0 in the parameter list means "if the caller doesn't provide this argument, use 0 automatically." This achieves the same flexibility as Module 6's two separate constructors, with less duplicated code โ a genuinely useful pattern beyond just constructors, for any function.
int dogAge = 0, actually achieve?๐ Static Members
Every member variable so far belongs to one specific object. This module covers data that belongs to the CLASS itself, shared identically across every object.
class Dog { public: std::string name; static int totalDogs; // shared by EVERY Dog object Dog(std::string dogName) : name(dogName) { totalDogs++; // every new Dog increments the SAME shared counter } }; int Dog::totalDogs = 0; // required: define it once, outside the class
A static member variable exists exactly ONCE, shared by the entire class โ not duplicated per object the way name is. Every Dog object created increments the SAME totalDogs, giving you an accurate running count across all of them.
Dog rex("Rex"); Dog bella("Bella"); std::cout << Dog::totalDogs; // prints 2 โ accessed via the CLASS, not an object
You access a static member through the class name itself (Dog::totalDogs), not through any particular object โ reflecting that it genuinely belongs to the class as a whole, not to any one Dog.
โ Operator Overloading
Phase 1's + adds numbers and Phase 3's + concatenates strings. This module lets YOUR OWN classes define what + (or any operator) means for objects of that class.
class Point { public: int x, y; Point(int xVal, int yVal) : x(xVal), y(yVal) {} Point operator+(const Point& other) { return Point(x + other.x, y + other.y); } }; Point a(1, 2); Point b(3, 4); Point c = a + b; // c.x is 4, c.y is 6
operator+ is a specially-named member function โ defining it teaches C++ exactly what + should DO when both sides are Point objects. Without it, writing a + b on two Point objects would simply be a compiler error, since C++ has no built-in idea what adding two points should mean.
Operator overloading lets objects of your own classes be used with the same natural, familiar syntax as built-in types โ a + b reads far more clearly than an equivalent a.add(b) function call, especially in math-heavy code (vectors, matrices, complex numbers) where the pattern matches how you'd write the equivalent math by hand.
other by reference (Phase 3 Module 7) avoids an unnecessary copy, and const promises the function won't modify it โ the standard, idiomatic way to accept "read-only access to another object" as a parameter.๐ Capstone โ A BankAccount Class
Every idea from this phase, combined: private data, a validating constructor, encapsulated getters/setters, a static counter, and controlled deposit/withdraw behavior โ a genuinely realistic small class.
Build a BankAccount class with a private balance that can never go negative, a constructor requiring a starting balance, deposit/withdraw methods with real validation, and a static counter tracking how many accounts have been created.
#include <iostream> class BankAccount { private: std::string owner; double balance; public: static int totalAccounts; BankAccount(std::string ownerName, double startingBalance) : owner(ownerName) { balance = (startingBalance >= 0) ? startingBalance : 0; totalAccounts++; } void deposit(double amount) { if (amount > 0) { balance += amount; } } bool withdraw(double amount) { if (amount > 0 && amount <= balance) { balance -= amount; return true; } std::cout << "Withdrawal denied โ insufficient funds." << std::endl; return false; } double getBalance() { return balance; } }; int BankAccount::totalAccounts = 0; int main() { BankAccount acc("Jude", 100.00); acc.deposit(50.00); acc.withdraw(30.00); std::cout << "Balance: " << acc.getBalance() << std::endl; std::cout << "Total accounts: " << BankAccount::totalAccounts << std::endl; return 0; }
balanceisprivate(Module 2) โ the only way to change it is through validateddeposit/withdraw(Module 6's encapsulation).- The constructor itself validates the starting balance, guaranteeing no
BankAccountcan ever start negative โ Module 3's job of guaranteeing valid initial state. totalAccountsisstatic(Module 8), correctly shared and incremented across every account created.withdrawreturns aboolspecifically so calling code can check whether it actually succeeded โ a genuinely useful, realistic pattern.
Add operator+ for Transfers
Using Module 9, write a transferTo method that withdraws from one account and deposits into another โ reusing the validation you've already written rather than duplicating it. This mirrors exactly how real financial software has to think about combining already-validated operations safely.