CoreTechED

Loading

Learn C dynamic memory allocation: malloc, calloc, realloc, and free. Master heap memory management and avoid memory leaks.

Dynamic Memory Allocation in C: Complete Guide

Dynamic Memory Allocation in C: A Comprehensive Tutorial

Dynamic memory allocation allows programs to allocate and manage memory at runtime according to their requirements. It is useful when the amount of memory needed by a program is not known at compile time. Dynamic memory is allocated from the heap and must be explicitly managed by the programmer.

This tutorial provides an in-depth exploration of dynamic memory allocation in C: malloc, calloc, realloc, free, practical applications, common pitfalls, and best practices for safe memory management.


Conceptual Introduction & Fundamentals

What is Dynamic Memory Allocation?

Dynamic memory allocation is the process of allocating memory at runtime from the heap segment. Unlike static and automatic allocation (which happen at compile time or function entry), dynamic allocation gives the programmer control over when and how much memory is allocated and freed.

Key characteristics:

  • Allocated from the heap at runtime.
  • Size can be determined by user input or program logic.
  • Memory persists until explicitly freed.
  • Managed using malloc, calloc, realloc, and free.

Why Dynamic Memory Allocation Matters

  • Unknown size at compile time: Handle user input of variable length.
  • Flexible data structures: Linked lists, trees, dynamic arrays.
  • Efficient memory use: Allocate only what is needed.
  • Long-lived data: Data that persists beyond function scope.
  • Large data: Allocate large blocks that would overflow the stack.

Static vs. Dynamic Allocation

AspectStatic/StackDynamic (Heap)
Allocation timeCompile time or function entryRuntime
SizeFixed or automaticFlexible
LifetimeAutomatic (stack) or program (static)Programmer-controlled
SpeedFasterSlower
FailureStack overflowReturns NULL
ManagementAutomaticManual (free required)

Syntax & Core Mechanics

malloc() — Memory Allocation

Allocates a contiguous block of uninitialized memory.

Syntax:

c

void *malloc(size_t size);

Example:

c

#include <stdio.h> #include <stdlib.h> int main(void) { int *ptr = malloc(5 * sizeof(int)); if (ptr == NULL) { fprintf(stderr, "Memory allocation failed\n"); return EXIT_FAILURE; } for (int i = 0; i < 5; i++) { ptr[i] = i + 1; } for (int i = 0; i < 5; i++) { printf("%d ", ptr[i]); } printf("\n"); free(ptr); return 0; }

Expected output:

text

1 2 3 4 5

Important: malloc does not initialize memory; contents are indeterminate.

calloc() — Contiguous Allocation

Allocates memory for multiple elements and initializes all bytes to zero.

Syntax:

c

void *calloc(size_t num_elements, size_t element_size);

Example:

c

#include <stdio.h> #include <stdlib.h> int main(void) { int *ptr = calloc(5, sizeof(int)); if (ptr == NULL) { fprintf(stderr, "Memory allocation failed\n"); return EXIT_FAILURE; } /* All elements are zero-initialized */ for (int i = 0; i < 5; i++) { printf("%d ", ptr[i]); } printf("\n"); free(ptr); return 0; }

Expected output:

text

0 0 0 0 0

realloc() — Reallocate Memory

Changes the size of a previously allocated memory block.

Syntax:

c

void *realloc(void *ptr, size_t new_size);

Example:

c

#include <stdio.h> #include <stdlib.h> int main(void) { int *ptr = malloc(5 * sizeof(int)); if (ptr == NULL) { fprintf(stderr, "Memory allocation failed\n"); return EXIT_FAILURE; } for (int i = 0; i < 5; i++) { ptr[i] = i + 1; } /* Resize to hold 10 integers */ int *temp = realloc(ptr, 10 * sizeof(int)); if (temp == NULL) { fprintf(stderr, "Memory reallocation failed\n"); free(ptr); /* Free original block */ return EXIT_FAILURE; } ptr = temp; for (int i = 5; i < 10; i++) { ptr[i] = i + 1; } for (int i = 0; i < 10; i++) { printf("%d ", ptr[i]); } printf("\n"); free(ptr); return 0; }

Expected output:

text

1 2 3 4 5 6 7 8 9 10

Important: Use a temporary pointer for realloc to safely handle failure.

free() — Release Memory

Releases dynamically allocated memory.

Syntax:

c

void free(void *ptr);

Example:

c

#include <stdio.h> #include <stdlib.h> int main(void) { int *ptr = malloc(5 * sizeof(int)); if (ptr == NULL) { fprintf(stderr, "Memory allocation failed\n"); return EXIT_FAILURE; } /* Use the memory */ for (int i = 0; i < 5; i++) { ptr[i] = i * 10; } /* Free and set to NULL */ free(ptr); ptr = NULL; /* Prevent dangling pointer */ return 0; }

Full Practical Code Example with Explanation

This program demonstrates dynamic memory allocation in a realistic scenario: a dynamically growing array.

Program: Dynamic Array Manager

c

/** * dynamic_array_manager.c * Demonstrates dynamic memory allocation with malloc, calloc, realloc, and free. * Implements a dynamically growing array. */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #define INITIAL_CAPACITY 4 #define MAX_LINE_LEN 100 /* Dynamic array structure */ typedef struct { int *data; size_t size; /* Current number of elements */ size_t capacity; /* Allocated capacity */ } DynamicArray; /* Function prototypes */ int initArray(DynamicArray *arr, size_t initialCapacity); int pushBack(DynamicArray *arr, int value); int popBack(DynamicArray *arr, int *outValue); int getAt(const DynamicArray *arr, size_t index, int *outValue); void displayArray(const DynamicArray *arr); void freeArray(DynamicArray *arr); int readInteger(const char *prompt, int *outValue); int main(void) { DynamicArray arr; int choice; char buffer[MAX_LINE_LEN]; /* Initialize the dynamic array */ if (initArray(&arr, INITIAL_CAPACITY) != 0) { fprintf(stderr, "Failed to initialize array.\n"); return EXIT_FAILURE; } printf("=== Dynamic Array Manager ===\n"); while (1) { printf("\n--- Menu ---\n"); printf("1. Add Element (Push Back)\n"); printf("2. Remove Element (Pop Back)\n"); printf("3. Display Array\n"); printf("4. Get Element at Index\n"); printf("5. Display Capacity\n"); printf("6. Exit\n"); printf("Enter choice: "); fflush(stdout); if (fgets(buffer, sizeof(buffer), stdin) == NULL) { fprintf(stderr, "Error reading choice.\n"); freeArray(&arr); return EXIT_FAILURE; } choice = atoi(buffer); switch (choice) { case 1: { int value; if (readInteger("Enter value to add: ", &value) != 0) { fprintf(stderr, "Invalid input.\n"); break; } if (pushBack(&arr, value) == 0) { printf("Added %d. Size: %zu, Capacity: %zu\n", value, arr.size, arr.capacity); } break; } case 2: { int value; if (popBack(&arr, &value) == 0) { printf("Removed %d. Size: %zu, Capacity: %zu\n", value, arr.size, arr.capacity); } else { printf("Array is empty.\n"); } break; } case 3: displayArray(&arr); break; case 4: { size_t index; int value; printf("Enter index: "); fflush(stdout); if (fgets(buffer, sizeof(buffer), stdin) != NULL) { index = (size_t)strtoul(buffer, NULL, 10); if (getAt(&arr, index, &value) == 0) { printf("Element at index %zu: %d\n", index, value); } else { printf("Invalid index.\n"); } } break; } case 5: printf("Size: %zu, Capacity: %zu\n", arr.size, arr.capacity); break; case 6: printf("Exiting.\n"); freeArray(&arr); return EXIT_SUCCESS; default: printf("Invalid choice. Please try again.\n"); break; } } freeArray(&arr); return EXIT_SUCCESS; } /* Initializes the dynamic array */ int initArray(DynamicArray *arr, size_t initialCapacity) { arr->data = malloc(initialCapacity * sizeof(int)); if (arr->data == NULL) { return -1; } arr->size = 0; arr->capacity = initialCapacity; return 0; } /* Adds an element to the end, growing if necessary */ int pushBack(DynamicArray *arr, int value) { /* Check if we need to grow */ if (arr->size >= arr->capacity) { size_t newCapacity = arr->capacity * 2; int *temp = realloc(arr->data, newCapacity * sizeof(int)); if (temp == NULL) { fprintf(stderr, "Reallocation failed.\n"); return -1; } arr->data = temp; arr->capacity = newCapacity; } arr->data[arr->size] = value; arr->size++; return 0; } /* Removes the last element */ int popBack(DynamicArray *arr, int *outValue) { if (arr->size == 0) { return -1; } arr->size--; *outValue = arr->data[arr->size]; /* Optionally shrink if capacity is much larger than size */ if (arr->size < arr->capacity / 4 && arr->capacity > INITIAL_CAPACITY) { size_t newCapacity = arr->capacity / 2; int *temp = realloc(arr->data, newCapacity * sizeof(int)); if (temp != NULL) { arr->data = temp; arr->capacity = newCapacity; } } return 0; } /* Gets element at a specific index */ int getAt(const DynamicArray *arr, size_t index, int *outValue) { if (index >= arr->size) { return -1; } *outValue = arr->data[index]; return 0; } /* Displays the array */ void displayArray(const DynamicArray *arr) { if (arr->size == 0) { printf("Array is empty.\n"); return; } printf("Array elements: "); for (size_t i = 0; i < arr->size; i++) { printf("%d ", arr->data[i]); } printf("\n"); } /* Frees the dynamic array */ void freeArray(DynamicArray *arr) { if (arr->data != NULL) { free(arr->data); arr->data = NULL; } arr->size = 0; arr->capacity = 0; } /* Reads an integer safely */ int readInteger(const char *prompt, int *outValue) { char buffer[MAX_LINE_LEN]; char *endPtr; long val; printf("%s", prompt); fflush(stdout); if (fgets(buffer, sizeof(buffer), stdin) == NULL) { return -1; } errno = 0; val = strtol(buffer, &endPtr, 10); if (endPtr == buffer || (*endPtr != '\n' && *endPtr != '\0')) { return -1; } if (errno == ERANGE || val < -2147483647 - 1 || val > 2147483647) { return -1; } *outValue = (int)val; return 0; }

Explanation of Dynamic Memory Usage

  1. Initialization: malloc allocates initial capacity.
  2. Growth: realloc doubles capacity when full.
  3. Shrinkage: realloc reduces capacity when significantly underused.
  4. Cleanup: free releases all allocated memory.
  5. Safe reallocation: Uses temporary pointer to handle failure.

Expected Output (sample run)

text

=== Dynamic Array Manager === --- Menu --- 1. Add Element (Push Back) 2. Remove Element (Pop Back) 3. Display Array 4. Get Element at Index 5. Display Capacity 6. Exit Enter choice: 1 Enter value to add: 10 Added 10. Size: 1, Capacity: 4 --- Menu --- ... Enter choice: 1 Enter value to add: 20 Added 20. Size: 2, Capacity: 4 --- Menu --- ... Enter choice: 1 Enter value to add: 30 Added 30. Size: 3, Capacity: 4 --- Menu --- ... Enter choice: 1 Enter value to add: 40 Added 40. Size: 4, Capacity: 4 --- Menu --- ... Enter choice: 1 Enter value to add: 50 Added 50. Size: 5, Capacity: 8 --- Menu --- ... Enter choice: 3 Array elements: 10 20 30 40 50 --- Menu --- ... Enter choice: 2 Removed 50. Size: 4, Capacity: 8 --- Menu --- ... Enter choice: 6 Exiting.

Common Pitfalls, Edge Cases & Memory Considerations

1. Memory Leaks

Forgetting to free allocated memory causes leaks.

c

int *ptr = malloc(100); /* Missing free(ptr); */

Solution: Always free allocated memory; use tools like Valgrind to detect leaks.

2. Dangling Pointers

Using pointers after freeing causes undefined behavior.

c

int *ptr = malloc(sizeof(int)); free(ptr); *ptr = 10; // BUG: dangling pointer

Solution: Set pointer to NULL after free.

3. Double Free

Calling free twice on the same pointer corrupts the heap.

c

int *ptr = malloc(sizeof(int)); free(ptr); free(ptr); // BUG: double free

Solution: Set pointer to NULL after freeing; free(NULL) is safe.

4. realloc Failure Handling

Overwriting the original pointer with realloc result loses the original block on failure.

c

ptr = realloc(ptr, newSize); // BUG: loses ptr on failure

Solution: Use a temporary pointer:

c

int *temp = realloc(ptr, newSize); if (temp != NULL) { ptr = temp; }

5. Not Checking malloc Return

Using malloc result without checking for NULL can crash.

c

int *ptr = malloc(1000000000000); ptr[0] = 10; // BUG: ptr may be NULL

Solution: Always check if (ptr != NULL) after allocation.

6. Memory Fragmentation

Frequent allocations/deallocations of varying sizes fragment the heap.

Solution: Use memory pools or allocate large blocks at once.

7. Using sizeof Wrong Type

Using sizeof with the wrong type can allocate incorrect size.

c

int *ptr = malloc(10 * sizeof(char)); // BUG: intended int

Solution: Use sizeof(*ptr) or sizeof(int).

8. Off-by-One in Allocation

Forgetting space for null terminator in strings.

c

char *str = malloc(strlen("Hello")); // BUG: no space for '\0' strcpy(str, "Hello"); // Overflow

Solution: Allocate strlen("Hello") + 1 bytes.

9. Freeing Stack Memory

Calling free on stack or static memory is undefined.

c

int arr[10]; free(arr); // BUG: arr is not heap-allocated

Solution: Only free memory allocated by malloc, calloc, or realloc.

10. Memory Considerations

  • Heap allocation is slower than stack allocation.
  • Large allocations may fail; always check return values.
  • Memory allocated with malloc is uninitialized; use calloc for zero-initialization.
  • realloc may move the memory block; update all pointers accordingly.

Summary & Best Practices

Dynamic memory allocation provides flexibility for programs that need variable amounts of memory at runtime. Proper management is essential to avoid leaks, dangling pointers, and other memory errors.

Key Takeaways

  • malloc: Allocates uninitialized memory.
  • calloc: Allocates zero-initialized memory.
  • realloc: Resizes previously allocated memory.
  • free: Releases allocated memory.
  • Always check allocation return values.
  • Use temporary pointers for realloc.
  • Set pointers to NULL after freeing.

Best Practices

  1. Always check malloc/calloc/realloc return values.
  2. Free allocated memory when no longer needed.
  3. Set pointers to NULL after free to prevent dangling pointers.
  4. Use temporary pointers for realloc to handle failure safely.
  5. Use sizeof(*ptr) for allocation size to avoid type mistakes.
  6. Allocate +1 for string null terminators.
  7. Use calloc when zero-initialization is needed.
  8. Avoid excessive allocation/deallocation to prevent fragmentation.
  9. Document ownership of dynamically allocated memory.
  10. Use tools like Valgrind to detect memory leaks and errors.

By mastering dynamic memory allocation, you’ll write flexible C programs that efficiently manage memory at runtime and avoid common memory-related bugs.

Teacher pointing to whiteboard chart about memory layout of C programs in a classroom

Memory Layout of C Programs: Complete Guide

Memory Layout of C Programs: A Comprehensive Tutorial

The memory layout of a C program shows how its data is stored in memory during execution. It helps developers understand and manage memory efficiently. The memory is divided into sections such as code (text), data, heap, and stack. Understanding this layout is crucial for optimizing performance, debugging, and preventing errors like segmentation faults and memory leaks.

This tutorial provides an in-depth exploration of the memory layout of C programs: text segment, data segment (initialized and BSS), heap, stack, practical verification, common pitfalls, and best practices.


Conceptual Introduction & Fundamentals

What is Memory Layout?

Memory layout refers to the organization of a program’s data and code in the computer’s memory during execution. The operating system allocates memory to a process in a structured manner, dividing it into distinct segments:

text

High Address +-------------------+ | Stack | (grows downward) | ↓ | | | | ↑ | | Heap | (grows upward) | | | BSS Segment | (uninitialized global/static) | Data Segment | (initialized global/static) | Text Segment | (executable code) +-------------------+ Low Address

Why Memory Layout Matters

  • Debugging: Identify where variables are stored and why crashes occur.
  • Performance optimization: Understand cache behavior and memory access patterns.
  • Memory management: Proper allocation and deallocation.
  • Preventing errors: Avoid stack overflow, heap fragmentation, and memory leaks.
  • Low-level programming: Direct memory manipulation for embedded systems.

The Segments at a Glance

SegmentContentsLifespanGrowth Direction
Text (Code)Executable instructionsProgram lifetimeFixed
Data (Initialized)Global/static variables with initial valuesProgram lifetimeFixed
BSS (Uninitialized)Global/static variables without initial valuesProgram lifetimeFixed
HeapDynamically allocated memoryManaged by programmerUpward (to higher addresses)
StackLocal variables, function parameters, return addressesFunction callsDownward (to lower addresses)

Syntax & Core Mechanics

1. Text Segment (Code Segment)

The text segment stores the executable code of the program—functions and instructions.

Characteristics:

  • Read-only (prevents accidental modification)
  • Contains compiled machine code
  • Size depends on program complexity
  • Typically at lower memory addresses

c

#include <stdio.h> void myFunction(void) { printf("This function's code is in the text segment\n"); } int main(void) { myFunction(); return 0; }

The compiled code of main and myFunction resides in the text segment.

2. Data Segment

The data segment stores global and static variables. It is divided into:

A. Initialized Data Segment

Stores global and static variables that have been initialized by the programmer.

c

#include <stdio.h> /* Global initialized variables (stored in initialized data segment) */ int globalVar = 10; char message[] = "Hello"; const int constantValue = 100; /* May be in read-only data */ int main(void) { /* Static initialized variable (also in initialized data segment) */ static int staticVar = 20; printf("Global: %d\n", globalVar); printf("Static: %d\n", staticVar); printf("Message: %s\n", message); printf("Constant: %d\n", constantValue); return 0; }

Expected output:

text

Global: 10 Static: 20 Message: Hello Constant: 100

B. Uninitialized Data Segment (BSS)

Stores global and static variables that are not explicitly initialized. These are automatically zero-initialized.

c

#include <stdio.h> #include <string.h> /* Global uninitialized variables (stored in BSS segment) */ int globalVar; char message[50]; int main(void) { /* Static uninitialized variable (also in BSS) */ static int staticVar; /* These will be zero initially */ printf("Before assignment:\n"); printf("globalVar: %d\n", globalVar); printf("staticVar: %d\n", staticVar); printf("message[0]: %d (null)\n", message[0]); /* Assign values at runtime */ globalVar = 10; staticVar = 20; snprintf(message, sizeof(message), "Hello BSS"); printf("\nAfter assignment:\n"); printf("globalVar: %d\n", globalVar); printf("staticVar: %d\n", staticVar); printf("message: %s\n", message); return 0; }

Expected output:

text

Before assignment: globalVar: 0 staticVar: 0 message[0]: 0 (null) After assignment: globalVar: 10 staticVar: 20 message: Hello BSS

3. Heap Segment

The heap is used for dynamic memory allocation using malloc, calloc, realloc, and free.

c

#include <stdio.h> #include <stdlib.h> int main(void) { /* Dynamically allocated memory on the heap */ int *ptr = malloc(10 * sizeof(int)); if (ptr == NULL) { fprintf(stderr, "Memory allocation failed\n"); return EXIT_FAILURE; } for (int i = 0; i < 10; i++) { ptr[i] = i * 10; } for (int i = 0; i < 10; i++) { printf("%d ", ptr[i]); } printf("\n"); /* Free allocated memory */ free(ptr); return 0; }

Expected output:

text

0 10 20 30 40 50 60 70 80 90

4. Stack Segment

The stack stores local variables, function parameters, and return addresses. Each function call creates a stack frame.

c

#include <stdio.h> void func(int param) { /* Local variable stored on the stack */ int localVar = param * 2; printf("localVar: %d\n", localVar); } int main(void) { func(10); return 0; }

Expected output:

text

localVar: 20

Full Practical Code Example with Explanation

This program demonstrates the memory layout by printing addresses of variables in different segments.

Program: Memory Layout Explorer

c

/** * memory_layout.c * Demonstrates memory layout by printing addresses of variables * in different segments: text, data, bss, heap, and stack. */ #include <stdio.h> #include <stdlib.h> #include <string.h> /* Global initialized variable (Data Segment) */ int initializedGlobal = 42; /* Global uninitialized variable (BSS Segment) */ int uninitializedGlobal; /* Constant global variable (Read-only Data) */ const int constantGlobal = 100; /* Static initialized variable (Data Segment) */ static int staticInitialized = 200; /* Static uninitialized variable (BSS Segment) */ static int staticUninitialized; /* Function in Text Segment */ void printAddresses(void) { /* Local variable (Stack Segment) */ int localVariable = 10; static int staticLocal = 50; /* Data Segment */ printf("\n=== Inside function ===\n"); printf("Local variable (stack): %p\n", (void*)&localVariable); printf("Static local (data): %p\n", (void*)&staticLocal); } int main(void) { /* Heap allocation */ int *heapVariable = malloc(sizeof(int)); if (heapVariable == NULL) { fprintf(stderr, "Memory allocation failed\n"); return EXIT_FAILURE; } *heapVariable = 99; /* Stack variables */ int stackVariable = 7; printf("=== Memory Layout Explorer ===\n"); printf("\n--- Text Segment ---\n"); printf("Address of main: %p\n", (void*)&main); printf("Address of printAddresses: %p\n", (void*)&printAddresses); printf("Address of printf: %p\n", (void*)&printf); printf("\n--- Data Segment ---\n"); printf("Initialized global: %p\n", (void*)&initializedGlobal); printf("Constant global: %p\n", (void*)&constantGlobal); printf("Static initialized: %p\n", (void*)&staticInitialized); printf("\n--- BSS Segment ---\n"); printf("Uninitialized global: %p\n", (void*)&uninitializedGlobal); printf("Static uninitialized: %p\n", (void*)&staticUninitialized); printf("\n--- Heap Segment ---\n"); printf("Heap variable: %p\n", (void*)heapVariable); printf("\n--- Stack Segment ---\n"); printf("Stack variable (main): %p\n", (void*)&stackVariable); /* Print addresses from function */ printAddresses(); /* Print values to verify */ printf("\n=== Values ===\n"); printf("initializedGlobal = %d\n", initializedGlobal); printf("uninitializedGlobal = %d (zero-initialized)\n", uninitializedGlobal); printf("constantGlobal = %d\n", constantGlobal); printf("staticInitialized = %d\n", staticInitialized); printf("staticUninitialized = %d (zero-initialized)\n", staticUninitialized); printf("*heapVariable = %d\n", *heapVariable); printf("stackVariable = %d\n", stackVariable); /* Free heap memory */ free(heapVariable); return 0; }

Explanation of Memory Layout

  1. Text Segment: Addresses of functions (main, printAddresses, printf).
  2. Data Segment: Addresses of initialized globals and statics.
  3. BSS Segment: Addresses of uninitialized globals and statics.
  4. Heap Segment: Address of malloc‘d memory.
  5. Stack Segment: Addresses of local variables.

Expected Output (addresses will vary)

text

=== Memory Layout Explorer === --- Text Segment --- Address of main: 0x4011d0 Address of printAddresses: 0x401230 Address of printf: 0x401500 --- Data Segment --- Initialized global: 0x404020 Constant global: 0x402084 Static initialized: 0x404024 --- BSS Segment --- Uninitialized global: 0x404028 Static uninitialized: 0x40402c --- Heap Segment --- Heap variable: 0x119592a0 --- Stack Segment --- Stack variable (main): 0x7ffe8289c66c === Inside function === Local variable (stack): 0x7ffe8289c64c Static local (data): 0x404030 === Values === initializedGlobal = 42 uninitializedGlobal = 0 (zero-initialized) constantGlobal = 100 staticInitialized = 200 staticUninitialized = 0 (zero-initialized) *heapVariable = 99 stackVariable = 7

Observations:

  • Text segment addresses are lowest.
  • Data segment addresses are higher than text.
  • Heap addresses are much higher (dynamic allocation).
  • Stack addresses are the highest (growing downward).
  • The layout matches the theoretical model.

Common Pitfalls, Edge Cases & Memory Considerations

1. Stack Overflow

Large local variables or deep recursion can exhaust the stack.

c

void func(void) { int bigArray[1000000]; // 4 MB on stack, may overflow }

Solution: Use static or malloc for large buffers.

2. Memory Leaks in Heap

Forgetting to free heap memory causes leaks.

c

int *ptr = malloc(100); /* Missing free(ptr); */

Solution: Always free dynamically allocated memory.

3. Dangling Pointers

Using pointers to freed memory is undefined behavior.

c

int *ptr = malloc(sizeof(int)); free(ptr); *ptr = 10; // BUG: dangling pointer

Solution: Set pointer to NULL after free.

4. Uninitialized BSS Variables

BSS variables are zero-initialized, but relying on this can hide bugs.

c

int global; // BSS: zero-initialized /* But explicit initialization is clearer */

Solution: Explicitly initialize variables when the initial value matters.

5. Modifying Read-Only Data

String literals and constants may be in read-only memory.

c

char *str = "Hello"; // Points to read-only memory str[0] = 'h'; // BUG: segmentation fault

Solution: Use const char * or char str[] = "Hello";

6. Stack and Heap Collision

When the stack (growing down) and heap (growing up) meet, memory is exhausted.

Solution: Monitor memory usage; use ulimit or equivalent to set limits.

7. Assuming Memory Layout is Fixed

The exact layout varies by operating system, compiler, and architecture.

Solution: Do not rely on specific memory addresses; use standard C constructs.

8. Buffer Overflow in Stack

Writing beyond array bounds on the stack can corrupt other variables.

c

char buffer[10]; strcpy(buffer, "This is too long"); // BUG: overflow

Solution: Use strncpy or snprintf with size limits.

9. Heap Fragmentation

Frequent allocate/free cycles can fragment the heap, reducing performance.

Solution: Use memory pools or allocate large blocks at once.

10. Memory Considerations

  • Text segment: read-only, shared among processes.
  • Data segment: part of the executable file.
  • BSS segment: zero-initialized at startup, not stored in file.
  • Heap: grows upward, managed by malloc/free.
  • Stack: grows downward, automatically managed.

Summary & Best Practices

Understanding the memory layout of C programs is essential for efficient memory management, debugging, and preventing common errors like stack overflow and memory leaks.

Key Takeaways

  • Text segment: Executable code (read-only).
  • Data segment: Initialized global/static variables.
  • BSS segment: Uninitialized global/static variables (zero-initialized).
  • Heap: Dynamic memory allocation (grows upward).
  • Stack: Local variables and function frames (grows downward).
  • Different segments have different lifespans and management.

Best Practices

  1. Free dynamically allocated memory to prevent leaks.
  2. Set pointers to NULL after free to avoid dangling pointers.
  3. Use static or malloc for large buffers to avoid stack overflow.
  4. Use const for read-only data and respect read-only memory.
  5. Check malloc return values before using allocated memory.
  6. Use stack for small, short-lived variables; use heap for large or long-lived data.
  7. Monitor memory usage in long-running programs.
  8. Use tools like Valgrind to detect memory errors.
  9. Avoid assuming specific memory addresses; rely on standard C behavior.
  10. Document memory ownership (who allocates, who frees).

By mastering the memory layout, you’ll write efficient, safe C programs that effectively manage memory and avoid common pitfalls.