Stack and Heap in C++

Memory Segment of Program

The memory that a program uses is typically divided into several segments. Each segment serves a specific purpose and has its own characteristics.

1. Code Segment (Text Segment):

This segment stores the executable code of the program. It is usually read-only and contains the compiled instructions of the program.

  • Description: The text segment contains the executable instructions of the program, i.e., the compiled code. This is where the actual code that runs the program is stored. Since code generally doesn’t change during execution, this segment is usually read-only to prevent accidental modification of instructions.
  • Memory Type: Static (fixed size).
  • Access: Typically read-only, ensuring that the program’s instructions cannot be altered during execution.
  • Example:
    • When you write a function or declare variables, the code that runs these is stored in the text segment.
Example Code:
int add(int a, int b) {
    return a + b;
}

The compiled machine code for the add() function will reside in the text section.

2. Data Segment:

The data segment is where global and static variables are stored. It is further divided into two sub-segments:

The data segment is further divided into two subsegments:

Initialized Data Segment (Static Data Segment):

  • This part of the data segment contains global and static variables that are initialized before the program starts.
  • Description: This sub-segment holds global and static variables that are explicitly initialized by the programmer. The values of these variables are known at compile-time.
  • Memory Type: Static (allocated at program start and deallocated at program exit).
Example:
int globalVar = 42;  // Stored in initialized data segment
static int staticVar = 10;  // Stored in initialized data segment

Uninitialized Data Segment (BSS - Block Started by Symbol):

  • This part contains global and static variables that are initialized to zero or have not been explicitly initialized in the code.
  • Description: Also known as the Block Started by Symbol (BSS), this sub-segment contains global and static variables that are not initialized explicitly. The system initializes these variables to zero by default.
  • Memory Type: Static.
Example:
int uninitializedGlobal;  // Stored in BSS
static int uninitializedStatic;  // Stored in BSS

3. Heap:

The heap is a dynamic memory segment used for dynamic memory allocation during the program's execution. It is managed by the programmer and is not automatically reclaimed.

  • Description: The heap segment is used for dynamic memory allocation. Memory allocated using functions like malloc(), calloc(), realloc() in C or new in C++ is stored in the heap. Unlike stack memory, heap memory is not automatically deallocated when a function returns; the programmer must manually free the memory (using free() in C or delete in C++).
  • Memory Type: Dynamic (managed explicitly by the programmer).
  • Lifetime: Memory on the heap persists until explicitly freed by the programmer, making it prone to memory leaks if not managed properly.
  • Characteristics:
    • Larger than the stack, but access is slower.
    • Useful for long-lived data structures, such as linked lists or trees, whose size is not known at compile time.
Example:
int* p = (int*) malloc(sizeof(int));  // C style (in the heap)
*p = 10;
free(p);  // Don't forget to free memory

int* q = new int(5);  // C++ style (in the heap)
delete q;  // Must delete dynamically allocated memory

4. Stack:

The stack is used for storing local variables and function call information. Each time a function is called, a new stack frame is created, and it is popped off the stack when the function returns. The stack is managed automatically by the compiler.

  • Description: The stack segment is used for storing local variables and function call information. Every time a function is called, a stack frame is created to store the function’s local variables and return address. The stack grows downward (towards lower memory addresses), and when the function returns, its stack frame is destroyed.
  • Memory Type: Dynamic (allocated and deallocated automatically when functions are called and returned from).
  • Lifetime: Variables on the stack are destroyed automatically when they go out of scope.
  • Characteristics:
    • Fast access: Because the stack is managed automatically by the CPU.
    • Size limitation: The stack is usually limited in size, which can lead to issues like stack overflow if too much memory is consumed (e.g., deep recursion).
Example:
void func() {
    int localVar = 5;  // Stored in the stack
}

Each time func() is called, localVar is stored on the stack, and it is removed once the function completes.

6. Constants Segments:

  • This segment contains constant values, often string literals and other constant data. This segment is typically marked as read-only.

What is the Heap Segment?

The heap is a region of a program's memory space that is used for dynamic memory allocation during runtime.

In C++, when you use the new operator to allocate memory, this memory is allocated in application's heap segment.

int* ptr { new int }; // ptr is assigned 4 bytes in the heap
int* array { new int[10] }; // array is assigned 40 bytes in the heap

The address of this memory is passed back by operator new, and can then be stored in a pointer. You do not have to worry about the mechanics behind the process of how free memory is located and allocated to the user.

int* ptr1 { new int };
int* ptr2 { new int };
// ptr1 and ptr2 may not have sequential addresses

When a dynamically allocated variable is deleted, the memory is “returned” to the heap and can then be reassigned future allocation requests are received. Remember that deleting a pointer does not delete the variable. Allocated memory should be freed using delete operator.

int *dynamicArray = new int[10]; // Allocating an integer array of size 10 on the heap
// Perform operations on dynamicArray
delete[] dynamicArray; // Deallocate memory to prevent memory leaks

Advantages of Heap

  1. Because the heap is a big pool of memory, large arrays, structures, or classes can be allocated here.
  2. Allocated memory stays allocated until it is specifically deallocated (beware memory leaks) or the application ends (at which point the OS should clean it up).

Disadvantages of Heap

  1. Allocating memory on the heap is comparatively slow.
  2. Dynamically memory must be accessed through a pointer. Dereferencing a pointer is slower than accessing a variable directly.
  3. Failing to deallocate memory on the heap can lead to memory leaks, where allocated memory is not released, causing the program to consume more and more memory over time.

The Call Stack

The call stack (usually referred to as “the stack”) keeps track of all the active functions (those that have been called but have not yet terminated) from the start of the program to the current point of execution, and handles allocation of all function parameters and local variables.

The call stack is implemented as a data structure.

The call stack segment

The call stack segment holds the memory used for the call stack. When the application starts, the main() function is pushed on the call stack by the operating system. Then the program begins executing.

When a function call is encountered, the function is pushed onto the call stack. When the current function ends, the function is popped off the call stack (this process is sometimes called unwinding the stack). Thus, by looking at the function that are currently on the call stack.

The call stack in action

Let's examine in more detail how the call stack works.

  1. The program encounters a function call.
  2. A stack frame is constructed and pushed on the stack. The stack frame consists of:
    * The address of the instruction beyond the function call (called the return address). This is how the CPU remembers where to return to after the called functions exits.
    * All function arguments.
    * Memory for any local variables.
    * Saved copies of any registers modified by the function that need to be restored when the function returns.
  3. The CPU jumps to the function's start point.
  4. The instructions inside of the function begin executing.

When the function terminates, the following steps happen:

  1. Registers are restored from the call stack.
  2. The stack frame is popped off the stack. This frees the memory for all local variables and arguments.
  3. The return value is handled.
  4. The CPU resumes execution at the return address.

Return values can be handled in a different ways, depending on the computer's architecture. Some architectures include the return value as part of the stack frame. Others use CPU registers.

Stack Overflow

The stack has a limited size, and consequently can only hold a limited amount of information. If the program tries to put too much information on the stack, stack overflow will result. Stack overflow happens when all the memory in the stack has been allocated.

Stack overflow is generally the result of allocating too many variables on the stack, and/or making too many nested function calls (where function A calls function B calls function C calls function D etc…) On modern operating systems, overflowing the stack will generally cause your OS to issue an access violation and terminate the program.

Here is an example program that likely  cause a stack overflow. You can run it on your system and watch it crash:

#include <iostream>

int main()
{
    int stack[10000000];
    std::cout << "hi" << stack[0]; // we'll use stack[0] here so the compiler won't optimize the array away

    return 0;
}

This program tries to allocate a huge (likely 40MB)array on the stack. Because the stack is not large enough to handle this array, the array allocation overflows into portions of memory the program is not allowed to use.

Advantages of Stack

  • Allocating memory on the stack is comparatively fast.
  • All memory allocated on the stack is known at compile time. Consequently, this memory can be accessed directly through a variable.

Disadvantages of Stack

  • Memory allocated on the stack stays in scope as long as it is on the stack. It is destroyed when it is popped off the stack.
  • Because the stack is relatively small, it is generally not a good idea to do anything that eats up lots of stack space. This includes allocating or copying large arrays or other memory-intensive structures.

Common Memory Issues

1 Memory Leaks:

Failure to deallocate memory allocated on the heap can cause memory leaks. This happens when free() or delete is not used properly.

Example:
int* p = new int(10);
// forget to delete

2 Stack Overflow:

If the stack memory is exhausted (e.g., due to deep recursion or large local variables), a stack overflow occurs.

Example:
void recurse() {
    recurse();  // Infinite recursion leads to stack overflow
}

3 Dangling Pointers:

Occurs when a pointer points to a memory location that has been freed. Accessing this pointer can lead to undefined behavior.

Example:
int* p = new int(10);
delete p;
*p = 20;  // Dangling pointer!