Unit 5 - Notes

CSE202

Unit 5: Dynamic Memory Management and Polymorphism

1. Dynamic Memory Management

In C++, memory can be allocated in two ways: Statically (at compile time via stack memory) and Dynamically (at run time via heap memory). Dynamic memory management allows the program to request specific amounts of memory during execution, which is essential for data structures whose size is unknown until runtime (e.g., linked lists, trees, dynamic arrays).

The new and delete Operators

C++ provides the new and delete operators to manage the specific memory area known as the Free Store or Heap.

The new Operator

  • Purpose: Allocates memory on the heap for a variable or an array and returns the address (pointer) to that memory.
  • Syntax: pointer_variable = new data_type;
  • Initialization: pointer_variable = new data_type(value);

The delete Operator

  • Purpose: Deallocates memory previously allocated by new. If this step is skipped, the memory remains occupied (orphaned) until the program terminates.
  • Syntax: delete pointer_variable;
  • Array Syntax: delete[] pointer_array_variable;

Example:

CPP
#include <iostream>

int main() {
    // Single variable allocation
    int* p = new int; 
    *p = 25;
    
    // Initialization during allocation
    int* q = new int(50); 

    // Array allocation
    int* arr = new int[5]; 

    std::cout << *p << ", " << *q << std::endl;

    // Deallocation
    delete p;
    delete q;
    delete[] arr; // MUST use [] for arrays

    return 0;
}

Memory Leaks and Allocation Failures

Allocation Failures

The heap is limited. If the program requests more memory than is available, the new operator will fail.

  • Standard Behavior: Throws a std::bad_alloc exception.
  • nothrow Version: Returns a NULL pointer instead of throwing an exception.

CPP
int* p = new(std::nothrow) int[100000000000];
if (!p) {
    std::cout << "Allocation failed\n";
}

Memory Leaks

A memory leak occurs when dynamically allocated memory is not deallocated (using delete) before the pointer to that memory is lost or reassigned.

  • Consequence: The program consumes more and more RAM over time, eventually leading to system slowdowns or crashes.
  • Prevention: Every new must have a corresponding delete.

2. Dynamic Constructors

A Dynamic Constructor is a constructor that allocates memory for the object's member variables using the new operator. This is typically used when a class needs to handle a variable amount of data (like a custom String class).

Key Requirements:

  1. The constructor uses new.
  2. The destructor must use delete to free that memory.
  3. Deep Copying is usually required (implementing a Copy Constructor and Assignment Operator) to prevent two objects pointing to the same memory location.

Example:

CPP
class DynamicString {
    char* str;
public:
    DynamicString(const char* s) {
        int length = strlen(s);
        str = new char[length + 1]; // Allocate memory
        strcpy(str, s);
    }

    ~DynamicString() {
        delete[] str; // Release memory
        std::cout << "Memory freed." << std::endl;
    }

    void display() { std::cout << str << std::endl; }
};


3. Introduction to Self-Referential Classes

A Self-Referential Class is a class that contains a pointer member pointing to an object of the same class type.

  • Significance: These are the building blocks of dynamic data structures like Linked Lists, Trees, and Graphs.
  • Constraint: A class cannot contain an instance of itself (this causes infinite recursion in size calculation), but it can contain a pointer to itself.

Example (Linked List Node):

CPP
class Node {
public:
    int data;
    Node* next; // Pointer to an object of type Node

    Node(int val) {
        data = val;
        next = NULL;
    }
};


4. Polymorphism: Compile-time vs. Run-time

Polymorphism means "many forms." It allows a single interface to map to different entities (functions or objects).

Compile-time Polymorphism (Static Binding)

  • Definition: The compiler determines which function to call before the program runs.
  • Mechanisms: Function Overloading, Operator Overloading.
  • Speed: Faster execution (no runtime overhead).

Run-time Polymorphism (Dynamic Binding)

  • Definition: The specific function to call is determined during program execution based on the type of object being pointed to.
  • Mechanisms: Virtual Functions, Function Overriding.
  • Flexibility: High flexibility using pointers/references to base classes.

Early Binding vs. Late Binding

Feature Early Binding (Static) Late Binding (Dynamic)
Time of Resolution Compile Time Run Time
Method Compiler directly links call to address. Compiler uses a v-table (virtual table).
Keywords Normal function calls. virtual keyword.
Efficiency Faster execution. Slight runtime overhead.
Flexibility Low. High (supports inheritance hierarchies).

5. Virtual Functions

A Virtual Function is a member function in the base class that is redefined (overridden) in a derived class. It ensures that the correct function is called for an object, regardless of the type of pointer used to access the object.

Rules for Virtual Functions

  1. Must be declared with the virtual keyword in the base class.
  2. Must be members of some class.
  3. They are accessed through object pointers or references.
  4. Prototypes (return type and parameters) must be identical in the base and derived classes.

Working Mechanism

When a class contains a virtual function, the compiler creates a V-Table (Virtual Table) containing addresses of virtual functions. The object creates a VPTR (Virtual Pointer) pointing to this table. At runtime, the VPTR is used to resolve the function call.

Example:

CPP
class Base {
public:
    virtual void show() {
        std::cout << "Base class show" << std::endl;
    }
    void print() {
        std::cout << "Base class print" << std::endl;
    }
};

class Derived : public Base {
public:
    void show() override { // 'override' is good practice in modern C++
        std::cout << "Derived class show" << std::endl;
    }
    void print() {
        std::cout << "Derived class print" << std::endl;
    }
};

int main() {
    Base* bptr;
    Derived d;
    bptr = &d;

    // Virtual function: Calls Derived's version (Late Binding)
    bptr->show(); 

    // Non-virtual function: Calls Base's version (Early Binding)
    bptr->print(); 

    return 0;
}

Output:
TEXT
Derived class show
Base class print


6. Pure Virtual Functions and Abstract Classes

Pure Virtual Functions

A pure virtual function is a function declared in a base class that has no definition relative to the base class. It is a "placeholder" meant to be overridden by derived classes.

Syntax:

CPP
virtual void functionName() = 0;

Abstract Classes

  • Definition: A class containing at least one pure virtual function is called an Abstract Class.
  • Characteristics:
    • Cannot be instantiated (you cannot create an object of an abstract class).
    • Can contain pointers and references.
    • Used to define interfaces or architectural hierarchies.

Concrete Classes

A class that inherits from an abstract class and provides implementations for all pure virtual functions is called a Concrete Class. Concrete classes can be instantiated.

Example:

CPP
// Abstract Class
class Shape {
public:
    // Pure Virtual Function
    virtual void draw() = 0; 
};

// Concrete Class
class Circle : public Shape {
public:
    void draw() {
        std::cout << "Drawing Circle" << std::endl;
    }
};

// Concrete Class
class Rect : public Shape {
public:
    void draw() {
        std::cout << "Drawing Rectangle" << std::endl;
    }
};

int main() {
    // Shape s; // ERROR: Cannot instantiate abstract class
    Shape* s1 = new Circle();
    Shape* s2 = new Rect();
    
    s1->draw(); // Output: Drawing Circle
    s2->draw(); // Output: Drawing Rectangle
    
    delete s1;
    delete s2;
}


7. Virtual Destructors

When using polymorphism (base class pointer pointing to derived class object), using the delete operator on the base pointer triggers the destructor.

  • Problem: If the base class destructor is not virtual, only the Base class destructor is called. The Derived class destructor is skipped. This causes a memory leak if the Derived class allocated memory dynamically.
  • Solution: Declare the base class destructor as virtual. This ensures destructors are called in the correct reverse order (Derived first, then Base).

Scenario without Virtual Destructor:

CPP
class Base {
public:
    ~Base() { cout << "Base Destructor"; }
};
class Derived : public Base {
    int* arr;
public:
    Derived() { arr = new int[10]; }
    ~Derived() { delete[] arr; cout << "Derived Destructor"; }
};

Base* b = new Derived();
delete b; 
// OUTPUT: "Base Destructor" 
// RESULT: 'arr' in Derived is leaked!

Scenario WITH Virtual Destructor:

CPP
class Base {
public:
    virtual ~Base() { cout << "Base Destructor"; }
};
// ... Derived class same as above ...

Base* b = new Derived();
delete b;
// OUTPUT: "Derived Destructor" then "Base Destructor"
// RESULT: Memory clean.

Rule of Thumb: If a class has any virtual functions, it should provide a virtual destructor.