University of Wales Trinity Saint David
ACCB5017 - Games Architecture and Mechanics

Stack vs Heap Allocation

Overview

This lab uses a small console program to make the two places a C++ program stores its data directly visible: the stack, where every local variable lives until its enclosing scope ends, and the heap, where memory outlives any single function and has to be released by hand. Starting from a working demo that allocates a single int and a Person object on the heap, you will add stack-allocated equivalents alongside them, print addresses from both regions to compare them, watch a constructor and destructor fire at different points depending on where an object lives, and finally track down a memory leak caused by a missing delete.

Learning Objectives

By the end of this lab you will be able to:

Concept

A C++ program needs somewhere to put every variable it creates, and it has exactly two places to choose from: the stack and the heap. Get this choice wrong and a program either loses data the moment a function returns, because a local variable stops existing as soon as its enclosing block ends, or it leaves memory reserved forever because nothing ever frees it. Neither failure produces a compiler error, so the difference has to be understood, not discovered by trial and error.

The stack is a fixed block of memory reserved for the current thread, and it behaves exactly like the data structure it is named after, or like a stack of plates in a kitchen cupboard. every function call pushes a new frame holding that function's local variables onto the top, the way a clean plate gets added to the top of the stack, and returning from the function pops that frame straight back off and reclaims its memory instantly, the way the top plate gets lifted off again. nothing is ever added or removed anywhere except the top, so there is never any question of where a stack variable is or how long it takes to find. Because a stack variable's memory is tied to the scope it was declared in, it has automatic storage duration. it is created and destroyed for you. The heap, by contrast, is a much larger pool of memory that any part of the program can request from at any time through dynamic allocation, more like a spare bedroom than a cupboard. things get put wherever there happens to be room, in whatever order they arrive, with no tidy top-of-the-stack rule governing any of it, and nothing about it is tied to a function's scope, so nothing about it is cleaned up automatically either. the only way to find something in the heap again later is to have kept hold of exactly where you put it, which is precisely what a pointer is for.

STACK Frame A (oldest) Frame B Frame C (top) added and removed only at the top HEAP object object object object object placed wherever there is room, in any order

That push-and-pop rule is not just an analogy, it is exactly what happens while code runs. Every function call pushes a new stack frame holding that function's local variables and parameters, and every return pops the most recently pushed frame straight back off. Step through the short example below, a chain of three calls, to watch it happen. the highlighted line in the code tracks exactly what is executing at each step.

int main() {
    calculateTotal();
}

void calculateTotal() {
    calculateArea(5);
}

int calculateArea(int side) {
    return side * side;
}
stack grows upward from here

A pointer is what connects a piece of code to a heap allocation, and it is simply a variable that stores a memory address instead of an ordinary value, declared with a type followed by an asterisk, such as int* h. Two operators move between a pointer and the thing it targets, and they are opposites. the address-of operator & takes an existing variable and produces its address, as in &stackInt, which is how a pointer can be made to target something that already exists, on the stack or the heap. the dereference operator * does the reverse. given a pointer, it reaches through the stored address to read or write the value sitting there, as in *h = 20, which leaves h itself pointing at the same place but changes the int at that address. The symbol * means two different things depending on where it appears. in a declaration, int* h, it says "h is a pointer to an int". in an expression, *h = 20, it means "the int that h points to".

STACK h a pointer HEAP int the object h points to stores the address of the pointer variable itself lives on the stack, the memory it points to lives on the heap

Telling the two apart in source code has nothing to do with a variable's type and everything to do with how it is declared. A declaration that includes new somewhere in it, such as Person* peter = new Person();, puts the object on the heap, even though peter itself, the pointer holding that object's address, lives on the stack like any other local variable. A declaration with no new anywhere in it, such as Person stackPerson;, puts the entire object on the stack, with no separate heap request involved at all. A pointer type such as Person* is not by itself evidence of heap allocation, it only means the variable holds an address. the only thing that determines where the pointee lives is whether new produced that address.

Knowing how to tell stack and heap apart still leaves the practical question. given a free choice, why put something on the heap at all instead of just declaring it on the stack? Three situations force the choice. the first is size. the stack is a small, fixed block of memory, often only a few megabytes for an entire thread, so a large array declared directly on the stack, such as Person team[1000];, can exhaust it outright and crash the program with a stack overflow before a single line of useful work runs. The heap is far larger, limited mainly by how much memory the machine actually has, so a large array or a large number of objects, a level's worth of enemies in a game, belongs there instead, built with new Person[1000]. the second is lifetime. a stack object is destroyed the instant its function returns, so a function that needs to hand an object back to whoever called it, or store it somewhere that outlives the function entirely, such as inside a container owned by a different object, cannot return a pointer to a local stack variable. that pointer would point at memory that no longer belongs to anything the moment the function returns, a dangling pointer. only a heap allocation survives past the function that created it. the third is size unknown until the program is already running. a plain stack array's size has to be fixed before the program runs, but the number of enemies to spawn, or the number of lines read from a file, is usually only known once the program is already executing, and only new can request exactly that many elements at runtime.

The tool for requesting heap memory is the new keyword, which returns a pointer, an address, to the memory it reserved. Whoever holds that pointer is responsible for calling delete on it exactly once, and every single new in a program needs exactly one matching delete somewhere, no more and no fewer. forget it and the memory is never released, a memory leak. A leak rarely causes any visible problem right away, since the operating system reclaims all of a process's memory the moment it exits anyway, so a short console program that leaks once and then exits can look perfectly fine. The real damage shows up in a long-running program, such as a game that allocates a new object every frame without a matching delete, where each leaked allocation stays reserved for the rest of the run and the total leaked memory keeps growing the longer the program runs, eventually exhausting available memory and causing an allocation to fail or the process to be killed. Because a leak produces no compiler error and no immediate crash, treat every new as unfinished business until its matching delete exists somewhere in the code. A class's destructor, written as ~ClassName(), is where that matching delete usually belongs, since it runs automatically no matter where the object holding it was allocated.

Tasks

Task 1 - Compare a stack address to a heap address

Open StackVsHeap.cpp. It already allocates one int and one Person on the heap through the pointers h and peter. Add a stack-allocated version of each directly alongside them, then print all four addresses so you can compare a stack address to a heap address in the same run.

  1. Declare a stack-allocated int called stackInt, initialised to 20, using plain int stackInt = 20;. no new, no pointer.
  2. Declare a stack-allocated Person called stackPerson, using plain Person stackPerson;. again, no new, no pointer.
  3. Print the address of stackInt with std::cout << &stackInt, and the address of stackPerson with std::cout << &stackPerson, following the same pattern used below for h and peter.

The diagram below shows where each variable ends up once your two new lines are in place. h and peter stay on the stack as pointers, but the memory they point to is on the heap. stackInt and stackPerson are the two variables you are adding, and both live entirely on the stack, no arrow leaves the stack box for either of them.

StackVsHeap.cpp
#include <iostream>
#include "Person.h"

int main()
{
    // Heap allocation: h itself is a stack variable, but new int reserves
    // space on the heap and returns its address.
    int* h = new int;
    *h = 20;

    // Heap allocation: peter is a stack variable holding the address of a
    // Person object that was constructed on the heap.
    Person* peter = new Person();

    // TODO: declare a stack-allocated int called stackInt, initialised to 20

    // TODO: declare a stack-allocated Person called stackPerson

    std::cout << "heap int address (h):        " << h << std::endl;
    std::cout << "heap Person address (peter): " << peter << std::endl;
    // TODO: print the address of stackInt with std::cout << &stackInt
    // TODO: print the address of stackPerson with std::cout << &stackPerson

    delete h;
    delete peter;
}

Success condition: the program compiles and prints four addresses. the two stack addresses (stackInt, stackPerson) are close together and clearly separated from the two heap addresses (h, peter), matching the diagram above.

Task 2 - Watch construction and destruction happen

Printing addresses shows where each object lives, but not when it is created or destroyed. Open Person.cpp and add a print statement to the constructor and another to the destructor, so you can watch both events happen as the program runs.

  1. In Person::Person(), after age is allocated and set, print "Person constructed at " followed by this.
  2. In Person::~Person(), before age is freed, print "Person destroyed at " followed by this.
  3. Rebuild and run the program from Task 1 again.
Person.cpp
#include "Person.h"
#include <iostream>

Person::Person()
{
    this->age = new int;
    *this->age = 5;

    // TODO: print "Person constructed at " followed by this
}

Person::~Person()
{
    // TODO: print "Person destroyed at " followed by this

    delete this->age;
}

Success condition: the run now prints two Person constructed lines near the top, one for peter and one for stackPerson, in the order they are declared in main(). It then prints two Person destroyed lines, but not in the same order they were constructed. the line for peter prints exactly where delete peter; runs, while the line for stackPerson prints last, automatically, after main()'s closing brace, even though stackPerson is never passed to delete anywhere in the file.

Task 3 - Find and fix the missing delete

The version of StackVsHeap.cpp below was checked in by a teammate who was refactoring the file from Task 1 and Task 2. Build and run it as it stands.

Two Person constructed lines print as expected, but only one Person destroyed line ever appears, for stackPerson. The program still exits normally. no crash, no error message. peter's object is constructed, printed, and then simply never reported as destroyed.

  1. Compare this file line by line against the version you built in Task 1 and Task 2.
  2. Find what your teammate's refactor left out, and add it back in the right place.
StackVsHeap.cpp
#include <iostream>
#include "Person.h"

int main()
{
    int* h = new int;
    *h = 20;

    Person* peter = new Person();

    int stackInt = 20;
    Person stackPerson;

    std::cout << "heap int address (h):        " << h << std::endl;
    std::cout << "heap Person address (peter): " << peter << std::endl;
    std::cout << "stack int address:           " << &stackInt << std::endl;
    std::cout << "stack Person address:        " << &stackPerson << std::endl;

    delete h;
}

Success condition: after your fix, the run once again prints two Person destroyed lines, in the order established in Task 2, one for peter exactly where its delete runs, and one for stackPerson automatically after that.

Solution

Try the tasks above before revealing the solution.