Skip to content

Module 01 Tutorial

Prerequisites: Review Module 01 Concepts first.

Module 01 introduces memory management in C++. You’ll learn the critical difference between stack and heap allocation, understand references as aliases, and master pointers to member functions.


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
  • FORBIDDEN: if/else chains

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