Unit 5: Dynamic Memory Management and Polymorphism
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
newhave dynamic storage duration and remain alive until explicitly destroyed bydelete. - 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
Circleis aShape. - 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
newanddeleteexplain the mechanism, but production C++ generally prefers RAII containers and smart pointers such asstd::vectorandstd::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
nvalues can be allocated after readingn. - 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.
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 typeT; it must be paired withdelete. - Array allocation:
new T[n]createsnelements; it must be paired withdelete[]. - Initialization forms:
new intleaves the scalar value uninitialized.new int(25)initializes it to25.new int[5]{}value-initializes all five elements to zero.
- Type-safe result: Unlike C’s
malloc,newreturns a pointer of the correct type and invokes constructors for class objects. - Required pairing: Mixing
new[]withdelete, ornewwithdelete[], causes undefined behavior. - Dangling-pointer prevention: After deletion, the old address is invalid; assigning
nullptrhelps prevent accidental reuse.
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.
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
newthrowsstd::bad_allocwhen it cannot satisfy a request; it does not normally returnnullptr. - Non-throwing form:
new (std::nothrow)returnsnullptron failure and therefore requires an explicit check.
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 withdelete[]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::vectorandstd::stringprovide automatic copying, movement, and cleanup, avoiding most manual ownership errors.
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.
-
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)andprint(double)are selected from the argument’s static type.
-
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.
void show(int); // compile-time overload
void show(double);
Shape& s = circle;
s.draw(); // runtime virtual dispatchThe 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.
-
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.
-
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* ppoints to aDerived,p->f()invokesDerived::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
overridespecifier 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:
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(); // CircleThe 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.
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.
-
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.
-
Concrete class:
- Criterion: It has no unimplemented pure virtual functions.
- Role: It provides usable behavior and may be instantiated.
- Example: A
Circleimplementingarea()is concrete.
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.
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
nextpointer, a tree node has child pointers, and a graph node may store multiple links. - Base condition:
nullptrcommonly marks the end of a linked sequence or an absent child. - Dynamic growth: Nodes can be created individually with
newas 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.
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.
Did this save you a night before the exam?
LPU Notes is free, and it stays free. Ads cover part of the server bill. The rest comes out of a student's own pocket: the domain, the storage, and keeping the site up through the weeks everyone needs it at once.
The payment button didn't load. An ad blocker or a filtered network is the usual reason. to try again.
Nothing here is ever locked, and nothing unlocks. Chip in only if it was worth it. What it pays for →