Extension: Stack and Heap Memory

Volatile and Non-volatile Memory

Computer memory is the hardware the does the storage function in the four input, output, storage and processing functions of a computer.

Memory comes in two flavors, volatile memory, often refered to as RAM, and non-volatile memory. Unlike volatile memory, non-volatile memory does not loose its state when the computer’s power is turned off. It is also orders of magnitude both slower and cheaper than RAM is, so it is used for long term and large storage while RAM is what is mostly used as the workspace for running programs.

Both data and instructions are stored in each type of computer memory.

Memory Management for Volatile Memory

Now that we have been introduced to pointers, we will need to begin to develop a basic understanding of memory management, and to be able to distinguish between stack-based memory allocation and memory on what we call the heap.

Stack Memory

Let’s take a look at the following program, stack1.cpp.

#include <iostream>
using namespace std;

int* make_on_stack() {
    int local = 42;  // lives on the stack
    cout << "Inside make_on_stack():" << endl;
    cout << "  &local    = " << &local << endl;
    cout << "  local     = " << local << endl;
    return &local;   // BAD!!returning stack address - DANGLING!
}

int main() {
    int* p = make_on_stack();
    cout << "\nBack in main():" << endl;
    cout << "  p (stale address) = " << p << endl;
    // The memory at p is no longer valid. Accessing it is undefined behavior.
    // cout << *p << endl; // WARNING!!! DO NOT uncomment - undefined behavior
    return 0;
}

Compiling this on our computer with:

$ g++ stack1.cpp

gave us a warning:

stack1.cpp: In function ‘int* make_on_stack()’:
stack1.cpp:9:12: warning: address of local variable ‘local’ returned [-Wreturn-local-addr]
    9 |     return &local;   // BAD!!returning stack address — DANGLING!
      |            ^~~~~~
stack1.cpp:5:9: note: declared here
    5 |     int local = 42;  // lives on the stack
      |         ^~~~~

and then running it gave:

Inside make_on_stack():
  &local    = 0x7ffd7d80371c
  local     = 42

Back in main():
  p (stale address) = 0

Our compiler, GCC 16, is actually returning a null pointer, 0, and not a dangling pointer, as the comment suggests. If it had returned the stack address of local, 0x7ffd7d80371c, that would indeed have been a dangling pointer. That address would have been removed from the stack automatically when make_on_stack returned, and dereferencing it would lead to unpredictable behavior. Dereferencing the null pointer leads to a much safer segmentation fault. Try this on your machine. Uncomment the line main that warns you not to, and see what happens on your machine.

The big takaway here is that local variables declared inside a function are stored automatically in stack memory, and the memory they use is freed automatically when the function returns by removing them from the stack. They are no longer available to other parts of the program.

Heap Memory

So what if we want to set some value in memory inside a function and have it accessible in some other part of our program?

For that we need to use heap memory. Take a look at the following program, heap1.cpp.

#include <iostream>
using namespace std;

int* make_on_heap() {
    int* p = new int(42); // allocates on the heap
    cout << "Inside make_on_heap():" << endl;
    cout << "  p (heap addr) = " << p << endl;
    cout << "  *p            = " << *p << endl;
    return p;  // SAFE: heap outlives the function
}

int main() {
    int* q = make_on_heap();
    cout << "\nBack in main():" << endl;
    cout << "  q  = " << q << endl;
    cout << "  *q = " << *q << endl; // still valid
    delete q;  // must free it, or you will LEAK memory!
    return 0;
}

Compiling and running this on our computer gave us:

Inside make_on_heap():
  p (heap addr) = 0x55bfe868c020
  *p            = 42

Back in main():
  q  = 0x55bfe868c020
  *q = 42
#include <iostream>
using namespace std;

void show_addresses() {
    int stack_var = 10;           // on the stack
    int* heap_var  = new int(20); // on the heap

    cout << "stack variable: address = " << &stack_var
         << ", value = " << stack_var << endl;
    cout << "heap variable:  address = " << heap_var
         << ", value = " << *heap_var << endl;
    cout << "  (pointer itself lives at " << &heap_var << ")" << endl;
    delete heap_var;
}

int main() {
    cout << "Call 1:" << endl;
    show_addresses();
    cout << "\nCall 2:" << endl;
    show_addresses();
    return 0;
}
#include <iostream>
using namespace std;

void f(int depth) {
    int x = depth;
    cout << "  f(" << depth << ") : &x = " << &x << endl;
    if (depth > 0) f(depth - 1);
}

int main() {
    f(3);
    cout << "(call f again)\n";
    f(3);
    return 0;
}

Resources