07 · Classes & Objects Basics¶
🎥 Video walkthrough¶
A class is a blueprint; an object is an instance created from that blueprint.
Members and constructors¶
#include <iostream>
#include <string>
class Person {
public:
// fields ("member variables" / "data members")
std::string name;
int age;
// constructor -- runs when you create a new Person
Person(std::string name, int age) {
this->name = name; // "this->name" is the member, "name" is the parameter
this->age = age;
}
void introduce() const {
std::cout << "Hi, I'm " << name << ", age " << age << std::endl;
}
};
int main() {
Person alice("Alice", 30);
Person bob("Bob", 25);
alice.introduce(); // Hi, I'm Alice, age 30
bob.introduce(); // Hi, I'm Bob, age 25
std::cout << alice.name << std::endl; // Alice -- direct member access
}
this is a pointer to the current instance — this->name disambiguates a
member from a parameter of the same name. The const after introduce()
promises the method won't modify the object's members; the compiler enforces
this.
Constructor initializer lists¶
class Point {
public:
double x;
double y;
// initializer list -- preferred over assigning in the constructor body
Point(double x, double y) : x(x), y(y) {
}
double distanceFromOrigin() const {
return std::sqrt(x * x + y * y);
}
};
The : x(x), y(y) syntax is a member initializer list — it initializes
members directly as the object is constructed, rather than default-constructing
them and then assigning inside the body. For simple members like double the
difference is minor, but for class-typed members (like a std::string or
another object) it avoids an unnecessary default construction, and it's
required for const members and references. Prefer initializer lists as your
default style.
Multiple constructors (overloading)¶
class Rectangle {
public:
double width;
double height;
Rectangle(double width, double height) : width(width), height(height) {
}
// Overloaded constructor -- a square is a rectangle with equal sides
Rectangle(double side) : Rectangle(side, side) { // delegates to the other constructor
}
double area() const {
return width * height;
}
};
int main() {
Rectangle r(4, 5);
Rectangle square(3);
std::cout << r.area() << std::endl; // 20
std::cout << square.area() << std::endl; // 9
}
Just like free functions (Module 4), constructors can be overloaded — the compiler picks the right one based on the arguments you pass to it.
Encapsulation — private members with getters/setters¶
#include <stdexcept>
class BankAccount {
private:
double balance; // private -- not accessible outside this class
public:
BankAccount(double initialBalance) {
if (initialBalance < 0) {
throw std::invalid_argument("Initial balance cannot be negative");
}
balance = initialBalance;
}
double getBalance() const {
return balance;
}
void deposit(double amount) {
if (amount <= 0) {
throw std::invalid_argument("Deposit must be positive");
}
balance += amount;
}
void withdraw(double amount) {
if (amount > balance) {
throw std::runtime_error("Insufficient funds");
}
balance -= amount;
}
};
int main() {
BankAccount account(100.0);
account.deposit(50.0);
account.withdraw(30.0);
std::cout << account.getBalance() << std::endl; // 120
// account.balance = -500; // won't compile -- balance is private
}
public: and private: are access specifiers — everything after one
applies until the next specifier (or the end of the class). Keeping data
private and exposing only controlled methods is called encapsulation;
it's the default, idiomatic way to design classes in C++.
struct vs class¶
struct Point3D { // struct members are public by default
double x, y, z;
};
class Point3DClass { // class members are private by default
double x, y, z; // private here, unlike the struct above
};
struct and class are otherwise identical in C++ — the only difference is
the default access level. Convention: use struct for simple, passive data
bundles with no invariants to protect; use class when you have private
state, behavior, and invariants to enforce (like BankAccount above).
Each object owns its own data¶
class Circle {
public:
double radius;
Circle(double radius) : radius(radius) {
}
double area() const {
return 3.14159 * radius * radius;
}
};
int main() {
Circle c1(2.0);
Circle c2(5.0);
std::cout << c1.area() << std::endl; // 12.5664 -- each object has its own radius
std::cout << c2.area() << std::endl; // 78.5398
}
| Concept | Meaning |
|---|---|
| Class | Blueprint / type definition |
| Object (instance) | A concrete value of a class type |
| Member variable | A variable that lives on each instance |
| Constructor | Special member function that initializes a new instance |
this |
Pointer to the current instance |
private / public |
Access specifiers controlling encapsulation |
How It Actually Works¶
An object's memory layout is decided entirely at compile time: a class with
members int a; double b; std::string c; becomes, in memory, those three
members laid out back-to-back (with compiler-inserted padding so each
member starts at an address matching its alignment requirement — a double
typically needs to start at an 8-byte-aligned address, so the compiler may
insert unused padding bytes after a smaller member to satisfy that). There
is no per-object overhead beyond this — no hidden "class name" field, no
reference count — unless the class has virtual functions, in which case a
hidden vptr (pointer to a virtual function table) is added, usually as
the first 8 bytes.
A constructor is really just a function the compiler guarantees gets called
automatically at the point of object creation, that initializes each member
in the order the members are declared (not the order listed in the
initializer list — a common source of subtle bugs when one member's
initializer depends on another declared later). "Calling" a member function
like obj.method() compiles down to an ordinary function call with one
extra hidden argument: a pointer to obj itself, passed as this. There's
no dispatch table lookup for a non-virtual method — the compiler resolves
which function address to call at compile time, exactly like overload
resolution, so calling a member function costs the same as calling a free
function.
struct and class produce identical machine code — the only difference
the compiler treats differently is the default access level (public for
struct, private for class); everything about layout, constructors, and
dispatch is otherwise the same keyword-for-keyword.
🔀 See this in another language¶
Exercise¶
Write a Book class with private members title, author, and pagesRead
(starting at 0), a constructor taking title and author, a method
readPages(int n) that increases pagesRead, and a method
getProgress(int totalPages) const that returns the percentage read as a
double. Create two Book objects in main, read some pages on each, and
print their progress.