Skip to content

Module 01: Memory Allocation, References, and Pointers

Download Official Subject PDF

Key Concepts:

  • Stack vs Heap allocation
  • new and delete operators
  • References vs Pointers
  • Pointers to members
  • switch statements

Understanding New Operators in This Module

Section titled “Understanding New Operators in This Module”

This module introduces memory management operators. Here’s what they mean:

int* ptr = new int; // Allocate memory for one int
int* arr = new int[10]; // Allocate memory for 10 ints
  • new allocates memory on the heap (also called “free store”)
  • It returns a pointer to the allocated memory
  • Memory allocated with new persists until you explicitly delete it
  • Unlike malloc(), new automatically calculates the size and properly initializes objects
delete ptr; // Free memory for one object
delete[] arr; // Free memory for array (NOTE the []!)
  • delete frees memory that was allocated with new
  • delete[] (with square brackets) frees arrays allocated with new[]
  • CRITICAL: Using delete on an array is undefined behavior!
  • CRITICAL: Using delete[] on a single object is undefined behavior!
int& ref = x; // ref is an alias (another name) for x
  • & after a type creates a reference (not a pointer)
  • A reference is just another name for an existing variable
  • When you change ref, you change x - they are the same variable!
  • References must be initialized when declared
  • References cannot be reassigned to refer to a different variable
int* ptr = &x; // ptr stores the address of x (ptr "points to" x)
*ptr = 42; // Dereference ptr to access/modify x
ptr->method(); // If ptr points to an object, call its method
  • Remember: * before a pointer dereferences it (accesses the value)
  • & before a variable gets its address
  • -> is shorthand for dereference + member access
void (Class::*ptr)() = &Class::method; // ptr points to a member function
(object.*ptr)(); // Call through object
(objectPtr->*ptr)(); // Call through pointer to object
  • ->* combines the arrow operator with pointer-to-member
  • It’s used when you have a pointer to an object AND a pointer to one of its members
  • This is advanced syntax used for function pointers to member functions

Understanding Memory Layout (Visual Guide)

Section titled “Understanding Memory Layout (Visual Guide)”

Understanding how memory is organized helps you write better C++ code:

┌─────────────────────────────────────┐
│ HIGH MEMORY │
│ │
│ ┌──────────────────────────────┐ │
│ │ HEAP │ │ ← Dynamic allocation (new/delete)
│ │ (grows downward) │ │ Large, slower, manual cleanup
│ │ [Zombie* z] │ │
│ │ [Brain* b] ← new Brain() │ │
│ └──────────────────────────────┘ │
│ │
│ ↓ gap ↓ │
│ │
│ ┌──────────────────────────────┐ │
│ │ STACK │ │ ← Automatic allocation
│ │ (grows upward) │ │ Fast, small, automatic
│ │ │ │
│ │ ┌────────────────────┐ │ │
│ │ │ main() │ │ │
│ │ │ int x = 42; │ │ │
│ │ │ Zombie z; │ ← constructor called
│ │ │ │ │ │
│ │ └────────────────────┘ │ │
│ └──────────────────────────────┘ │
│ │
│ ┌──────────────────────────────┐ │
│ │ DATA SEGMENT │ │ ← Global/static variables
│ └──────────────────────────────┘ │
│ │
│ ┌──────────────────────────────┐ │
│ │ CODE SEGMENT │ │ ← Program instructions
│ └──────────────────────────────┘ │
│ │
│ LOW MEMORY │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ Stack Frame for function() │
├─────────────────────────────────────┤
│ Return address │ ← Where to go back to
├─────────────────────────────────────┤
│ Saved registers │ ← Previous state
├─────────────────────────────────────┤
│ Local variables: │
│ int x = 42; → [ 42 ] │
│ Zombie z; → [ object ] │
├─────────────────────────────────────┤
│ Function parameters │
└─────────────────────────────────────┘
Stack grows UP
┌─────────────────────────────────────┐
│ Contact object on STACK │
├─────────────────────────────────────┤
│ vptr (hidden) → vtable │ (only if class has virtual functions)
├─────────────────────────────────────┤
│ std::string _firstName │
│ ├─ pointer to heap memory │
│ └─ size/capacity info │
├─────────────────────────────────────┤
│ std::string _lastName │
├─────────────────────────────────────┤
│ int _age = 25; → [ 25 ] │
└─────────────────────────────────────┘

void function() {
int x = 42; // On stack
Zombie zombie; // On stack
} // Automatically destroyed here

Characteristics:

  • Fast allocation/deallocation
  • Limited size (~1-8 MB typically)
  • Automatic cleanup when scope ends
  • Cannot outlive function
void function() {
int* x = new int(42); // On heap
Zombie* zombie = new Zombie(); // On heap
// ... use them ...
delete x; // Must manually delete
delete zombie; // Must manually delete
}

Characteristics:

  • Slower allocation
  • Large capacity (limited by RAM)
  • Must be manually freed
  • Can outlive function (return pointer)

// Allocation
int* ptr = new int; // Uninitialized
int* ptr = new int(); // Zero-initialized
int* ptr = new int(42); // Initialized to 42
// Deallocation
delete ptr;
ptr = NULL; // Good practice (C++98)
// Allocation
int* arr = new int[10]; // Array of 10 ints
Zombie* horde = new Zombie[5]; // Array of 5 Zombies
// Deallocation - NOTE THE []
delete[] arr; // MUST use delete[] for arrays
delete[] horde;
// WRONG: Using delete on array
int* arr = new int[10];
delete arr; // UNDEFINED BEHAVIOR!
// WRONG: Using delete[] on single object
int* ptr = new int(42);
delete[] ptr; // UNDEFINED BEHAVIOR!
// WRONG: Double delete
int* ptr = new int(42);
delete ptr;
delete ptr; // CRASH or corruption!
Use Stack When…Use Heap When…
Object’s lifetime = function scopeObject must outlive function
Size known at compile timeSize determined at runtime
Small objectsLarge objects
Performance criticalReturning new objects

A reference is an alias - another name for an existing variable.

int x = 42;
int& ref = x; // ref IS x (not a copy, not a pointer)
ref = 100; // Changes x to 100
std::cout << x; // Prints 100
FeatureReferencePointer
Syntaxint& ref = x;int* ptr = &x;
Can be nullNoYes
Can be reassignedNoYes
Must be initializedYesNo
Access valueref*ptr
Access address&refptr
// MUST initialize
int& ref; // ERROR: references must be initialized
int& ref = x; // OK
// CANNOT be null
int& ref = NULL; // ERROR: cannot bind to null
// CANNOT be reassigned
int& ref = x;
ref = y; // This doesn't reassign ref, it copies y to x!
// BAD: Passing by value (copies entire object)
void printZombie(Zombie z) {
// z is a COPY - expensive for large objects
}
// BETTER: Passing by pointer
void printZombie(Zombie* z) {
if (z != NULL) // Must check for null
std::cout << z->getName();
}
// BEST: Passing by reference
void printZombie(Zombie& z) {
// z is the original object - no copy
// Cannot be null - no check needed
std::cout << z.getName();
}
// CONST reference - read-only access
void printZombie(const Zombie& z) {
std::cout << z.getName(); // Can read
// z.setName("X"); // ERROR: z is const
}
SituationUse
Never null, won’t change what it refers toReference
Might be nullPointer
Needs to be reassignedPointer
Return new object from functionPointer (or smart pointer in modern C++)
Optional parameterPointer (can pass NULL)

std::string str = "HI THIS IS BRAIN";
std::string* stringPTR = &str; // Pointer TO str
std::string& stringREF = str; // Reference (alias) OF str
// Addresses - all should be the same!
std::cout << &str << std::endl; // Address of str
std::cout << stringPTR << std::endl; // Pointer holds same address
std::cout << &stringREF << std::endl; // Address of ref = address of str
// Values - all should be the same!
std::cout << str << std::endl; // Direct access
std::cout << *stringPTR << std::endl; // Dereference pointer
std::cout << stringREF << std::endl; // Reference is same as original

class HumanA {
private:
std::string _name;
Weapon& _weapon; // Reference - MUST always have a weapon
public:
// MUST initialize reference in constructor
HumanA(std::string name, Weapon& weapon)
: _name(name), _weapon(weapon) {}
void attack() {
std::cout << _name << " attacks with " << _weapon.getType();
}
};
// Usage
Weapon club("club");
HumanA bob("Bob", club); // MUST provide weapon at construction
class HumanB {
private:
std::string _name;
Weapon* _weapon; // Pointer - might not have a weapon
public:
HumanB(std::string name) : _name(name), _weapon(NULL) {}
void setWeapon(Weapon& weapon) {
_weapon = &weapon;
}
void attack() {
if (_weapon)
std::cout << _name << " attacks with " << _weapon->getType();
else
std::cout << _name << " has no weapon";
}
};
// Usage
HumanB jim("Jim"); // No weapon initially
jim.setWeapon(club); // Weapon added later
  • Reference: When object MUST exist at construction and throughout lifetime
  • Pointer: When object might not exist, or might change

void sayHello() { std::cout << "Hello"; }
void sayBye() { std::cout << "Bye"; }
// Function pointer
void (*funcPtr)() = &sayHello;
funcPtr(); // Calls sayHello()
funcPtr = &sayBye;
funcPtr(); // Calls sayBye()
class Harl {
public:
void debug() { std::cout << "Debug"; }
void info() { std::cout << "Info"; }
void warning() { std::cout << "Warning"; }
void error() { std::cout << "Error"; }
void complain(std::string level) {
// Array of member function pointers
void (Harl::*funcs[4])() = {
&Harl::debug,
&Harl::info,
&Harl::warning,
&Harl::error
};
std::string levels[4] = {"DEBUG", "INFO", "WARNING", "ERROR"};
for (int i = 0; i < 4; i++) {
if (level == levels[i]) {
(this->*funcs[i])(); // Call the member function
return;
}
}
}
};
// Declaration
void (ClassName::*pointerName)(parameters);
// Assignment
pointerName = &ClassName::methodName;
// Call via object
(object.*pointerName)(args);
// Call via pointer to object
(objectPtr->*pointerName)(args);

switch (expression) {
case value1:
// code
break;
case value2:
// code
break;
default:
// code if no match
}
// WITHOUT break - falls through to next case
switch (level) {
case 3: // WARNING
std::cout << "Warning message\n";
// No break - falls through!
case 2: // INFO
std::cout << "Info message\n";
// No break - falls through!
case 1: // DEBUG
std::cout << "Debug message\n";
break;
}
// If level = 3: prints Warning, Info, Debug
// If level = 2: prints Info, Debug
// If level = 1: prints Debug
// Can only switch on integral types in C++98
switch (number) { ... } // OK
switch (character) { ... } // OK
switch (string) { ... } // ERROR in C++98!
// For strings, must use if-else
if (str == "DEBUG") { ... }
else if (str == "INFO") { ... }

Since C++98 cannot switch on strings, convert strings to integers first:

// Convert string to index for switch
int getLevel(const std::string& level) {
std::string levels[4] = {"DEBUG", "INFO", "WARNING", "ERROR"};
for (int i = 0; i < 4; i++) {
if (level == levels[i])
return i;
}
return -1; // Not found
}
void complain(const std::string& level) {
switch (getLevel(level)) {
case 0:
std::cout << "Debug message" << std::endl;
break;
case 1:
std::cout << "Info message" << std::endl;
break;
case 2:
std::cout << "Warning message" << std::endl;
break;
case 3:
std::cout << "Error message" << std::endl;
break;
default:
std::cout << "Unknown level" << std::endl;
}
}

This is cleaner than a long if-else chain and allows fall-through behavior.


#include <fstream>
#include <string>
std::ifstream inFile("input.txt");
if (!inFile.is_open()) {
std::cerr << "Cannot open file" << std::endl;
return 1;
}
std::string line;
while (std::getline(inFile, line)) {
std::cout << line << std::endl;
}
inFile.close();
std::ofstream outFile("output.txt");
if (!outFile.is_open()) {
std::cerr << "Cannot create file" << std::endl;
return 1;
}
outFile << "Hello, World!" << std::endl;
outFile << "Line 2" << std::endl;
outFile.close();
std::ifstream inFile("input.txt");
std::string content;
std::string line;
while (std::getline(inFile, line)) {
content += line;
content += "\n";
}

// Single object
Type* ptr = new Type(args);
delete ptr;
// Array
Type* arr = new Type[size];
delete[] arr;
Type& ref = original; // Create alias
// ref is now indistinguishable from original
void (Class::*ptr)(args) = &Class::method;
(object.*ptr)(args);
std::ifstream in("file");
std::ofstream out("file");
std::getline(in, str);
out << content;