Objectives

Upon completion of this short, you will:

  • understand the difference between local and global scope
  • know how C manages scope
  • see where C allocates memory for different types of variables
  • learn how a program is loaded and executed

Overview

In C, memory allocation is a critical aspect of programming, and it can be done in several ways. Memory in C is typically allocated in several areas: the stack, the heap, the text segment, and the data segment. The text and data segment are often collectively referred to as the text segment or the static segment (as it cannot be changed at run-time).

When compiling a C/C++ program, the C compiler (often gcc or clang) translates the C code into machine language instructions. Next, the linker combines the compiled binary from the compiler with pre-compiled system libraries (such as “stdio”) and forms a binary executable file: the program.

When we execute the program, the operating system creates a new process (through fork() followed by an exec()) and loads the executable’s code into that new process’ memory. The binary file of the executable is loaded into RAM in a particular manner.

After being loaded into the memory (RAM), the memory layout of a C Program has six components: the text segment containing the program’s instructions, the initialized data segment, the uninitialized data segment, the command-line arguments, the stack, and the heap. Each of these six different segments has its own read and write permissions. If a program tries to access memory in any segment differently than it is supposed to, it results in a segmentation fault error (segfault or core dump 1).

Memory Areas

The memory layout for a C/C++ program is comprised of six distinct areas: heap, stack, code segment, command-line arguments, uninitialized and initialized data segments. Each of these segments has separate read and write permissions and writing into a area for which the program does not have permission results in a segmentation fault and the program “crashes” – a process often referred to as “dumping core”. The figure below illustrates the general memory layout.

Scope vs Extent

In C, “scope” refers to the region or context in which a variable or identifier is valid and can be used. It determines where in your code a particular variable or identifier is visible and accessible. “Extent” refers to the lifetime of a variable’s memory and the time from which memory is allocated to the time when the memory is deallocated.

Understanding the scope and extent of variables and identifiers is crucial for writing correct and maintainable C programs. Scope and extent are not always the same. A variable can be allocated (it has “extent”) but not be currently accessible. For example, a local variable declared as static within a function has an extent that is the same as the program, i.e., the memory is allocated upon loading of the program and remains allocated until the program exits. However, the variable can only be accessed within the function. Of course, it is possible to get a “pointer” to the variable’s memory and manipulate the variable’s contents outside the function, but the fact remains that the variable’s scope is local to the function, even though the memory remains allocated whether the function is active or not.

There are several levels of scope in C:

  1. Block Scope:

    • Variables declared within a block of code, such as within a function or a compound statement enclosed in curly braces {}, have block scope.
    • These variables are only accessible within that specific block and its nested blocks.
    • They are not visible outside of the block in which they are defined.
    • Block-scope variables have local scope, which means they can shadow (hide) variables with the same name declared in outer blocks.
    void someFunction() {
        int x = 10; // x has block scope within someFunction
        {
            int y = 20; // y has block scope within the inner block
            // x is still visible here
        }
        // y is not visible here
    }
  2. Function Scope:

    • Function parameters and variables declared as an argument to a function (outside of any blocks) have function scope.
    • Like local variables, they are only accessible within that specific function.
    • Function-scope variables cannot be accessed outside of the function in which they are defined.
    int globalVar = 100; // globalVar has file scope
    void someFunction(int parameter) {
        // parameter has function scope
        int localVar = 42; // localVar also has function scope
        // ...
    }
  3. File Scope (Global Scope):

    • Variables declared outside of any function, typically at the top of a C source file, have file scope.
    • They are accessible from any function within that file.
    • File-scope variables can be declared as static to limit their visibility to the current file only (internal linkage), or they can be declared as extern to allow them to be accessed from other files (external linkage).
    extern int globalVar;   // file scope and external linkage
    
    static int internalVar; // file scope and internal linkage
  4. Function Prototype Scope:

    • Function prototypes (declarations) also have their own scope.
    • Parameters declared in function prototypes do not have the same scope as the actual function parameters but can be thought of as having prototype scope.
    void someFunction(int parameter); // parameter has prototype scope

It’s important to understand the scope of variables and identifiers in C to avoid naming conflicts, manage the lifetime or extent of variables, and control access to data within your program. Properly scoping variables helps improve code readability, maintainability, and prevents unintended side effects.

Access to a variable is always its most recently declared scope. In the fragment below, within the function myFunc(), both the global and the function scope declarations of i are hidden by the more recently declared locally scoped i. The i in main() and the i in myFunc() are in separate scopes and thus do not conflict in any way.

static int i = 100;

int myFunc (int i)
{
  int i = 500;

  // i is 500
}

void otherFunc()
{
  // which _i_ and what is its value here?
}

void main (void)
{
  int i = 99;

  myFunc (i);

  // i is 99
}

So, then which i is referred to in otherFunc()?2

Memory Areas in Detail

Stack Allocation

The stack is a region of memory that is used for function call management and local variables. Memory allocated on the stack is automatically managed by the compiler and is typically small in size. Variables allocated on the stack have a limited scope, typically within the function in which they are declared. When a function is called, space is allocated on the stack for its local variables and function call information. When the function exits, this space is automatically deallocated.

Example of stack allocation:

void someFunction(char c) {
  int x; // x is allocated on the stack
  // ...
}

In the above code fragment, the memory for both x and c is allocated automatically on the stack when the function is called. The scope of the variables is local to the function. The memory is deallocated as soon as the function exits, wich is either when its last statement is executed or via an explicit return.

Heap Allocation

The heap is a larger region of memory that is used for dynamic memory allocation. It is typically used for objects whose size or lifetime cannot be determined at compile time. Memory on the heap is managed explicitly by the programmer. You allocate memory when needed and release it when no longer required.

Heap memory must be explicitly allocated using functions like malloc(), calloc(), or realloc(), and it should be explicitly deallocated using free() when it is no longer needed to avoid memory leaks.

Example of heap allocation:

int* dynamicArray = (int*)malloc(sizeof(int) * 10); // memory on the heap

// ...

free(dynamicArray); // deallocate memory when done

A call to malloc() returns the address of the first byte of allocated memory. The caller is responsible for knowing how much memory was allocated and to track the address so it can be accessed and freed later when no longer needed.

The regions of memory used for the stack and the heap are not pre-allocated. They grow (and shrink) as needed. The program’s process requests additional memory from the operating as needed by calling a system function. Memory requests are honored until an out of memory situation is reached. The allocated memory is most commonly virtual memory rather than physical RAM.

The dynamic memory from the heap can be accessed as long as there is a pointer to it, i.e., there is a variable that holds the address of the allocated memory.

malloc()

In C, you can request memory from the operating system using the malloc() function, which stands for “memory allocation.” The malloc() function is part of the C Standard Library (stdlib.h) and is commonly used to dynamically allocate memory at runtime.

Here’s how you can use the malloc() function to request memory:

#include <stdio.h>
#include <stdlib.h>

int main() {
    int *ptr; // Declare a pointer to int

    // Request memory for an integer (4 bytes)
    ptr = (int *)malloc(sizeof(int));

    if (ptr == NULL) {
        fprintf(stderr, "Memory allocation failed.\n");
        return 1; // Return an error code
    }

    // Now, you can use 'ptr' to store and access an integer
    *ptr = 42;

    // Don't forget to free the allocated memory when you're done with it
    free(ptr);

    return 0;
}

A few important points to note when using malloc():

  1. It returns a pointer to the first byte of the allocated memory block, or it returns NULL if memory allocation fails (e.g., due to insufficient memory).

  2. You should cast the result of malloc() to the appropriate pointer type. In the example above, we cast it to (int *) because we are allocating memory for an integer.

  3. Always check if the returned pointer is NULL after calling malloc(). If it’s NULL, it means that memory allocation failed, and you should handle the error gracefully.

  4. After you’re done using the allocated memory, it’s crucial to free it using the free() function to avoid memory leaks.

  5. The size to be passed to malloc() should be the size (in bytes) of the data type you want to store in the allocated memory. In the example, sizeof(int) is used because we’re allocating memory for an integer.

  6. malloc() allocates memory on the heap, which means the memory persists until you explicitly free it using free() or until the program exits.

Static and Global Allocation

Variables declared with the static keyword are allocated in a special section of memory separate from the stack and the heap. They retain their values between function calls.

Global variables are also allocated in a similar manner, and they can be accessed from anywhere in the program.

Example of static allocation:

static int counter = 0; // Allocated in a special memory section

In the above example, the memory remains allocated for the duration of the program’s execution and the scope of the variable is file scope.

Code Segment

Also sometimes called the text segment, this segment contains the binary machine code of the compiled program. Most commonly, the text segment is shared among all processes executing the same program, so that it only needs to be in memory once. For example, if multiple users were to start ‘vi’ or run ‘gcc’, then only one copy is loaded and the memory pages are shared among the processes. This sharing is managed by the operating system’s virtual memory subsystem and is of no concern to programmers.

The code segment is generally a read-only segment, which means that a program cannot modify its code. Doing so (often by accident) results in a segmentation fault.

Command-Line Arguments

This area of memory holds the command line arguments that are passed to main(). Often those are simply pushed onto the stack when the program is loaded rather than being a completely separate area.

There is always at least one command line argument, namely the name of the running program.

Uninitialized and Initialized Data Segments

The initialized data segment stores all global, static, constant, and external variables (those declared with extern keyword ) that are initialized at time of declaration. This memory segment is read-write as the values can be modified at run-time, although some variables can be protected from modification by the compiler (but nothing is stopping someone from getting the address of a const variable and modifying it that way, so the memory is not actually protected).

#include <stdio.h>

char c[] = "read-write";  // global variable stored in the initialized 
                          // data segment and is read-write
                          
const char s[] = "const-var"; // global variable stored in the initialized 
                              // data segment and treated as read-only
                              
int glob_var;  // global variables stored in the uninitialized
               // data segment

int main(int argc, char* argv[])
{
  static int x = 99;  // static local variable stored in the 
                      // initialized data segment*/
                      
  return 0;
}

Note that one should assume that all variables are uninitialized unless explicitly initialized.

Tutorial

If you prefer a narrated version with code examples, then watch the narrated lesson by Martin Schedlbauer below:

The Stack: Details

The stack is not only used to store local variables, but is an integral part of the calling mechanism for functions. In C, the stack plays a critical role in managing program execution and function calls. It is a region of memory used for organizing and tracking function call information, including local variables, function parameters, return addresses, and control flow.

Below is an explanation of how the stack is used in C programs and to enable and support function calls:

  1. Function Calls and the Stack:
    • When a function is called in a C program, a new stack frame, also known as an activation record, is created on the stack to manage the function’s execution.
    • Each stack frame contains information about the function’s local variables, parameters, and other necessary data.
  2. Function Parameters:
    • The parameters passed to a function are typically pushed onto the stack.
    • These parameters become local variables within the function, and their values can be accessed through the stack frame.
  3. Local Variables:
    • Local variables declared within a function are also stored on the stack.
    • They are created when the function is called and destroyed when the function exits.
    • The stack ensures that local variables have a scope limited to the duration of the function call.
  4. Return Addresses:
    • When a function is called, the address of the next instruction to be executed after the function call is pushed onto the stack as a return address.
    • This allows the program to return to the correct point in the code after the function completes its execution.
  5. Stack Frames Hierarchy:
    • Function calls can be nested, creating a hierarchy of stack frames.
    • The stack frames are organized in a last-in, first-out (LIFO) fashion. The most recently called function’s stack frame is at the top of the stack, and it is the first one to be removed when the function returns.
  6. Stack Pointer (SP):
    • A special register known as the stack pointer (SP) keeps track of the top of the stack.
    • It is incremented and decremented as functions are called and return, respectively.
  7. Stack Overflow:
    • The stack has a limited size, and if too many nested function calls or excessive local variables are used, it can result in a stack overflow.
    • A stack overflow occurs when the available stack space is exhausted, typically causing a program crash.

Here’s a simplified visual representation of the stack during function calls:

Stack grows ↓
+----------------------+
|     Stack Frame 1    |  <- Function 1
|     Local Vars       |
|     Parameters       |
|     Return Address   |  <- Return address for Function 1
+----------------------+
|     Stack Frame 2    |  <- Function 2 (nested within Function 1)
|     Local Vars       |
|     Parameters       |
|     Return Address   |  <- Return address for Function 2
+----------------------+
|     ...              |  <- More stack frames for nested functions
+----------------------+
|     Stack Frame N    |  <- Most recent function call
|     Local Vars       |
|     Parameters       |
|     Return Address   |  <- Return address for Function N
+----------------------+
Stack shrinks ↑

In summary, the stack is a fundamental data structure used in C programs to manage function calls and their associated data. It ensures proper scoping, local variable allocation, and control flow within the program. Understanding how the stack works is essential for writing efficient and well-structured C code.

As an aside, while this lesson deals with memory management in C, all of this also applies to C++ and to many other languages, such as Java.

Summary

In summary, C programs allocate memory on the stack for local variables with a fixed lifetime and size, while memory on the heap is used for dynamic data that can vary in size and lifetime. It’s crucial to manage heap memory properly to avoid memory leaks and to release allocated memory using free() when it is no longer needed. Failure to deallocate memory can lead to memory leaks, where your program consumes more and more memory over time, potentially causing it to run out of available memory.


All Files for Lesson 8.118

Learn More

TBD

Errata

Let us know.


  1. The term “core dump” has its origins in early computing and the use of magnetic core memory, which was a type of primary storage used in early computers. Today, the term “core dump” is still used to describe the practice of saving a program’s memory state when it encounters a critical error, even though the underlying technology has changed significantly. Instead of magnetic core memory, modern systems typically save this snapshot to a file named “core” or with an extension like “.core.” This file can then be used for debugging and analysis, as described in the previous response.↩︎

  2. it would refer to the file scope i with value 100↩︎