Back to blog

Phase 2: Object-Oriented Programming in Modern C++

cppc++oopprogrammingclasses
Phase 2: Object-Oriented Programming in Modern C++

Welcome to Phase 2

You've mastered C++ fundamentals—types, functions, references, and the compilation model. Now it's time to explore Object-Oriented Programming in C++. Unlike Java or Python where OOP is straightforward, C++ gives you fine-grained control over memory layout, object lifetime, and performance. This power comes with responsibility.

This phase covers everything you need to build robust, maintainable C++ classes: constructors, destructors, the Rule of Five (and when to use Rule of Zero), inheritance, polymorphism, and operator overloading. We'll emphasize RAII (Resource Acquisition Is Initialization)—the foundational C++ idiom that makes resource management safe and automatic.

Time commitment: 1-2 weeks, 1-2 hours daily Prerequisite: Phase 1: C++ Fundamentals

What You'll Learn

By the end of Phase 2, you'll be able to:

✅ Design classes with proper encapsulation
✅ Write all special member functions (constructors, destructors, copy/move)
✅ Understand RAII and apply it for resource management
✅ Use inheritance and virtual functions correctly
✅ Implement polymorphic hierarchies
✅ Overload operators for custom types
✅ Apply modern C++ idioms (= default, = delete, override, final)
✅ Avoid common OOP pitfalls in C++


1. Classes and Objects

Class Definition Syntax

class Rectangle {
private:  // Access specifier (default for class)
    double width_;
    double height_;
 
public:
    // Constructor
    Rectangle(double width, double height)
        : width_{width}, height_{height} {}
 
    // Member functions
    double area() const {
        return width_ * height_;
    }
 
    double perimeter() const {
        return 2 * (width_ + height_);
    }
 
    // Getters
    double width() const { return width_; }
    double height() const { return height_; }
 
    // Setters
    void set_width(double w) { width_ = w; }
    void set_height(double h) { height_ = h; }
};

Key observations:

  • Access specifiers: private, public, protected
  • Member initializer list: : width_{width}, height_{height} (preferred over assignment)
  • const member functions: Promise not to modify object state
  • Trailing underscore convention: width_ distinguishes member from parameter

struct vs class

The only difference: default access.

class MyClass {
    int x;  // private by default
public:
    int y;  // public
};
 
struct MyStruct {
    int x;  // public by default
private:
    int y;  // private
};

Convention:

  • Use struct for plain data aggregates (no invariants)
  • Use class when you have invariants to maintain

Creating and Using Objects

#include <iostream>
 
int main() {
    // Stack allocation (automatic storage)
    Rectangle rect1{5.0, 3.0};  // Uniform initialization
    Rectangle rect2(5.0, 3.0);  // Traditional constructor syntax
 
    std::cout << "Area: " << rect1.area() << '\n';
    std::cout << "Perimeter: " << rect1.perimeter() << '\n';
 
    // Heap allocation (dynamic storage) - prefer smart pointers!
    Rectangle* rect3 = new Rectangle{10.0, 20.0};
    std::cout << "Pointer area: " << rect3->area() << '\n';
    delete rect3;  // Manual cleanup required
 
    return 0;
}

Modern C++ advice: Prefer stack allocation. When heap allocation is needed, use smart pointers (std::unique_ptr, std::shared_ptr).


2. Constructors

Default Constructor

A constructor with no parameters (or all default parameters):

class Point {
private:
    double x_;
    double y_;
 
public:
    // Default constructor
    Point() : x_{0.0}, y_{0.0} {}
 
    // Parameterized constructor
    Point(double x, double y) : x_{x}, y_{y} {}
 
    double x() const { return x_; }
    double y() const { return y_; }
};
 
int main() {
    Point origin;           // Calls default constructor
    Point p{3.0, 4.0};      // Calls parameterized constructor
}

= default and = delete

Modern C++ lets you explicitly control special member functions:

class Widget {
public:
    Widget() = default;  // Compiler generates default constructor
    Widget(int value);   // Custom constructor
 
    // Prevent copying
    Widget(const Widget&) = delete;
    Widget& operator=(const Widget&) = delete;
 
    // Allow moving (will cover later)
    Widget(Widget&&) = default;
    Widget& operator=(Widget&&) = default;
};

When = default helps:

  • Documents intent ("yes, I want the default behavior")
  • Required when you've declared other constructors but still want the default
  • Can be more efficient than hand-written versions

Member Initializer Lists

Always prefer initializer lists over assignment in constructor body:

class BadExample {
private:
    std::string name_;
    int count_;
 
public:
    BadExample(const std::string& name, int count) {
        // BAD: default constructs, then assigns
        name_ = name;
        count_ = count;
    }
};
 
class GoodExample {
private:
    std::string name_;
    int count_;
 
public:
    GoodExample(const std::string& name, int count)
        : name_{name}, count_{count} {}  // GOOD: direct initialization
};

Why initializer lists matter:

  1. Performance: Avoids default construction + assignment
  2. Required for: const members, reference members, members without default constructors
  3. Order: Members initialized in declaration order, not initializer list order
class Example {
private:
    int a_;  // Initialized first
    int b_;  // Initialized second
 
public:
    // WARNING: b_ is initialized BEFORE a_ despite list order!
    // This could cause bugs if b_ initialization depends on a_
    Example(int x) : b_{x}, a_{b_ + 1} {}  // UB: a_ uses uninitialized b_
};

Delegating Constructors (C++11)

One constructor can call another:

class Circle {
private:
    double radius_;
    double x_, y_;
 
public:
    // Primary constructor
    Circle(double radius, double x, double y)
        : radius_{radius}, x_{x}, y_{y} {}
 
    // Delegates to primary constructor
    Circle(double radius)
        : Circle{radius, 0.0, 0.0} {}
 
    // Default: unit circle at origin
    Circle()
        : Circle{1.0, 0.0, 0.0} {}
};

explicit Keyword

Prevents implicit conversions:

class Meter {
private:
    double value_;
 
public:
    // Allows: Meter m = 5.0;  (implicit conversion)
    Meter(double v) : value_{v} {}
};
 
class Kilometer {
private:
    double value_;
 
public:
    // Prevents: Kilometer k = 5.0;  (must use Kilometer k{5.0};)
    explicit Kilometer(double v) : value_{v} {}
};
 
void processKm(Kilometer km);
 
int main() {
    Meter m = 5.0;        // OK: implicit conversion
    // Kilometer k = 5.0; // ERROR: no implicit conversion
    Kilometer k{5.0};     // OK: explicit construction
    Kilometer k2(5.0);    // OK: direct initialization
 
    // processKm(5.0);    // ERROR: can't implicitly convert
    processKm(Kilometer{5.0});  // OK
}

Best practice: Use explicit for single-argument constructors to prevent surprises.


3. Destructors and RAII

Destructor Basics

The destructor is called when an object's lifetime ends:

class Logger {
private:
    std::string name_;
 
public:
    Logger(const std::string& name) : name_{name} {
        std::cout << "Logger '" << name_ << "' created\n";
    }
 
    ~Logger() {
        std::cout << "Logger '" << name_ << "' destroyed\n";
    }
};
 
int main() {
    Logger log1{"main"};
    {
        Logger log2{"inner"};
        // log2 destroyed here (end of scope)
    }
    std::cout << "After inner scope\n";
    // log1 destroyed here (end of main)
}

Output:

Logger 'main' created
Logger 'inner' created
Logger 'inner' destroyed
After inner scope
Logger 'main' destroyed

RAII: Resource Acquisition Is Initialization

RAII is the most important C++ idiom. The idea: tie resource lifetime to object lifetime.

#include <fstream>
#include <stdexcept>
 
class FileHandle {
private:
    FILE* file_;
 
public:
    explicit FileHandle(const char* filename, const char* mode) {
        file_ = std::fopen(filename, mode);
        if (!file_) {
            throw std::runtime_error("Failed to open file");
        }
    }
 
    ~FileHandle() {
        if (file_) {
            std::fclose(file_);  // Always cleaned up!
        }
    }
 
    // Prevent copying (would cause double-close)
    FileHandle(const FileHandle&) = delete;
    FileHandle& operator=(const FileHandle&) = delete;
 
    FILE* get() const { return file_; }
};
 
void processFile(const char* filename) {
    FileHandle file{filename, "r"};  // Opens file
 
    // Do work with file.get()...
 
    // Even if an exception is thrown, ~FileHandle() runs
    // and the file is properly closed!
}

Why RAII is powerful:

  1. No leaks: Destructor always runs (even with exceptions)
  2. No forget-to-close bugs: Cleanup is automatic
  3. Composable: RAII objects can contain other RAII objects

Standard library RAII types:

  • std::unique_ptr - Owns heap memory
  • std::shared_ptr - Shared heap memory ownership
  • std::lock_guard - Holds mutex locks
  • std::fstream - Manages file handles
  • std::vector, std::string - Manage their own memory

4. Copy Semantics

Copy Constructor

Creates a new object as a copy of an existing one:

class IntArray {
private:
    int* data_;
    size_t size_;
 
public:
    explicit IntArray(size_t size)
        : data_{new int[size]{}}, size_{size} {}
 
    // Copy constructor
    IntArray(const IntArray& other)
        : data_{new int[other.size_]}, size_{other.size_} {
        std::copy(other.data_, other.data_ + size_, data_);
    }
 
    ~IntArray() {
        delete[] data_;
    }
 
    int& operator[](size_t index) { return data_[index]; }
    size_t size() const { return size_; }
};
 
int main() {
    IntArray arr1{5};
    arr1[0] = 42;
 
    IntArray arr2 = arr1;  // Copy constructor called
    IntArray arr3{arr1};   // Also copy constructor
 
    arr2[0] = 100;
    // arr1[0] is still 42 (deep copy!)
}

Copy Assignment Operator

Assigns one existing object to another:

class IntArray {
    // ... (same as above)
 
    // Copy assignment operator
    IntArray& operator=(const IntArray& other) {
        if (this != &other) {  // Self-assignment check
            delete[] data_;  // Clean up existing resource
 
            size_ = other.size_;
            data_ = new int[size_];
            std::copy(other.data_, other.data_ + size_, data_);
        }
        return *this;
    }
};
 
int main() {
    IntArray arr1{5};
    IntArray arr2{3};
 
    arr2 = arr1;  // Copy assignment operator called
}

Copy-and-Swap Idiom

A safer, exception-safe way to implement copy assignment:

#include <utility>  // std::swap
 
class IntArray {
private:
    int* data_;
    size_t size_;
 
public:
    explicit IntArray(size_t size)
        : data_{new int[size]{}}, size_{size} {}
 
    // Copy constructor (same as before)
    IntArray(const IntArray& other)
        : data_{new int[other.size_]}, size_{other.size_} {
        std::copy(other.data_, other.data_ + size_, data_);
    }
 
    // Swap function
    friend void swap(IntArray& a, IntArray& b) noexcept {
        using std::swap;
        swap(a.data_, b.data_);
        swap(a.size_, b.size_);
    }
 
    // Copy assignment using copy-and-swap
    IntArray& operator=(IntArray other) {  // Note: passed by VALUE
        swap(*this, other);
        return *this;
    }  // 'other' destructor cleans up old data
 
    ~IntArray() {
        delete[] data_;
    }
};

Benefits:

  1. Exception-safe: If copy construction fails, original is unchanged
  2. Handles self-assignment: Naturally works correctly
  3. DRY: Reuses copy constructor logic

5. Move Semantics

The Problem with Copies

Sometimes copying is wasteful:

IntArray createArray() {
    IntArray temp{1000000};  // Million integers
    // ... fill temp ...
    return temp;  // Copy to caller's variable (expensive!)
}
 
IntArray arr = createArray();  // Copy constructor called

Rvalue References and Move Constructor

C++11 introduced move semantics to "steal" resources from temporary objects:

class IntArray {
private:
    int* data_;
    size_t size_;
 
public:
    explicit IntArray(size_t size)
        : data_{new int[size]{}}, size_{size} {}
 
    // Move constructor
    IntArray(IntArray&& other) noexcept
        : data_{other.data_}, size_{other.size_} {
        other.data_ = nullptr;  // Leave source in valid empty state
        other.size_ = 0;
    }
 
    // Move assignment operator
    IntArray& operator=(IntArray&& other) noexcept {
        if (this != &other) {
            delete[] data_;  // Clean up existing
 
            data_ = other.data_;  // Steal resources
            size_ = other.size_;
 
            other.data_ = nullptr;  // Leave source empty
            other.size_ = 0;
        }
        return *this;
    }
 
    ~IntArray() {
        delete[] data_;  // Safe: delete nullptr is no-op
    }
};

Key points:

  • IntArray&& is an rvalue reference (binds to temporaries)
  • Move operations should be noexcept when possible
  • Moved-from object must be left in a valid but unspecified state

std::move

std::move doesn't move anything—it casts to rvalue reference:

#include <utility>
 
IntArray arr1{100};
IntArray arr2 = std::move(arr1);  // Move, not copy
 
// arr1 is now empty (moved-from)
// arr2 owns the data

Important: After std::move, don't use the moved-from object except to:

  • Destroy it
  • Assign to it
  • Call methods that don't require a valid state

6. The Rule of Five (and Rule of Zero)

Rule of Five

If you define any of these, you probably need to define all five:

  1. Destructor
  2. Copy constructor
  3. Copy assignment operator
  4. Move constructor
  5. Move assignment operator
class Resource {
private:
    int* data_;
 
public:
    // Constructor
    Resource() : data_{new int{42}} {}
 
    // 1. Destructor
    ~Resource() { delete data_; }
 
    // 2. Copy constructor
    Resource(const Resource& other)
        : data_{new int{*other.data_}} {}
 
    // 3. Copy assignment
    Resource& operator=(const Resource& other) {
        if (this != &other) {
            *data_ = *other.data_;
        }
        return *this;
    }
 
    // 4. Move constructor
    Resource(Resource&& other) noexcept
        : data_{other.data_} {
        other.data_ = nullptr;
    }
 
    // 5. Move assignment
    Resource& operator=(Resource&& other) noexcept {
        if (this != &other) {
            delete data_;
            data_ = other.data_;
            other.data_ = nullptr;
        }
        return *this;
    }
};

Rule of Zero

Better approach: Don't manage resources directly. Use RAII types:

#include <memory>
#include <vector>
#include <string>
 
class Person {
private:
    std::string name_;                        // Manages its own memory
    std::vector<int> scores_;                 // Manages its own memory
    std::unique_ptr<SomeResource> resource_;  // Manages heap object
 
public:
    Person(std::string name)
        : name_{std::move(name)}
        , resource_{std::make_unique<SomeResource>()} {}
 
    // No destructor needed!
    // No copy/move functions needed! (or = delete copy if unique_ptr)
    // The compiler-generated defaults do the right thing.
};

Rule of Zero: Design your classes so that compiler-generated special members are correct.


7. Inheritance

Basic Inheritance

class Animal {
protected:
    std::string name_;
 
public:
    Animal(std::string name) : name_{std::move(name)} {}
 
    void eat() const {
        std::cout << name_ << " is eating\n";
    }
 
    std::string name() const { return name_; }
};
 
class Dog : public Animal {  // public inheritance
public:
    Dog(std::string name) : Animal{std::move(name)} {}
 
    void bark() const {
        std::cout << name_ << " says: Woof!\n";
    }
};
 
int main() {
    Dog dog{"Buddy"};
    dog.eat();   // Inherited from Animal
    dog.bark();  // Dog's own method
}

Access Specifiers in Inheritance

class Base {
public:
    int pub;
protected:
    int prot;
private:
    int priv;
};
 
class PublicDerived : public Base {
    // pub is public
    // prot is protected
    // priv is not accessible
};
 
class ProtectedDerived : protected Base {
    // pub is protected
    // prot is protected
    // priv is not accessible
};
 
class PrivateDerived : private Base {
    // pub is private
    // prot is private
    // priv is not accessible
};

Almost always use public inheritance. Private/protected inheritance is rare.

Constructor/Destructor Order

class Base {
public:
    Base() { std::cout << "Base()\n"; }
    ~Base() { std::cout << "~Base()\n"; }
};
 
class Derived : public Base {
public:
    Derived() { std::cout << "Derived()\n"; }
    ~Derived() { std::cout << "~Derived()\n"; }
};
 
int main() {
    Derived d;
}

Output:

Base()
Derived()
~Derived()
~Base()

Order: Base constructors first (top-down), then destructors in reverse order (bottom-up).


8. Polymorphism

Virtual Functions

Enable runtime polymorphism:

class Shape {
public:
    virtual double area() const = 0;  // Pure virtual (abstract)
    virtual void draw() const {
        std::cout << "Drawing a shape\n";
    }
    virtual ~Shape() = default;  // CRITICAL: virtual destructor
};
 
class Circle : public Shape {
private:
    double radius_;
 
public:
    explicit Circle(double r) : radius_{r} {}
 
    double area() const override {  // 'override' ensures we're overriding
        return 3.14159 * radius_ * radius_;
    }
 
    void draw() const override {
        std::cout << "Drawing a circle with radius " << radius_ << '\n';
    }
};
 
class Rectangle : public Shape {
private:
    double width_, height_;
 
public:
    Rectangle(double w, double h) : width_{w}, height_{h} {}
 
    double area() const override {
        return width_ * height_;
    }
 
    void draw() const override {
        std::cout << "Drawing a " << width_ << "x" << height_ << " rectangle\n";
    }
};

Using Polymorphism

#include <memory>
#include <vector>
 
void printArea(const Shape& shape) {
    std::cout << "Area: " << shape.area() << '\n';
}
 
int main() {
    Circle circle{5.0};
    Rectangle rect{4.0, 3.0};
 
    printArea(circle);  // Uses Circle::area()
    printArea(rect);    // Uses Rectangle::area()
 
    // Polymorphism with pointers/smart pointers
    std::vector<std::unique_ptr<Shape>> shapes;
    shapes.push_back(std::make_unique<Circle>(2.0));
    shapes.push_back(std::make_unique<Rectangle>(3.0, 4.0));
    shapes.push_back(std::make_unique<Circle>(1.5));
 
    for (const auto& shape : shapes) {
        shape->draw();  // Calls correct override
        std::cout << "Area: " << shape->area() << '\n';
    }
}

Virtual Destructors

Critical rule: If a class has virtual functions, it needs a virtual destructor.

class Base {
public:
    virtual void foo() {}
    // ~Base() {}  // BUG: non-virtual destructor
    virtual ~Base() = default;  // CORRECT
};
 
class Derived : public Base {
    int* data_;
public:
    Derived() : data_{new int[100]} {}
    ~Derived() { delete[] data_; }
};
 
int main() {
    Base* ptr = new Derived{};
    delete ptr;  // Without virtual destructor, only Base::~Base() runs!
                 // Derived::~Derived() never runs = MEMORY LEAK
}

override and final

class Base {
public:
    virtual void foo() const;
    virtual void bar();
};
 
class Derived : public Base {
public:
    void foo() const override;  // OK: matches Base::foo
    // void foo() override;     // ERROR: const mismatch caught by 'override'
 
    void bar() final;  // Can't be overridden further
};
 
class MoreDerived : public Derived {
public:
    // void bar() override;  // ERROR: bar is final in Derived
};
 
class FinalClass final : public Base {
    // Cannot be inherited from
};

Best practice: Always use override when overriding virtual functions.


9. Operator Overloading

Common Operators

class Fraction {
private:
    int numerator_;
    int denominator_;
 
public:
    Fraction(int num, int den = 1)
        : numerator_{num}, denominator_{den} {}
 
    // Arithmetic operators (as member functions)
    Fraction operator+(const Fraction& other) const {
        return Fraction{
            numerator_ * other.denominator_ + other.numerator_ * denominator_,
            denominator_ * other.denominator_
        };
    }
 
    Fraction operator*(const Fraction& other) const {
        return Fraction{
            numerator_ * other.numerator_,
            denominator_ * other.denominator_
        };
    }
 
    // Unary minus
    Fraction operator-() const {
        return Fraction{-numerator_, denominator_};
    }
 
    // Comparison (C++20 spaceship operator)
    auto operator<=>(const Fraction& other) const {
        return (numerator_ * other.denominator_) <=>
               (other.numerator_ * denominator_);
    }
 
    bool operator==(const Fraction& other) const = default;
 
    // Conversion operator
    explicit operator double() const {
        return static_cast<double>(numerator_) / denominator_;
    }
 
    int numerator() const { return numerator_; }
    int denominator() const { return denominator_; }
};
 
// Stream operator (non-member, usually friend)
std::ostream& operator<<(std::ostream& os, const Fraction& f) {
    return os << f.numerator() << '/' << f.denominator();
}

Usage

int main() {
    Fraction a{1, 2};  // 1/2
    Fraction b{2, 3};  // 2/3
 
    Fraction c = a + b;  // 7/6
    Fraction d = a * b;  // 2/6
 
    std::cout << a << " + " << b << " = " << c << '\n';
    std::cout << "-a = " << -a << '\n';
 
    if (a < b) {
        std::cout << a << " < " << b << '\n';
    }
 
    double val = static_cast<double>(a);  // 0.5
}

Operator Overloading Guidelines

OperatorMember vs Non-member
= (assignment)Must be member
[], (), ->, ->*Must be member
+=, -=, *=, etc.Usually member
+, -, *, /Prefer non-member
<<, >> (streams)Must be non-member
<, <=, >, >=, ==, !=Prefer non-member
<=> (C++20)Either, but member is common

10. Multiple Inheritance

Basic Multiple Inheritance

class Printable {
public:
    virtual void print() const = 0;
    virtual ~Printable() = default;
};
 
class Serializable {
public:
    virtual std::string serialize() const = 0;
    virtual ~Serializable() = default;
};
 
class Document : public Printable, public Serializable {
private:
    std::string content_;
 
public:
    explicit Document(std::string content) : content_{std::move(content)} {}
 
    void print() const override {
        std::cout << content_ << '\n';
    }
 
    std::string serialize() const override {
        return "{\"content\": \"" + content_ + "\"}";
    }
};

The Diamond Problem

class Animal {
public:
    std::string name;
};
 
class Mammal : public Animal {};
class Bird : public Animal {};
 
class Bat : public Mammal, public Bird {
    // Problem: Bat has TWO copies of Animal!
    // bat.name is ambiguous
};

Virtual Inheritance

Solves the diamond problem:

class Animal {
public:
    std::string name;
    Animal(std::string n) : name{std::move(n)} {}
};
 
class Mammal : virtual public Animal {
public:
    Mammal(std::string n) : Animal{std::move(n)} {}
};
 
class Bird : virtual public Animal {
public:
    Bird(std::string n) : Animal{std::move(n)} {}
};
 
class Bat : public Mammal, public Bird {
public:
    // Must call Animal constructor directly
    Bat(std::string n)
        : Animal{std::move(n)}, Mammal{""}, Bird{""} {}
    // Now there's only ONE Animal subobject
};

Advice: Avoid multiple inheritance of classes. Prefer:

  1. Single inheritance with interfaces (pure abstract classes)
  2. Composition over inheritance

11. Modern OOP Patterns

Composition Over Inheritance

Instead of:

class Car : public Engine, public Transmission { };  // Bad

Prefer:

class Car {
private:
    Engine engine_;
    Transmission transmission_;
 
public:
    void start() {
        engine_.start();
        transmission_.engage();
    }
};

Interface Pattern

// Pure abstract class = Interface
class Drawable {
public:
    virtual void draw() const = 0;
    virtual ~Drawable() = default;
};
 
class Clickable {
public:
    virtual void onClick() = 0;
    virtual ~Clickable() = default;
};
 
// Implement multiple interfaces
class Button : public Drawable, public Clickable {
public:
    void draw() const override { /* ... */ }
    void onClick() override { /* ... */ }
};

Factory Pattern

#include <memory>
 
class Shape {
public:
    virtual ~Shape() = default;
    virtual void draw() const = 0;
};
 
class Circle : public Shape {
public:
    void draw() const override { std::cout << "Circle\n"; }
};
 
class Square : public Shape {
public:
    void draw() const override { std::cout << "Square\n"; }
};
 
// Factory function
std::unique_ptr<Shape> createShape(const std::string& type) {
    if (type == "circle") return std::make_unique<Circle>();
    if (type == "square") return std::make_unique<Square>();
    return nullptr;
}

Common Pitfalls

1. Slicing

class Base {
public:
    int x = 1;
    virtual void foo() { std::cout << "Base\n"; }
};
 
class Derived : public Base {
public:
    int y = 2;
    void foo() override { std::cout << "Derived\n"; }
};
 
void badFunction(Base b) {  // Pass by VALUE = slicing!
    b.foo();  // Always calls Base::foo()
}
 
int main() {
    Derived d;
    badFunction(d);  // Derived part is SLICED OFF
}

Fix: Pass polymorphic types by reference or pointer.

2. Forgetting Virtual Destructor

Already covered—always make destructors virtual in base classes.

3. Calling Virtual Functions in Constructor/Destructor

class Base {
public:
    Base() {
        init();  // Calls Base::init(), even in Derived construction!
    }
    virtual void init() { std::cout << "Base init\n"; }
};
 
class Derived : public Base {
public:
    void init() override { std::cout << "Derived init\n"; }
};
 
int main() {
    Derived d;  // Prints "Base init", not "Derived init"!
}

Why? During Base constructor, the object is still a Base. Virtual dispatch doesn't work as expected.

4. Raw Pointer Ownership Confusion

class Container {
    Widget* widget_;  // Who owns this? Unclear!
public:
    Container(Widget* w) : widget_{w} {}
    ~Container() { delete widget_; }  // Maybe double-delete?
};

Fix: Use smart pointers to express ownership.


Summary

Classes: Use access specifiers, prefer struct for data, class for behavior
Constructors: Use initializer lists, explicit for single-arg, = default/= delete
RAII: Tie resource lifetime to object lifetime
Rule of Five/Zero: Define all five, or use RAII types and define none
Inheritance: Use public inheritance, always virtual destructors
Polymorphism: Use override, pass by reference/pointer
Operators: Follow conventions (member vs non-member)
Composition: Prefer composition over inheritance

What's Next?

Additional Resources


Series: Modern C++ Learning Roadmap
Previous: Phase 1: C++ Fundamentals
Next: Phase 3: Modern C++ Features

📬 Subscribe to Newsletter

Get the latest blog posts delivered to your inbox every week. No spam, unsubscribe anytime.

We respect your privacy. Unsubscribe at any time.

💬 Comments

Sign in to leave a comment

We'll never post without your permission.