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:
#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_allocexception. nothrowVersion: Returns aNULLpointer instead of throwing an exception.
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
newmust have a correspondingdelete.
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:
- The constructor uses
new. - The destructor must use
deleteto free that memory. - Deep Copying is usually required (implementing a Copy Constructor and Assignment Operator) to prevent two objects pointing to the same memory location.
Example:
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):
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
- Must be declared with the
virtualkeyword in the base class. - Must be members of some class.
- They are accessed through object pointers or references.
- 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:
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:
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:
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:
// 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:
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:
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.