Dynamic Memory & Advanced Topics
Hey! Welcome back. Today, we’re stepping into the deep end of C++: Dynamic Memory & Advanced Topics.
We’ll cover how to manually lease memory directly from the operating system, how to safeguard our programs against unexpected crashes using exceptions, and how to write generic templates that work with any data type.
Dynamic Memory Allocation
Normally, when you declare a variable (like int x = 10;), C++ manages the memory for you on the Stack. The stack is fast, but it has a major drawback: stack memory is allocated at compile-time and has a fixed size.
If you want to allocate memory dynamically at runtime (for instance, creating an array whose size is decided by a user inputting values), you must allocate it on the Heap using the new operator. When you are done, you must manually free it using the delete operator.
graph TD
subgraph Stack vs. Heap Allocation
direction TB
subgraph Stack Memory
ptr[Pointer variable: ptr <br/> Value: 0x82f1b0]
end
subgraph Heap Memory
block[Allocated Integer <br/> Address: 0x82f1b0 <br/> Value: 42]
end
ptr -->|Points to| block
end
Allocating and Freeing Single Variables and Arrays
- Single Variable: You allocate with
new typeand delete withdelete pointer. - Array: You allocate with
new type[size]and delete withdelete[] pointer.
[!CAUTION] Heap memory is not managed automatically. If you forget to call
deleteordelete[], your program will hold onto that memory until it exits, causing a Memory Leak. Always pairnewwithdelete, andnew[]withdelete[]!
Example:
#include <iostream>
int main() {
// 1. Allocating a single integer on the Heap
int* ptr = new int(42);
std::cout << "Heap Integer Value: " << *ptr << " at Address: " << ptr << std::endl;
// Free the single integer memory
delete ptr;
// 2. Allocating a dynamic array on the Heap
int size = 5;
int* arr = new int[size];
// Initialize and print
for (int i = 0; i < size; ++i) {
arr[i] = (i + 1) * 10;
}
std::cout << "Heap Array elements: ";
for (int i = 0; i < size; ++i) {
std::cout << arr[i] << " ";
}
std::cout << std::endl;
// Free the array memory (notice the brackets [])
delete[] arr;
return 0;
}
Output:
Heap Integer Value: 42 at Address: 0x82f1b0
Heap Array elements: 10 20 30 40 50
Handling Allocation Failures (std::nothrow)
By default, if the operating system runs out of memory, the new operator will throw a std::bad_alloc exception and crash your program.
If you prefer a clean failure where the pointer simply returns nullptr, you can pass std::nothrow to new:
Example:
#include <iostream>
#include <new> // Required for std::nothrow
int main() {
// Attempt to allocate memory safely
int* ptr = new(std::nothrow) int(100);
if (ptr == nullptr) {
std::cerr << "Memory allocation failed!" << std::endl;
return 1;
}
std::cout << "Safe allocation success. Value: " << *ptr << std::endl;
delete ptr;
return 0;
}
Output:
Safe allocation success. Value: 100
Overloading Functions and Operators
Function Overloading
Function overloading allows you to write multiple functions with the same name as long as their parameter lists are different (different parameter count or different data types).
Think of it like a coffee machine: you press a single button to brew coffee, but you can pass parameters (like cup size, milk preference) to get different variations.
Example:
#include <iostream>
class Calculator {
public:
// Adds two integers
int add(int a, int b) {
return a + b;
}
// Adds three integers
int add(int a, int b, int c) {
return a + b + c;
}
// Adds two doubles
double add(double a, double b) {
return a + b;
}
};
int main() {
Calculator calc;
std::cout << "Sum (2 ints): " << calc.add(10, 20) << std::endl;
std::cout << "Sum (3 ints): " << calc.add(10, 20, 30) << std::endl;
std::cout << "Sum (2 doubles): " << calc.add(5.5, 4.5) << std::endl;
return 0;
}
Output:
Sum (2 ints): 30
Sum (3 ints): 60
Sum (2 doubles): 10
Operator Overloading
Operator overloading allows you to define how standard operators (like +, -, <<) behave when applied to your custom objects. It makes your classes feel like built-in primitive types.
Let’s overload the + and - operators for a custom Complex number class:
Example:
#include <iostream>
class Complex {
public:
double real, imag;
Complex(double r = 0, double i = 0) : real(r), imag(i) {}
// Overloading the '+' operator
Complex operator + (const Complex& other) {
return Complex(real + other.real, imag + other.imag);
}
// Overloading the '-' operator
Complex operator - (const Complex& other) {
return Complex(real - other.real, imag - other.imag);
}
void display() const {
std::cout << real << " + " << imag << "i" << std::endl;
}
};
int main() {
Complex c1(4.0, 3.0);
Complex c2(2.0, 5.0);
Complex sum = c1 + c2; // Calls operator+
Complex diff = c1 - c2; // Calls operator-
std::cout << "Sum: "; sum.display();
std::cout << "Difference: "; diff.display();
return 0;
}
Output:
Sum: 6 + 8i
Difference: 2 + -2i
Exception Handling: Protecting Against Crashes
An exception is an error or unexpected event that happens while your program is running (like dividing by zero or failing to open a file).
C++ exception handling uses three keywords:
try: Wraps the code that might trigger an error.throw: Signals that an error has occurred, sending an error object out of the block.catch: Catches the thrown object and handles the error, preventing the program from crashing.
graph TD
try_block[Try Block: Running code] -->|No Errors| end_prog[Continue program]
try_block -->|Error detected| throw_stmt[Throw exception object]
throw_stmt --> catch_type1{Matches catch type 1?}
catch_type1 -->|Yes| handle1[Catch block 1 executes]
catch_type1 -->|No| catch_type2{Matches catch type 2?}
catch_type2 -->|Yes| handle2[Catch block 2 executes]
catch_type2 -->|No| catch_all{Matches catch-all ...?}
catch_all -->|Yes| handle_all[Catch-all block executes]
catch_all -->|No| crash[Program terminates / std::terminate]
Let’s see exceptions in code:
1. Basic Exception Catching
Example:
#include <iostream>
int main() {
double numerator = 10.0;
double denominator = 0.0;
try {
if (denominator == 0.0) {
throw "Division by zero is forbidden!"; // Throwing a string literal
}
double result = numerator / denominator;
std::cout << "Result: " << result << std::endl;
}
catch (const char* msg) {
std::cerr << "Caught Exception: " << msg << std::endl;
}
std::cout << "Program continues running safely..." << std::endl;
return 0;
}
Output:
Caught Exception: Division by zero is forbidden!
Program continues running safely...
2. Handling Multiple Exceptions & Catch-All (...)
You can list multiple catch blocks to handle different types of thrown errors. You can also add a catch-all block using three dots (...) to catch any error that wasn’t caught by previous blocks.
Example:
#include <iostream>
void processInput(int choice) {
if (choice == 1) throw 404; // Throws an int
if (choice == 2) throw "Access Denied"; // Throws a const char*
if (choice == 3) throw 3.14; // Throws a double
}
int main() {
for (int choice = 1; choice <= 3; ++choice) {
try {
processInput(choice);
}
catch (int code) {
std::cout << "Caught Error Code: " << code << std::endl;
}
catch (const char* msg) {
std::cout << "Caught Message: " << msg << std::endl;
}
catch (...) {
// Catch-all handler
std::cout << "Caught unknown exception type!" << std::endl;
}
}
return 0;
}
Output:
Caught Error Code: 404
Caught Message: Access Denied
Caught unknown exception type!
3. Standard and Custom Exceptions
C++ provides standard built-in exception classes inside the <stdexcept> and <exception> headers. The base class is std::exception, which offers a virtual member function const char* what() const noexcept that returns a description of the error.
Let’s create a custom exception class by inheriting from std::exception:
Example:
#include <iostream>
#include <exception>
#include <string>
// Custom exception class
class InsufficientFundsException : public std::exception {
private:
std::string message;
public:
InsufficientFundsException(const std::string& msg) : message(msg) {}
// Overriding the virtual what() function
const char* what() const noexcept override {
return message.c_str();
}
};
int main() {
double balance = 50.0;
double withdrawal = 100.0;
try {
if (withdrawal > balance) {
throw InsufficientFundsException("Insufficient account balance for withdrawal!");
}
balance -= withdrawal;
}
catch (const InsufficientFundsException& e) {
std::cerr << "Custom Exception: " << e.what() << std::endl;
}
return 0;
}
Output:
Custom Exception: Insufficient account balance for withdrawal!
4. Nested Try-Catch & Re-throwing
You can nest try-catch blocks. You can also catch an exception in an inner block, perform some local cleanup, and then re-throw the exact same exception to the outer block using the throw; keyword (with no object specified).
Example:
#include <iostream>
int main() {
try {
std::cout << "Outer Try Block started." << std::endl;
try {
std::cout << "Inner Try Block started." << std::endl;
throw "Database connection timed out!";
}
catch (const char* e) {
std::cout << "Inner Catch: Performing local cleanup... re-throwing error." << std::endl;
throw; // Re-throws the string exception to the outer block
}
}
catch (const char* e) {
std::cout << "Outer Catch: Logged critical error: " << e << std::endl;
}
return 0;
}
Output:
Outer Try Block started.
Inner Try Block started.
Inner Catch: Performing local cleanup... re-throwing error.
Outer Catch: Logged critical error: Database connection timed out!
Mastering Templates: Write Once, Use for Any Type
Have you ever written a function (like findMax) only to realize you had to copy-paste the exact same code to support float, double, and long?
C++ templates solve this. Templates let you write a generic function or class where data types are passed as parameters, making your code highly reusable while maintaining strict compile-time type checking.
graph TD
subgraph Compile-time Template Instantiation
tpl[Template Function: add<T>]
tpl -->|Compiler sees add<int>| bin_int[add_int implementation generated]
tpl -->|Compiler sees add<double>| bin_dbl[add_double implementation generated]
end
1. Function Templates
Here is how we write a generic function template:
Example:
#include <iostream>
#include <string>
// Declaring template parameter 'T'
template <typename T>
T add(T a, T b) {
return a + b;
}
int main() {
// 1. Integer addition
std::cout << "Add Ints: " << add<int>(5, 10) << std::endl;
// 2. Double addition
std::cout << "Add Doubles: " << add<double>(3.14, 2.86) << std::endl;
// 3. String concatenation (works since std::string supports the + operator!)
std::string s1 = "C++ ", s2 = "Templates";
std::cout << "Add Strings: " << add<std::string>(s1, s2) << std::endl;
return 0;
}
Output:
Add Ints: 15
Add Doubles: 6
Add Strings: C++ Templates
2. Class Templates
Just like functions, you can create generic classes where data member types or method return types are template parameters:
Example:
#include <iostream>
template <typename T>
class Box {
private:
T content;
public:
Box(T val) : content(val) {}
T getContent() {
return content;
}
void setContent(T val) {
content = val;
}
};
int main() {
// Class box holding an integer
Box<int> intBox(123);
std::cout << "Integer Box: " << intBox.getContent() << std::endl;
// Same class box holding a char
Box<char> charBox('A');
std::cout << "Character Box: " << charBox.getContent() << std::endl;
return 0;
}
Output:
Integer Box: 123
Character Box: A
Level Up: Extra Practice & Interview Questions
Q1. Write a C++ function that dynamically allocates an array of integers, initializes it with values from 1 to n, and then deallocates the memory.
This validates proper heap array construction and deletion safety.
Code:
#include <iostream>
void manageHeapArray(int n) {
// Allocate heap memory
int* arr = new int[n];
for (int i = 0; i < n; ++i) {
arr[i] = i + 1;
}
std::cout << "Array elements: ";
for (int i = 0; i < n; ++i) {
std::cout << arr[i] << " ";
}
std::cout << std::endl;
// Crucial: Deallocate to prevent memory leak
delete[] arr;
}
int main() {
manageHeapArray(5);
return 0;
}
Output:
Array elements: 1 2 3 4 5
Q2. Write a C++ program that demonstrates function overloading by creating two functions named add that add two integers and three integers, respectively.
This tests simple overload dispatch.
Code:
#include <iostream>
int add(int a, int b) {
return a + b;
}
int add(int a, int b, int c) {
return a + b + c;
}
int main() {
std::cout << "Sum of 2: " << add(5, 15) << std::endl;
std::cout << "Sum of 3: " << add(5, 15, 25) << std::endl;
return 0;
}
Output:
Sum of 2: 20
Sum of 3: 45
Q3. Write a C++ class Complex that overloads the + and - operators to add and subtract complex numbers.
This practice reviews class-based operator syntax.
Code:
#include <iostream>
class Complex {
private:
double r, i;
public:
Complex(double real = 0.0, double imag = 0.0) : r(real), i(imag) {}
Complex operator + (const Complex& o) {
return Complex(r + o.r, i + o.i);
}
Complex operator - (const Complex& o) {
return Complex(r - o.r, i - o.i);
}
void print() const {
std::cout << r << " + " << i << "i" << std::endl;
}
};
int main() {
Complex c1(5.5, 2.5), c2(1.5, 1.5);
Complex cSum = c1 + c2;
Complex cDiff = c1 - c2;
std::cout << "Sum: "; cSum.print();
std::cout << "Difference: "; cDiff.print();
return 0;
}
Output:
Sum: 7 + 4i
Difference: 4 + 1i
Q4. Write a C++ function that throws an exception if the input is less than zero and catches it in the main function.
This verifies validation throw-and-catch workflows.
Code:
#include <iostream>
#include <stdexcept>
void checkPositive(int val) {
if (val < 0) {
throw std::invalid_argument("Input value cannot be negative!");
}
std::cout << "Value: " << val << " is valid." << std::endl;
}
int main() {
try {
checkPositive(10);
checkPositive(-5);
}
catch (const std::invalid_argument& e) {
std::cerr << "Caught Exception: " << e.what() << std::endl;
}
return 0;
}
Output:
Value: 10 is valid.
Caught Exception: Input value cannot be negative!
Q5. Write a C++ template function that finds the maximum of two values.
This validates writing a simple generic lookup template.
Code:
#include <iostream>
#include <string>
template <typename T>
T findMax(T a, T b) {
return (a > b) ? a : b;
}
int main() {
std::cout << "Max of Ints: " << findMax<int>(10, 20) << std::endl;
std::cout << "Max of Doubles: " << findMax<double>(5.57, 5.56) << std::endl;
std::string s1 = "Alpha", s2 = "Beta";
std::cout << "Max of Strings: " << findMax<std::string>(s1, s2) << std::endl;
return 0;
}
Output:
Max of Ints: 20
Max of Doubles: 5.57
Max of Strings: Beta
quiz Test Your Understanding
What operator is used to deallocate dynamically allocated memory in C++?
The delete (or delete[] for arrays) operator deallocates memory on the heap allocated by new.