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 memory is crucial for writing correct, efficient C++ code. Here’s why these concepts matter:

The stack is fast but limited—like a small desk where you work on the current task. The heap is larger but requires manual management—like a warehouse where you store long-term items.

References are safer aliases for existing variables. They can’t be null and can’t be reassigned. Use them when you need to pass large objects efficiently without copying.

In C++, new and delete do more than just allocate memory—they call constructors and destructors. This ensures objects are properly initialized and cleaned up.


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";
}

This exercise asks you to create a Zombie class with two functions:

  • newZombie(std::string name): Creates a zombie on the HEAP, returns a pointer
  • randomChump(std::string name): Creates a zombie on the STACK, announces itself

The key insight: when should memory survive beyond a function call?

Step 1 - Understand the lifetime requirement

Ask yourself: “Does the caller need the zombie after the function returns?”

  • newZombie: YES → Heap allocation (survives function return)
  • randomChump: NO → Stack allocation (auto-cleanup)

Step 2 - Design the Zombie class

  • Private _name attribute
  • Constructor that sets name
  • Destructor that prints “[name] is dead”
  • announce() method that prints “[name]: BraiiiiiiinnnzzzZ…”

Step 3 - Implement both allocation strategies

Stage 1 - Zombie class:

Zombie.hpp
#ifndef ZOMBIE_HPP
#define ZOMBIE_HPP
#include <string>
class Zombie {
private:
std::string _name;
public:
Zombie(std::string name);
~Zombie();
void announce() const;
};
#endif

Stage 2 - Implementation:

Zombie.cpp
#include "Zombie.hpp"
#include <iostream>
Zombie::Zombie(std::string name) : _name(name) {}
Zombie::~Zombie() {
std::cout << _name << " is dead." << std::endl;
}
void Zombie::announce() const {
std::cout << _name << ": BraiiiiiiinnnzzzZ..." << std::endl;
}

Stage 3 - The two allocation functions:

newZombie.cpp
#include "Zombie.hpp"
Zombie* newZombie(std::string name) {
return new Zombie(name); // Heap - caller must delete!
}
randomChump.cpp
#include "Zombie.hpp"
void randomChump(std::string name) {
Zombie zombie(name); // Stack - auto destroyed at function end
zombie.announce();
}
// Destructor called HERE automatically
LineCodeWhy
new Zombie(name)Allocates on heapMemory survives function return
return new Zombie(...)Returns pointerCaller gets ownership
Zombie zombie(name)Stack allocationFast, automatic cleanup
End of randomChumpDestructor runsStack variables cleaned up

1. Memory leaks with newZombie

// WRONG - memory leak!
newZombie("Bob"); // Created but never deleted
// RIGHT
Zombie* z = newZombie("Bob");
z->announce();
delete z; // Clean up!

2. Returning stack pointer (undefined behavior)

// WRONG - returning address of dead object!
Zombie* badZombie(std::string name) {
Zombie z(name);
return &z; // z dies here, pointer is dangling!
}

3. Forgetting destructor message

The subject requires destructor to print. Evaluators will check!

Terminal window
# Compile
c++ -Wall -Wextra -Werror *.cpp -o zombie
# Test heap zombie
./zombie
# Should see: announce message, then later "X is dead" when deleted
# Watch for memory leaks with valgrind
valgrind --leak-check=full ./zombie
Zombie.hpp
#ifndef ZOMBIE_HPP
#define ZOMBIE_HPP
#include <string>
class Zombie {
private:
std::string _name;
public:
Zombie(std::string name);
~Zombie();
void announce() const;
};
Zombie* newZombie(std::string name);
void randomChump(std::string name);
#endif

Create a function zombieHorde(int N, std::string name) that:

  • Allocates N zombies in a single allocation
  • Initializes all zombies with the same name
  • Returns a pointer to the first zombie

Key constraint: ONE allocation, not N separate new calls.

Step 1 - Use array allocation syntax

new Zombie[N] allocates N zombies contiguously.

Step 2 - Handle the default constructor requirement

Array allocation calls default constructor. You need to add one!

Step 3 - Initialize names after allocation

Since default constructor can’t take parameters, add a setName() method.

Stage 1 - Add default constructor and setter:

Zombie.hpp
class Zombie {
private:
std::string _name;
public:
Zombie(); // Default constructor for array allocation
Zombie(std::string name);
~Zombie();
void setName(std::string name); // Set name after construction
void announce() const;
};

Stage 2 - Implement zombieHorde:

zombieHorde.cpp
#include "Zombie.hpp"
Zombie* zombieHorde(int N, std::string name) {
if (N <= 0)
return NULL;
// Single allocation for all N zombies
Zombie* horde = new Zombie[N];
// Initialize each zombie's name
for (int i = 0; i < N; i++)
horde[i].setName(name);
return horde;
}
LineCodeWhy
new Zombie[N]Array allocationSingle contiguous block
horde[i].setName(name)Initialize eachDefault constructor left name empty
return hordeReturn first elementArray decays to pointer

1. Using delete instead of delete[]

Zombie* horde = new Zombie[N];
// WRONG - undefined behavior!
delete horde;
// RIGHT - must use delete[] for arrays
delete[] horde;

2. Making N separate allocations

// WRONG - N allocations, not one!
for (int i = 0; i < N; i++)
horde[i] = new Zombie(name);
// RIGHT - single allocation
Zombie* horde = new Zombie[N];

3. Returning stack array

// WRONG - stack array dies at return!
Zombie* zombieHorde(int N, std::string name) {
Zombie horde[N]; // VLA on stack (also non-standard!)
return horde; // Dangling pointer!
}
Terminal window
# Test horde creation and cleanup
int main() {
Zombie* horde = zombieHorde(5, "Walker");
for (int i = 0; i < 5; i++)
horde[i].announce();
delete[] horde; // Should see 5 "is dead" messages
return 0;
}
zombieHorde.cpp
#include "Zombie.hpp"
Zombie* zombieHorde(int N, std::string name) {
if (N <= 0)
return NULL;
Zombie* horde = new Zombie[N];
for (int i = 0; i < N; i++)
horde[i].setName(name);
return horde;
}

Demonstrate that references are aliases by showing:

  • A string variable
  • A pointer to that string
  • A reference to that string

All three should print the same address and same value.

This is a simple demonstration. The key insight: a reference IS the original variable, just with another name.

main.cpp
#include <iostream>
#include <string>
int main() {
std::string str = "HI THIS IS BRAIN";
std::string* stringPTR = &str; // Pointer stores address
std::string& stringREF = str; // Reference IS str
// Print addresses - all should be identical
std::cout << "Address of str: " << &str << std::endl;
std::cout << "Address in stringPTR: " << stringPTR << std::endl;
std::cout << "Address of stringREF: " << &stringREF << std::endl;
// Print values - all should be identical
std::cout << "Value of str: " << str << std::endl;
std::cout << "Value via stringPTR: " << *stringPTR << std::endl;
std::cout << "Value of stringREF: " << stringREF << std::endl;
return 0;
}
LineCodeWhy
std::string* stringPTR = &strStore addressPointer holds memory address
std::string& stringREF = strCreate aliasReference IS the original
&strGet addressAddress-of operator
&stringREFGet addressSame as &str!
*stringPTRDereferenceGet value at address
stringREFUse directlyNo dereference needed

1. Confusing & meanings

// In declaration: & means "reference type"
std::string& ref = str;
// In expression: & means "address of"
std::cout << &str;

2. Thinking reference is a copy

std::string& ref = str;
ref = "CHANGED";
// str is also "CHANGED" - they're the same thing!
Terminal window
./brain
# Output should show same address 3 times
# Output should show same value 3 times
main.cpp
#include <iostream>
#include <string>
int main() {
std::string str = "HI THIS IS BRAIN";
std::string* stringPTR = &str;
std::string& stringREF = str;
std::cout << &str << std::endl;
std::cout << stringPTR << std::endl;
std::cout << &stringREF << std::endl;
std::cout << str << std::endl;
std::cout << *stringPTR << std::endl;
std::cout << stringREF << std::endl;
return 0;
}

Create classes demonstrating when to use reference vs pointer as class member:

  • HumanA: Always has a weapon → use reference
  • HumanB: Might not have a weapon → use pointer

The weapon is shared - when it changes, both humans see the change.

Step 1 - Understand the design decision

HumanAHumanB
Must have weapon at constructionMay not have weapon
Weapon never changesWeapon can be set later
Use Weapon& (reference)Use Weapon* (pointer)

Step 2 - Reference initialization rule

References MUST be initialized at construction. Cannot be NULL.

Step 3 - Pointer can be NULL

Pointers can start as NULL and be assigned later.

Stage 1 - Weapon class:

Weapon.hpp
#ifndef WEAPON_HPP
#define WEAPON_HPP
#include <string>
class Weapon {
private:
std::string _type;
public:
Weapon(std::string type);
const std::string& getType() const;
void setType(std::string type);
};
#endif

Stage 2 - HumanA with reference:

HumanA.hpp
class HumanA {
private:
std::string _name;
Weapon& _weapon; // Reference - must always exist
public:
HumanA(std::string name, Weapon& weapon);
void attack() const;
};
HumanA.cpp
HumanA::HumanA(std::string name, Weapon& weapon)
: _name(name), _weapon(weapon) {} // MUST use initializer list!
void HumanA::attack() const {
std::cout << _name << " attacks with their " << _weapon.getType() << std::endl;
}

Stage 3 - HumanB with pointer:

HumanB.hpp
class HumanB {
private:
std::string _name;
Weapon* _weapon; // Pointer - can be NULL
public:
HumanB(std::string name);
void setWeapon(Weapon& weapon);
void attack() const;
};
HumanB.cpp
HumanB::HumanB(std::string name) : _name(name), _weapon(NULL) {}
void HumanB::setWeapon(Weapon& weapon) {
_weapon = &weapon;
}
void HumanB::attack() const {
if (_weapon)
std::cout << _name << " attacks with their " << _weapon->getType() << std::endl;
else
std::cout << _name << " has no weapon!" << std::endl;
}
LineCodeWhy
Weapon& _weaponReference memberMust exist, can’t be NULL
: _weapon(weapon)Initializer listReferences MUST be initialized here
Weapon* _weaponPointer memberCan be NULL, set later
_weapon = &weaponStore addressConvert reference to pointer
if (_weapon)NULL checkPointer might be unset
_weapon->getType()Arrow operatorDereference + member access

1. Not using initializer list for reference

// WRONG - reference not initialized!
HumanA::HumanA(std::string name, Weapon& weapon) {
_name = name;
_weapon = weapon; // ERROR: _weapon already needed to exist!
}
// RIGHT - use initializer list
HumanA::HumanA(std::string name, Weapon& weapon)
: _name(name), _weapon(weapon) {}

2. Not checking NULL for pointer

// WRONG - crashes if no weapon!
void HumanB::attack() const {
std::cout << _weapon->getType(); // NULL dereference!
}
// RIGHT - check first
if (_weapon)
std::cout << _weapon->getType();
Terminal window
# Test that weapon changes affect both humans
int main() {
Weapon club("crude spiked club");
HumanA bob("Bob", club);
HumanB jim("Jim");
jim.setWeapon(club);
bob.attack();
jim.attack();
club.setType("some other type of club");
bob.attack(); // Should show new weapon!
jim.attack(); // Should show new weapon!
}
Weapon.cpp
#include "Weapon.hpp"
Weapon::Weapon(std::string type) : _type(type) {}
const std::string& Weapon::getType() const {
return _type;
}
void Weapon::setType(std::string type) {
_type = type;
}

Create a program that replaces all occurrences of string s1 with s2 in a file:

  • Read file content
  • Replace ALL occurrences (not just first)
  • Write to <filename>.replace
  • FORBIDDEN: std::string::replace() function

Step 1 - Read entire file into string

Use std::ifstream and std::getline() to read line by line.

Step 2 - Implement replacement without replace()

Use find() to locate occurrences, append() or substr() to build result.

Step 3 - Write to output file

Use std::ofstream to create <filename>.replace.

Stage 1 - The replacement algorithm:

std::string replaceAll(const std::string& content,
const std::string& s1,
const std::string& s2) {
if (s1.empty())
return content; // Avoid infinite loop!
std::string result;
std::size_t pos = 0;
std::size_t found;
while ((found = content.find(s1, pos)) != std::string::npos) {
result.append(content, pos, found - pos); // Before match
result.append(s2); // Replacement
pos = found + s1.length(); // Move past match
}
result.append(content, pos, std::string::npos); // Remainder
return result;
}

Stage 2 - File I/O:

main.cpp
int main(int argc, char** argv) {
if (argc != 4) {
std::cerr << "Usage: " << argv[0] << " <file> <s1> <s2>" << std::endl;
return 1;
}
std::string filename = argv[1];
std::ifstream inFile(filename.c_str()); // .c_str() for C++98
if (!inFile.is_open()) {
std::cerr << "Error: Cannot open file" << std::endl;
return 1;
}
// Read file content
std::string content;
std::string line;
bool first = true;
while (std::getline(inFile, line)) {
if (!first)
content += '\n';
content += line;
first = false;
}
inFile.close();
// Replace and write
std::string result = replaceAll(content, argv[2], argv[3]);
std::ofstream outFile((filename + ".replace").c_str());
outFile << result;
outFile.close();
return 0;
}
LineCodeWhy
content.find(s1, pos)Find next occurrenceStart searching from pos
std::string::npos”Not found” valueReturned when no match
result.append(content, pos, found - pos)Append substringEverything before match
pos = found + s1.length()Skip past matchDon’t re-find same occurrence
.c_str()String to C-stringC++98 file streams need this

1. Using forbidden std::string::replace()

// WRONG - forbidden!
str.replace(pos, s1.length(), s2);
// RIGHT - use find + append
result.append(content, pos, found - pos);
result.append(s2);

2. Not handling empty s1

// Without this check: infinite loop!
if (s1.empty())
return content;

3. Only replacing first occurrence

// WRONG - only finds first
size_t pos = content.find(s1);
// ... replace once
// RIGHT - loop until no more found
while ((found = content.find(s1, pos)) != std::string::npos) {
// ... replace each
}
Terminal window
# Create test file
echo "Hello World World" > test.txt
# Replace "World" with "42"
./sed test.txt World 42
# Check result
cat test.txt.replace
# Should show: "Hello 42 42"
main.cpp
#include <iostream>
#include <fstream>
#include <string>
std::string replaceAll(const std::string& content,
const std::string& s1,
const std::string& s2) {
if (s1.empty())
return content;
std::string result;
std::size_t pos = 0;
std::size_t found;
while ((found = content.find(s1, pos)) != std::string::npos) {
result.append(content, pos, found - pos);
result.append(s2);
pos = found + s1.length();
}
result.append(content, pos, std::string::npos);
return result;
}
int main(int argc, char** argv) {
if (argc != 4) {
std::cerr << "Usage: " << argv[0] << " <file> <s1> <s2>" << std::endl;
return 1;
}
std::ifstream inFile(argv[1]);
if (!inFile.is_open()) {
std::cerr << "Error: Cannot open file" << std::endl;
return 1;
}
std::string content, line;
bool first = true;
while (std::getline(inFile, line)) {
if (!first) content += '\n';
content += line;
first = false;
}
inFile.close();
std::string result = replaceAll(content, argv[2], argv[3]);
std::ofstream outFile((std::string(argv[1]) + ".replace").c_str());
outFile << result;
outFile.close();
return 0;
}

Create a Harl class that complains at different levels:

  • DEBUG, INFO, WARNING, ERROR
  • Subject requirement: avoid a “forest” of if/else if/else (use pointers to member functions)

The solution: use pointers to member functions.

Step 1 - Declare member function pointer array

void (Harl::*funcs[4])();

This declares an array of 4 pointers to Harl member functions.

Step 2 - Map levels to functions

Create parallel arrays: one for function pointers, one for level strings.

Step 3 - Loop and match

Find the matching level, call the corresponding function.

Stage 1 - Harl class structure:

Harl.hpp
#ifndef HARL_HPP
#define HARL_HPP
#include <string>
class Harl {
private:
void debug();
void info();
void warning();
void error();
public:
void complain(std::string level);
};
#endif

Stage 2 - Using function pointers:

Harl.cpp
void Harl::complain(std::string level) {
// Array of member function pointers
void (Harl::*funcs[4])() = {
&Harl::debug,
&Harl::info,
&Harl::warning,
&Harl::error
};
// Parallel array of level names
std::string levels[4] = {"DEBUG", "INFO", "WARNING", "ERROR"};
// Find and call
for (int i = 0; i < 4; i++) {
if (level == levels[i]) {
(this->*funcs[i])(); // Call member function via pointer
return;
}
}
}
LineCodeWhy
void (Harl::*funcs[4])()Declare array4 pointers to Harl methods
&Harl::debugGet function addressAddress-of member function
(this->*funcs[i])()Call via pointerDereference + call

1. Using if/else forest

// WRONG - forbidden!
if (level == "DEBUG")
debug();
else if (level == "INFO")
info();
// ...
// RIGHT - use function pointers
(this->*funcs[i])();

2. Wrong syntax for member function pointer call

// WRONG
funcs[i](); // Missing this
this->funcs[i](); // Wrong syntax
*funcs[i](); // Wrong syntax
// RIGHT
(this->*funcs[i])();
Terminal window
./harl DEBUG
# Should print debug message
./harl ERROR
# Should print error message
./harl INVALID
# Should print nothing (or handle gracefully)
Harl.cpp
#include "Harl.hpp"
#include <iostream>
void Harl::debug() {
std::cout << "[ DEBUG ]" << std::endl;
std::cout << "I love having extra bacon..." << std::endl;
}
void Harl::info() {
std::cout << "[ INFO ]" << std::endl;
std::cout << "I cannot believe adding extra bacon costs more..." << std::endl;
}
void Harl::warning() {
std::cout << "[ WARNING ]" << std::endl;
std::cout << "I think I deserve to have some extra bacon for free..." << std::endl;
}
void Harl::error() {
std::cout << "[ ERROR ]" << std::endl;
std::cout << "This is unacceptable! I want to speak to the manager now." << std::endl;
}
void Harl::complain(std::string level) {
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])();
return;
}
}
}

Create a log filter using switch with fall-through:

  • Given a minimum level, print that level AND all levels above it
  • DEBUG → prints DEBUG, INFO, WARNING, ERROR
  • WARNING → prints WARNING, ERROR
  • Invalid → prints special message

Step 1 - Convert string to int

C++98 can’t switch on strings. Convert level name to index (0-3).

Step 2 - Use fall-through

Don’t use break between cases - let execution “fall through” to subsequent cases.

Stage 1 - Convert level to int:

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; // Invalid level
}

Stage 2 - Switch with fall-through:

switch (getLevel(argv[1])) {
case 0:
harl.debug();
// NO BREAK - fall through!
case 1:
harl.info();
// NO BREAK - fall through!
case 2:
harl.warning();
// NO BREAK - fall through!
case 3:
harl.error();
break; // Only break at the end
default:
std::cout << "[ Probably complaining about insignificant problems ]" << std::endl;
}
LineCodeWhy
case 0:DEBUG levelEntry point for DEBUG
No breakFall-throughContinue to next case
case 3: ... break;ERROR levelStop after ERROR
default:Invalid levelCatch-all

1. Adding break to each case

// WRONG - doesn't filter correctly!
case 0:
harl.debug();
break; // Stops here, doesn't show INFO/WARNING/ERROR
// RIGHT - no break for fall-through
case 0:
harl.debug();
// fall through
case 1:
harl.info();

2. Trying to switch on string

// WRONG - C++98 doesn't support this!
switch (level) { // level is std::string
case "DEBUG":
// ...
}
// RIGHT - convert to int first
switch (getLevel(level)) {
case 0:
// ...
}
Terminal window
./harlFilter DEBUG
# Should print: DEBUG, INFO, WARNING, ERROR
./harlFilter WARNING
# Should print: WARNING, ERROR
./harlFilter ERROR
# Should print: ERROR only
./harlFilter INVALID
# Should print: [ Probably complaining about insignificant problems ]
main.cpp
#include "Harl.hpp"
#include <iostream>
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;
}
int main(int argc, char** argv) {
if (argc != 2) {
std::cerr << "Usage: " << argv[0] << " <level>" << std::endl;
return 1;
}
Harl harl;
switch (getLevel(argv[1])) {
case 0:
harl.debug();
case 1:
harl.info();
case 2:
harl.warning();
case 3:
harl.error();
break;
default:
std::cout << "[ Probably complaining about insignificant problems ]" << std::endl;
}
return 0;
}

// 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;

Continue your C++ journey:

Visit the Glossary for definitions of: