Chapter 4: Object-Oriented Programming in C++
Classes and Structs
In C++, struct and class are nearly identical. The only difference is default access level.
struct Point { // Members are public by default
double x;
double y;
};
class Point2 { // Members are private by default
double x;
double y;
public:
double getX() const { return x; }
};
Convention: Use struct for plain data, class for objects with behavior.
Class Anatomy
class BankAccount {
private: // Only accessible within the class
std::string owner;
double balance;
static int accountCount; // Shared across all instances
public: // Accessible from anywhere
// Constructor
BankAccount(const std::string& ownerName, double initialBalance)
: owner(ownerName), balance(initialBalance) { // Initializer list
accountCount++;
}
// Destructor
~BankAccount() {
accountCount--;
}
// Member function
void deposit(double amount) {
if (amount > 0) {
balance += amount;
}
}
// Const member function (doesn't modify object)
double getBalance() const {
return balance;
}
// Static member function
static int getAccountCount() {
return accountCount;
}
protected: // Accessible to this class and derived classes
void setBalance(double newBalance) {
balance = newBalance;
}
};
// Initialize static member outside class
int BankAccount::accountCount = 0;
Constructors
Types of Constructors
class Widget {
int id;
std::string name;
int* data;
public:
// Default constructor
Widget() : id(0), name("unnamed"), data(nullptr) {}
// Parameterized constructor
Widget(int i, const std::string& n)
: id(i), name(n), data(new int[100]) {}
// Copy constructor
Widget(const Widget& other)
: id(other.id), name(other.name), data(new int[100]) {
std::copy(other.data, other.data + 100, data);
}
// Move constructor
Widget(Widget&& other) noexcept
: id(other.id), name(std::move(other.name)), data(other.data) {
other.data = nullptr;
}
// Destructor
~Widget() {
delete[] data;
}
};
Initializer List vs Assignment
class Example {
const int id; // Must use initializer list
std::string& ref; // Must use initializer list
std::string name;
public:
// WRONG: Assignment in body
Example(int i, std::string& r, const std::string& n) {
// id = i; // ERROR: can't assign to const
// ref = r; // ERROR: can't assign to reference
name = n; // Works but inefficient (default construct then assign)
}
// CORRECT: Initializer list
Example(int i, std::string& r, const std::string& n)
: id(i), ref(r), name(n) { // Direct initialization
}
};
Rule: Always use initializer lists for member initialization.
Delegating Constructors (C++11)
class Rectangle {
int width, height;
public:
Rectangle() : Rectangle(0, 0) {} // Delegates to below
Rectangle(int size) : Rectangle(size, size) {} // Delegates to below
Rectangle(int w, int h) : width(w), height(h) {} // Main constructor
};
Explicit Constructors
class Wrapper {
int value;
public:
// Without explicit: allows implicit conversion
Wrapper(int v) : value(v) {}
};
void takeWrapper(Wrapper w);
takeWrapper(42); // OK: implicitly converts int to Wrapper
class SafeWrapper {
int value;
public:
// With explicit: prevents implicit conversion
explicit SafeWrapper(int v) : value(v) {}
};
void takeSafeWrapper(SafeWrapper w);
// takeSafeWrapper(42); // ERROR: no implicit conversion
takeSafeWrapper(SafeWrapper(42)); // OK: explicit construction
Rule: Use explicit for single-argument constructors to prevent accidental conversions.
The Rule of Five (and Zero)
If you define any of these, you should define all of them:
- Destructor
- Copy constructor
- Copy assignment operator
- Move constructor
- Move assignment operator
class Resource {
int* data;
size_t size;
public:
// Constructor
Resource(size_t s) : data(new int[s]), size(s) {}
// 1. Destructor
~Resource() {
delete[] data;
}
// 2. Copy constructor
Resource(const Resource& other) : data(new int[other.size]), size(other.size) {
std::copy(other.data, other.data + size, data);
}
// 3. Copy assignment
Resource& operator=(const Resource& other) {
if (this != &other) {
delete[] data;
size = other.size;
data = new int[size];
std::copy(other.data, other.data + size, data);
}
return *this;
}
// 4. Move constructor
Resource(Resource&& other) noexcept : data(other.data), size(other.size) {
other.data = nullptr;
other.size = 0;
}
// 5. Move assignment
Resource& operator=(Resource&& other) noexcept {
if (this != &other) {
delete[] data;
data = other.data;
size = other.size;
other.data = nullptr;
other.size = 0;
}
return *this;
}
};
Rule of Zero
If you don't manage resources directly, you don't need to define any special members:
class Person {
std::string name; // Manages its own memory
std::vector<int> scores; // Manages its own memory
std::unique_ptr<Data> data; // Manages its own memory
public:
Person(const std::string& n) : name(n) {}
// No destructor, copy, move needed - defaults work correctly
};
Prefer Rule of Zero: Use smart pointers and RAII containers to avoid manual memory management.
Operator Overloading
class Vector2D {
public:
double x, y;
Vector2D(double x = 0, double y = 0) : x(x), y(y) {}
// Arithmetic operators
Vector2D operator+(const Vector2D& other) const {
return Vector2D(x + other.x, y + other.y);
}
Vector2D operator-(const Vector2D& other) const {
return Vector2D(x - other.x, y - other.y);
}
Vector2D operator*(double scalar) const {
return Vector2D(x * scalar, y * scalar);
}
// Compound assignment
Vector2D& operator+=(const Vector2D& other) {
x += other.x;
y += other.y;
return *this;
}
// Comparison operators
bool operator==(const Vector2D& other) const {
return x == other.x && y == other.y;
}
bool operator!=(const Vector2D& other) const {
return !(*this == other);
}
// Subscript operator
double& operator[](size_t index) {
return index == 0 ? x : y;
}
// Function call operator (functor)
double operator()() const {
return std::sqrt(x*x + y*y); // Returns magnitude
}
};
// Non-member operator (for scalar * vector)
Vector2D operator*(double scalar, const Vector2D& v) {
return v * scalar;
}
// Stream insertion
std::ostream& operator<<(std::ostream& os, const Vector2D& v) {
return os << "(" << v.x << ", " << v.y << ")";
}
int main() {
Vector2D a(3, 4), b(1, 2);
Vector2D c = a + b; // (4, 6)
Vector2D d = a * 2; // (6, 8)
Vector2D e = 2 * a; // (6, 8)
double mag = a(); // 5.0 (magnitude)
std::cout << a << "\n"; // (3, 4)
}
Spaceship Operator (C++20)
#include <compare>
class Version {
public:
int major, minor, patch;
// One operator generates all comparisons
auto operator<=>(const Version&) const = default;
};
Version v1{1, 2, 3}, v2{1, 3, 0};
bool less = v1 < v2; // true
bool greater = v1 > v2; // false
bool equal = v1 == v2; // false
Inheritance
// Base class
class Shape {
protected:
double x, y; // Position
public:
Shape(double x, double y) : x(x), y(y) {}
virtual double area() const = 0; // Pure virtual (abstract)
virtual void draw() const {
std::cout << "Drawing shape at (" << x << ", " << y << ")\n";
}
virtual ~Shape() = default; // Virtual destructor for polymorphism
};
// Derived class
class Circle : public Shape {
double radius;
public:
Circle(double x, double y, double r) : Shape(x, y), radius(r) {}
double area() const override { // override keyword catches errors
return 3.14159 * radius * radius;
}
void draw() const override {
Shape::draw(); // Call base implementation
std::cout << " -> It's a circle with radius " << radius << "\n";
}
};
class Rectangle : public Shape {
double width, height;
public:
Rectangle(double x, double y, double w, double h)
: Shape(x, y), width(w), height(h) {}
double area() const override {
return width * height;
}
};
Inheritance Types
class Base {
public:
int pub;
protected:
int prot;
private:
int priv;
};
// Public inheritance: maintains access levels
class PubDerived : public Base {
// pub remains public
// prot remains protected
// priv is inaccessible
};
// Protected inheritance: public becomes protected
class ProtDerived : protected Base {
// pub becomes protected
// prot remains protected
// priv is inaccessible
};
// Private inheritance: everything becomes private
class PrivDerived : private Base {
// pub becomes private
// prot becomes private
// priv is inaccessible
};
Rule: Almost always use public inheritance. Private/protected inheritance is rare.
Polymorphism
#include <vector>
#include <memory>
void processShapes(const std::vector<std::unique_ptr<Shape>>& shapes) {
for (const auto& shape : shapes) {
shape->draw(); // Calls appropriate derived class method
std::cout << "Area: " << shape->area() << "\n";
}
}
int main() {
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Circle>(0, 0, 5));
shapes.push_back(std::make_unique<Rectangle>(10, 10, 4, 3));
processShapes(shapes);
return 0;
}
Virtual Functions and vtable
┌───────────────────────────────────────────────────────────────┐
│ How Virtual Functions Work (simplified) │
├───────────────────────────────────────────────────────────────┤
│ │
│ Each object with virtual functions has a hidden vptr │
│ │
│ ┌─────────────┐ ┌─────────────────────────────────┐│
│ │ Circle obj │ │ Circle vtable ││
│ ├─────────────┤ ├─────────────────────────────────┤│
│ │ vptr ───────┼─────────▶│ area() -> Circle::area() ││
│ │ x, y │ │ draw() -> Circle::draw() ││
│ │ radius │ │ ~Shape -> Circle::~Circle() ││
│ └─────────────┘ └─────────────────────────────────┘│
│ │
│ ┌─────────────┐ ┌─────────────────────────────────┐│
│ │ Rectangle │ │ Rectangle vtable ││
│ ├─────────────┤ ├─────────────────────────────────┤│
│ │ vptr ───────┼─────────▶│ area() -> Rectangle::area() ││
│ │ x, y │ │ draw() -> Shape::draw() ││
│ │ width,height│ │ ~Shape -> Rectangle::~Rectangle││
│ └─────────────┘ └─────────────────────────────────┘│
│ │
│ Cost: One pointer per object + one table per class │
└───────────────────────────────────────────────────────────────┘
Virtual Destructor Rule
class Base {
public:
~Base() { std::cout << "Base destructor\n"; } // Non-virtual
};
class Derived : public Base {
int* data;
public:
Derived() : data(new int[100]) {}
~Derived() {
delete[] data;
std::cout << "Derived destructor\n";
}
};
Base* ptr = new Derived();
delete ptr; // ONLY calls Base destructor - MEMORY LEAK!
// Fix: make Base destructor virtual
class Base {
public:
virtual ~Base() = default;
};
Rule: If a class has any virtual functions, make its destructor virtual.
Abstract Classes and Interfaces
// Abstract class (has at least one pure virtual function)
class Drawable {
public:
virtual void draw() const = 0; // Pure virtual
virtual ~Drawable() = default;
};
// Interface (all functions are pure virtual)
class Serializable {
public:
virtual std::string serialize() const = 0;
virtual void deserialize(const std::string& data) = 0;
virtual ~Serializable() = default;
};
// Multiple inheritance of interfaces
class Button : public Drawable, public Serializable {
std::string label;
public:
Button(const std::string& l) : label(l) {}
void draw() const override {
std::cout << "[" << label << "]\n";
}
std::string serialize() const override {
return "Button:" + label;
}
void deserialize(const std::string& data) override {
label = data.substr(7);
}
};
Composition vs Inheritance
Prefer composition over inheritance when you don't need polymorphism.
// INHERITANCE: "is-a" relationship
class Car : public Vehicle {
// Car IS A Vehicle
};
// COMPOSITION: "has-a" relationship
class Car {
Engine engine; // Car HAS AN Engine
Transmission trans;
std::vector<Wheel> wheels;
public:
void start() {
engine.start();
trans.engage();
}
};
Benefits of Composition
- More flexible (can change at runtime)
- No fragile base class problem
- Clearer ownership semantics
- Easier to test
Final and Override Keywords
class Base {
public:
virtual void foo();
virtual void bar() final; // Cannot be overridden
};
class Derived final : public Base { // Cannot be inherited from
void foo() override; // Explicitly marks override (catches typos)
// void bar() override; // ERROR: bar is final
// void fo() override; // ERROR: no Base::fo() to override
};
// class MoreDerived : public Derived {}; // ERROR: Derived is final
Rule: Always use override when overriding virtual functions.
Friend Functions and Classes
class SecretKeeper {
int secret = 42;
// Friend function can access private members
friend void reveal(const SecretKeeper& sk);
// Friend class can access private members
friend class Inspector;
};
void reveal(const SecretKeeper& sk) {
std::cout << sk.secret << "\n"; // OK: friend
}
class Inspector {
public:
void inspect(const SecretKeeper& sk) {
std::cout << sk.secret << "\n"; // OK: friend class
}
};
Use sparingly: Friend breaks encapsulation. Only use when truly necessary.
Exercises
Bank Account System
- Create classes for Account, SavingsAccount, CheckingAccount
- Implement proper inheritance hierarchy
- Add transactions with polymorphic behavior
Shape Hierarchy
- Create abstract Shape with Circle, Rectangle, Triangle
- Implement area() and perimeter() methods
- Create a function to process a vector of shapes
Rule of Five Practice
- Create a DynamicArray class that manages a raw array
- Implement all five special member functions
- Verify correct behavior with copies and moves
Operator Overloading
- Create a Fraction class (numerator/denominator)
- Overload +, -, *, /, ==, <, <<
- Implement automatic simplification
Previous: 03 - Memory Management Next: 05 - Modern C++ Features