Unit 5: Dynamic Memory Management and Polymorphism

CAP455 — Object Oriented Programming Using C++ 7 min read

I. Orientation — Resource Lifetime and Interface-Based Behavior

Dynamic memory management controls objects whose size or lifetime cannot be fixed conveniently at compile time, while polymorphism allows one interface to represent objects of different classes. In C++, these ideas interact closely: dynamically created derived objects are often accessed and destroyed through base-class pointers.

  • Storage duration: Local variables normally have automatic storage duration and are destroyed when their scope ends; objects created by new have dynamic storage duration and remain alive until explicitly destroyed by delete.
  • Resource ownership: Every dynamically allocated resource must have a clearly identified owner responsible for releasing it exactly once.
  • Type relationships: Public inheritance models an “is-a” relationship; for example, a Circle is a Shape.
  • Polymorphic interface: A base-class pointer or reference can access a derived object, and virtual dispatch selects the appropriate overridden function.
  • Lifetime safety: Destruction through a base pointer requires a virtual base-class destructor.
  • Modern convention: Direct new and delete explain the mechanism, but production C++ generally prefers RAII containers and smart pointers such as std::vector and std::unique_ptr.

II. Dynamic Storage Management — Allocation, Ownership, and Release

A. Importance of dynamic memory allocation

Dynamic allocation obtains memory during program execution, making storage responsive to runtime requirements.

  • Runtime size: The amount of memory may depend on input unavailable at compile time; an array of n values can be allocated after reading n.
  • Flexible lifetime: A dynamically allocated object can outlive the block or function in which it was created, provided its address remains accessible.
  • Dynamic structures: Linked lists, trees, graphs, and variable-sized buffers create nodes or storage as needed rather than reserving a fixed maximum.
  • Large objects: Dynamic storage avoids placing large arrays on the usually limited call stack, although heap capacity is also finite.
  • Main responsibility: Flexibility introduces manual lifetime duties—allocation, initialization, ownership tracking, and deallocation must agree.
CPP
int n;
std::cin >> n;
int* values = new int[n];  // n is known only at runtime
// use values
delete[] values;

Here, values stores the address of the first element, while n determines the number of dynamically created int objects.

B. Dynamic memory allocation using new and delete operators

The new operator allocates storage and initializes an object, whereas delete destroys the object and releases its storage.

  • Single object: new T(arguments) creates one object of type T; it must be paired with delete.
  • Array allocation: new T[n] creates n elements; it must be paired with delete[].
  • Initialization forms:
    • new int leaves the scalar value uninitialized.
    • new int(25) initializes it to 25.
    • new int[5]{} value-initializes all five elements to zero.
  • Type-safe result: Unlike C’s malloc, new returns a pointer of the correct type and invokes constructors for class objects.
  • Required pairing: Mixing new[] with delete, or new with delete[], causes undefined behavior.
  • Dangling-pointer prevention: After deletion, the old address is invalid; assigning nullptr helps prevent accidental reuse.
CPP
double* price = new double(49.5);
delete price;
price = nullptr;

std::string* names = new std::string[3];
delete[] names;
names = nullptr;

Deleting nullptr is safe and has no effect, but deleting the same non-null allocation twice is undefined behavior.

C. Memory leak and allocation failures

A memory leak occurs when allocated storage remains unreleased and the program loses the ability to access or free it.

  • Lost address: Reassigning the only pointer to an allocation leaks the original block.
CPP
int* p = new int(10);
p = new int(20);  // the object containing 10 is leaked
delete p;
  • Missing deallocation: Returning early, throwing an exception, or repeatedly allocating inside a loop without deletion can steadily increase memory use.
  • Failure behavior: Ordinary new throws std::bad_alloc when it cannot satisfy a request; it does not normally return nullptr.
  • Non-throwing form: new (std::nothrow) returns nullptr on failure and therefore requires an explicit check.
CPP
int* data = new (std::nothrow) int[1'000'000];
if (data == nullptr) {
    // handle allocation failure
}
delete[] data;
  • Preferred prevention: RAII ties resource lifetime to an automatic object. std::unique_ptr<int[]> data = std::make_unique<int[]>(n); releases the array automatically.
  • Diagnostic tools: AddressSanitizer, Valgrind, and compiler warnings can expose leaks, invalid deletion, and use-after-free defects.

D. Dynamic constructors

A dynamic constructor is a constructor that acquires dynamic memory or another runtime resource while initializing an object.

  • Purpose: It allows each object to own storage whose size is determined by constructor arguments.
  • Destructor requirement: A class that allocates with new[] must release that storage with delete[] in its destructor.
  • Copying danger: Compiler-generated copying duplicates only the pointer, causing shared ownership, double deletion, or unintended modification.
  • Rule of Three: A class defining a destructor, copy constructor, or copy-assignment operator usually needs all three.
  • Modern design: std::vector and std::string provide automatic copying, movement, and cleanup, avoiding most manual ownership errors.
CPP
class Buffer {
    std::size_t size;
    int* data;

public:
    explicit Buffer(std::size_t n)
        : size(n), data(new int[n]{}) {}

    ~Buffer() {
        delete[] data;
    }
};

Here, n is the requested element count, size records it, and data owns the allocated array. This term concerns resource acquisition inside a constructor; it does not mean that constructors themselves are virtual.

III. Forms of Polymorphism — Selection of Behavior

A. Compile time polymorphism vs run time polymorphism

Compile-time polymorphism selects an operation during compilation, while runtime polymorphism selects an overridden operation according to an object’s dynamic type.

  1. Compile-time polymorphism:

    • Mechanisms: Function overloading, operator overloading, and templates.
    • Selection basis: The compiler uses function signatures, argument types, and template substitution.
    • Performance: Calls can usually be resolved directly and optimized or inlined.
    • Example: print(int) and print(double) are selected from the argument’s static type.
  2. Runtime polymorphism:

    • Mechanism: Inheritance combined with virtual functions and calls through base pointers or references.
    • Selection basis: The actual, or dynamic, type of the object determines the final overrider.
    • Benefit: New derived classes can be used through an existing base interface.
    • Cost: Virtual dispatch generally adds an indirection and commonly requires per-object virtual-table information.
CPP
void show(int);                 // compile-time overload
void show(double);

Shape& s = circle;
s.draw();                       // runtime virtual dispatch

The alternatives are complementary: overloading expresses related operations for known types, whereas virtual dispatch supports extensible class hierarchies.

B. Early binding and late binding

Binding is the association of a function call with the function body that will execute.

  1. Early binding:

    • Timing: The target is determined at compile time.
    • Applies to: Non-virtual member functions, overloaded functions, and most ordinary function calls.
    • Static-type effect: A non-virtual call through Base* invokes the base implementation even when the pointer addresses a derived object.
  2. Late binding:

    • Timing: The target is determined at runtime for a virtual call.
    • Conditions: The function must be virtual, and the call must occur through a base-class pointer or reference.
    • Dynamic-type effect: If Base* p points to a Derived, p->f() invokes Derived::f() when that override is the final overrider.
    • Important exception: Virtual calls made from constructors or destructors dispatch only within the class currently being constructed or destroyed, not to a more-derived override.

IV. Virtual Class Hierarchies — Interfaces and Safe Destruction

A. Virtual functions

A virtual function is a non-static member function whose final overrider is selected dynamically when called through a base pointer or reference.

  • Declaration: The base class uses the keyword virtual; derived overrides remain virtual even if the keyword is omitted.
  • Override checking: The override specifier asks the compiler to verify that the signature actually overrides a base virtual function.
  • Object slicing: Copying a derived object into a base object removes its derived portion; use references or pointers to preserve polymorphic behavior.
  • Default arguments: Virtual function bodies are selected dynamically, but default arguments are chosen from the static type, so defaults should be used cautiously.
  • Concrete example:
CPP
class Shape {
public:
    virtual void draw() const { std::cout << "Shape\n"; }
};

class Circle : public Shape {
public:
    void draw() const override { std::cout << "Circle\n"; }
};

Circle c;
Shape& ref = c;
ref.draw();  // Circle

The static type of ref is Shape&, but the dynamic type of the referenced object is Circle.

B. Pure virtual functions

A pure virtual function declares a required interface by placing = 0 in its declaration.

  • Syntax: virtual double area() const = 0; requires concrete derived classes to provide a final implementation.
  • Abstracting behavior: The base class specifies what an operation means for the hierarchy without requiring one universal implementation.
  • Derived obligation: A derived class that leaves any inherited pure virtual function unimplemented remains abstract.
  • Possible definition: A pure virtual function may still have an out-of-class definition, although the class remains abstract.
  • Destructor rule: A pure virtual destructor must have a definition because destruction still invokes the base destructor.
CPP
class Shape {
public:
    virtual double area() const = 0;
    virtual ~Shape() = default;
};

C. Abstract classes and concrete class

An abstract class cannot be instantiated, while a concrete class supplies all required operations and can produce objects.

  1. Abstract class:

    • Criterion: It has, or inherits without overriding, at least one pure virtual function.
    • Role: It defines a common protocol, such as Shape::area().
    • Usage: Pointers and references to it are valid, but direct objects such as Shape s; are not.
  2. Concrete class:

    • Criterion: It has no unimplemented pure virtual functions.
    • Role: It provides usable behavior and may be instantiated.
    • Example: A Circle implementing area() is concrete.
CPP
class Circle final : public Shape {
    double radius;
public:
    explicit Circle(double r) : radius(r) {}
    double area() const override {
        return 3.141592653589793 * radius * radius;
    }
};

Here, radius is the circle’s radius, and the returned value is its area in squared units.

D. Virtual destructor

A virtual destructor ensures that deleting a derived object through a base-class pointer invokes the complete destructor chain.

  • Required situation: Any class intended for polymorphic deletion should declare a virtual destructor.
  • Destruction order: The derived destructor runs first, followed by base destructors in reverse construction order.
  • Failure case: Deleting through a base pointer whose destructor is non-virtual produces undefined behavior when the object is actually derived.
  • Typical declaration: virtual ~Base() = default; is sufficient when the base owns no special resource.
  • Interface design: A polymorphic abstract base usually makes its destructor public and virtual; restricted destruction may instead use a protected destructor under controlled ownership.
CPP
class Base {
public:
    virtual ~Base() = default;
};

Base* p = new Derived;
delete p;  // invokes Derived::~Derived(), then Base::~Base()

V. Recursive Object Structures — Linked Runtime Relationships

A. Self-referential classes

A self-referential class contains a pointer or reference to another object of the same class, enabling recursive data structures.

  • Pointer necessity: A class cannot contain a complete object of its own type directly because that would require infinite size; a pointer has a fixed size.
  • Typical structures: A singly linked node has one next pointer, a tree node has child pointers, and a graph node may store multiple links.
  • Base condition: nullptr commonly marks the end of a linked sequence or an absent child.
  • Dynamic growth: Nodes can be created individually with new as data arrives and connected by pointer assignment.
  • Ownership risk: Traversal pointers must not be confused with owning pointers; unclear ownership causes leaks, double deletion, or dangling links.
  • Modern alternative: std::unique_ptr<Node> can represent exclusive ownership, while non-owning raw pointers may represent back-links.
CPP
class Node {
public:
    int data;
    Node* next;

    explicit Node(int value, Node* link = nullptr)
        : data(value), next(link) {}
};

Node* head = new Node(20);
head = new Node(10, head);  // list: 10 -> 20 -> nullptr

while (head != nullptr) {
    Node* old = head;
    head = head->next;
    delete old;
}

The temporary pointer old preserves the current node’s address while head advances, ensuring that every allocated node is deleted exactly once.