← Back to DevBytes

Fix 'Segmentation fault' in C/C++

Understanding Segmentation Faults

What Is a Segmentation Fault?

A segmentation fault (often shortened to segfault) is a signal delivered to a process by the operating system when it attempts to access a memory location that it is not allowed to access. In C and C++, this is signal SIGSEGV (signal number 11 on most Unix-like systems). The kernel sends this signal when a program tries to read from or write to an invalid memory address, such as:

When a segmentation fault occurs, the default behavior is for the process to terminate immediately, often with the cryptic message Segmentation fault (core dumped) printed to the terminal. This crash happens because the CPU's memory management unit (MMU) detects the illegal access and raises an exception that the OS translates into SIGSEGV.

Why It Matters

Segmentation faults are among the most common and frustrating bugs in C and C++ development. They matter because:

Understanding how to systematically diagnose and fix segmentation faults is an essential skill for any C or C++ programmer. The good news is that with modern tools and disciplined practices, you can track down even the most elusive segfaults and prevent them from recurring.

Common Causes of Segmentation Faults

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

1. Null Pointer Dereference

This is the most straightforward cause. A pointer that is NULL (or nullptr in C++) does not point to any valid memory. Dereferencing it triggers an immediate segfault because address 0x0 is typically mapped as inaccessible by the OS.

int *ptr = NULL;
*ptr = 42;        // SIGSEGV: writing to address 0

2. Array Index Out of Bounds

Accessing an array element beyond its allocated size reads or writes into memory that does not belong to the array. If that memory happens to be unmapped or protected, a segfault occurs.

int arr[10];
arr[10] = 99;     // valid indices are 0..9; index 10 is out of bounds
// This may or may not segfault immediately depending on what lies beyond arr[9]

3. Buffer Overflow / Overrun

Similar to array out-of-bounds but typically refers to writing past the end of a dynamically allocated buffer or filling a fixed-size buffer with more data than it can hold (e.g., via strcpy without bounds checking).

char buffer[8];
strcpy(buffer, "This string is way too long!");  // overflow, may segfault

4. Use-After-Free (Dangling Pointer)

After free() or delete is called on a pointer, the memory it pointed to is returned to the heap allocator and may be reused. Accessing that memory afterward is undefined behavior and often results in a segfault.

int *p = (int*)malloc(sizeof(int));
free(p);
*p = 10;          // use-after-free: p is now a dangling pointer

5. Double Free

Calling free() or delete twice on the same pointer corrupts the heap's internal metadata and can cause a segfault either during the second free or during a subsequent allocation.

int *p = (int*)malloc(sizeof(int));
free(p);
free(p);          // double free: heap corruption, likely segfault

6. Stack Overflow

Deep or infinite recursion, or allocating very large local variables on the stack, can exhaust the stack space. Writing beyond the stack boundary triggers a segfault.

void recurse() {
    recurse();    // infinite recursion, eventually overflows the stack
}
// Alternative: char huge[100000000]; on the stack may also segfault

7. Uninitialized / Wild Pointers

A pointer declared but not initialized contains a garbage value. Dereferencing it accesses a random memory address, which almost certainly triggers a segfault or corrupts data silently.

int *p;           // uninitialized: contains random address
*p = 42;          // writing to a random memory location

8. Incorrect Format Specifiers in printf/scanf

Using the wrong format specifier can cause the variadic functions to interpret arguments incorrectly, potentially dereferencing invalid pointers.

char *str = "hello";
printf("%d", str);   // passing a pointer but interpreting it as an integer — undefined behavior
// Even worse: scanf("%s", 12345); passes an integer as a pointer — likely segfault

9. Returning a Pointer to a Local Variable

When a function returns, its stack frame is destroyed. A pointer to a local variable becomes a dangling pointer to freed stack memory.

int *bad_function() {
    int local = 42;
    return &local;   // pointer to stack memory that will be invalid after return
}
// The caller dereferencing this returned pointer may segfault

Debugging Segmentation Faults

Using GDB (GNU Debugger)

GDB is the most powerful tool for diagnosing segfaults interactively. It lets you inspect the program state at the exact moment of the crash, examine the call stack, and print variable values.

Step 1: Compile with debug symbols

gcc -g -O0 -o myprogram myprogram.c   # -g includes debug info, -O0 disables optimizations for clarity

Step 2: Run the program inside GDB

gdb ./myprogram
(gdb) run

When the segfault occurs, GDB will print something like:

Program received signal SIGSEGV, Segmentation fault.
0x0000000000401136 in main () at myprogram.c:12
12          *ptr = 42;

Key GDB commands after the crash:

Example debugging session:

$ gdb ./myprogram
(gdb) run
Starting program: /home/user/myprogram 

Program received signal SIGSEGV, Segmentation fault.
0x00005555555551a9 in compute (data=0x0, n=10) at myprogram.c:25
25          data[0] = 42;

(gdb) bt
#0  compute (data=0x0, n=10) at myprogram.c:25
#1  0x00005555555551d4 in main () at myprogram.c:35

(gdb) print data
$1 = (int *) 0x0
(gdb) frame 1
#1  0x00005555555551d4 in main () at myprogram.c:35
35          result = compute(NULL, 10);
(gdb) list
30      int main() {
31          int *arr = NULL;
32          int result;
33          // forgot to allocate arr
34          result = compute(arr, 10);
35          return 0;
36      }

From this session, you immediately see that arr was never allocated before being passed to compute.

Using Valgrind (Memcheck)

Valgrind's Memcheck tool detects memory errors at runtime without requiring you to attach a debugger interactively. It is excellent for catching use-after-free, invalid reads/writes, memory leaks, and uninitialized value usage.

gcc -g -O0 -o myprogram myprogram.c
valgrind ./myprogram

Valgrind output for a use-after-free example:

==12345== Invalid write of size 4
==12345==    at 0x4005A3: main (myprogram.c:10)
==12345==  Address 0x4c2f040 is 0 bytes inside a block of size 4 free'd
==12345==    at 0x4C2B3F0: free (in /usr/lib/valgrind/...)
==12345==    by 0x40059E: main (myprogram.c:9)
==12345==  Block was alloc'd at
==12345==    at 0x4C2A9F0: malloc (in /usr/lib/valgrind/...)
==12345==    by 0x40058D: main (myprogram.c:7)

Valgrind pinpoints the exact line where the invalid write occurred, where the memory was freed, and where it was originally allocated. This is invaluable for tracking down complex heap bugs.

Using Address Sanitizer (ASan)

Address Sanitizer is a compiler-based tool (available in GCC and Clang) that instruments your code at compile time to detect memory errors at runtime with minimal overhead. It catches buffer overflows, use-after-free, double-free, and more, often with very precise error messages.

gcc -fsanitize=address -g -O0 -o myprogram myprogram.c
./myprogram

Example ASan output for a buffer overflow:

=================================================================
==12345==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60300000eff8 at pc 0x0000004008a3
READ of size 4 at 0x60300000eff8 thread T0
    #0 0x4008a2 in main myprogram.c:15
0x60300000eff8 is located 0 bytes to the right of 40-byte region [0x60300000efd0,0x60300000eff8)
allocated by thread T0 here:
    #0 0x4009e0 in malloc (/lib/...)
    #1 0x40087d in main myprogram.c:10

ASan is often faster than Valgrind for day-to-day development and provides excellent stack traces. It is highly recommended to always compile with -fsanitize=address during development and testing.

Analyzing Core Dumps

When a program crashes with "core dumped," it writes a snapshot of its memory and registers to a file (usually named core or core.PID). You can analyze this post-mortem with GDB without re-running the program.

Enable core dumps:

ulimit -c unlimited    # allow unlimited core dump file size

Analyze the core file:

gdb ./myprogram core
(gdb) bt
(gdb) info registers
(gdb) print ptr

This is extremely useful for debugging crashes that happen in production or on remote machines where you cannot run a live debugger.

Strategic Print Statements (The "Binary Search" Method)

When tools are unavailable (e.g., on an embedded system), you can narrow down a segfault by inserting print statements to log progress. Use fflush(stdout) after each print to ensure the output is flushed before a crash, so you know exactly which print was the last one displayed.

printf("Checkpoint 1\n"); fflush(stdout);
do_something();
printf("Checkpoint 2\n"); fflush(stdout);
do_something_else();
printf("Checkpoint 3\n"); fflush(stdout);  // if this never prints, crash is in do_something_else()

How to Fix Segmentation Faults — Practical Examples

Example 1: Fixing a Null Pointer Dereference

Buggy code:

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

typedef struct Node {
    int value;
    struct Node *next;
} Node;

void print_node_value(Node *node) {
    printf("Value: %d\n", node->value);  // BUG: no null check
}

int main() {
    Node *head = NULL;
    print_node_value(head);  // segfault!
    return 0;
}

Fixed code with null guard:

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

typedef struct Node {
    int value;
    struct Node *next;
} Node;

void print_node_value(Node *node) {
    if (node == NULL) {
        printf("Error: node is NULL, cannot print value.\n");
        return;
    }
    printf("Value: %d\n", node->value);
}

int main() {
    Node *head = NULL;
    print_node_value(head);  // graceful error message instead of crash
    return 0;
}

The fix adds a defensive null check before dereferencing the pointer. In production code, you should also validate that head is properly initialized before any operations that assume a non-null list.

Example 2: Fixing an Array Out-of-Bounds Access

Buggy code:

#include <stdio.h>

int main() {
    int numbers[5] = {10, 20, 30, 40, 50};
    
    // Iterating one past the last element
    for (int i = 0; i <= 5; i++) {   // BUG: i <= 5 should be i < 5
        printf("numbers[%d] = %d\n", i, numbers[i]);
    }
    return 0;
}

Fixed code:

#include <stdio.h>

int main() {
    int numbers[5] = {10, 20, 30, 40, 50};
    int array_size = sizeof(numbers) / sizeof(numbers[0]);
    
    for (int i = 0; i < array_size; i++) {   // correct: i < array_size
        printf("numbers[%d] = %d\n", i, numbers[i]);
    }
    return 0;
}

The fix uses i < array_size instead of i <= 5. The constant 5 was replaced with a computed array_size to prevent the error from recurring if the array size changes later. Always compute the array size using sizeof(array)/sizeof(array[0]) for static arrays.

Example 3: Fixing a Use-After-Free

Buggy code:

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

int main() {
    char *buffer = (char*)malloc(100);
    strcpy(buffer, "Hello, world!");
    
    printf("Before free: %s\n", buffer);
    free(buffer);
    printf("After free: %s\n", buffer);  // BUG: using freed memory
    
    return 0;
}

Fixed code with pointer nullification:

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

int main() {
    char *buffer = (char*)malloc(100);
    if (buffer == NULL) {
        fprintf(stderr, "Memory allocation failed.\n");
        return 1;
    }
    strcpy(buffer, "Hello, world!");
    
    printf("Before free: %s\n", buffer);
    
    free(buffer);
    buffer = NULL;   // critical: nullify the pointer after freeing
    
    // Any subsequent use of buffer will now be a clean null pointer dereference
    // which is easier to detect than a subtle use-after-free
    if (buffer != NULL) {
        printf("After free: %s\n", buffer);
    } else {
        printf("Buffer has been freed and is no longer available.\n");
    }
    
    return 0;
}

The fix nullifies the pointer immediately after free(). This converts a dangerous use-after-free into a deterministic null pointer dereference that tools like ASan or even a simple null check can catch. It also makes the intent explicit in the code.

Example 4: Fixing a Stack Overflow from Deep Recursion

Buggy code — infinite recursion:

#include <stdio.h>

unsigned long long factorial(unsigned int n) {
    // BUG: no base case for n == 0 or n == 1
    return n * factorial(n - 1);  // infinite recursion for n > 0
}

int main() {
    printf("Factorial of 5: %llu\n", factorial(5));
    return 0;
}

Fixed code with proper base case:

#include <stdio.h>

unsigned long long factorial(unsigned int n) {
    if (n == 0 || n == 1) {
        return 1;           // base case: stops the recursion
    }
    return n * factorial(n - 1);
}

int main() {
    printf("Factorial of 5: %llu\n", factorial(5));
    return 0;
}

Every recursive function must have a base case that does not call itself. For large inputs that could still overflow the stack, consider converting the algorithm to an iterative version:

unsigned long long factorial_iterative(unsigned int n) {
    unsigned long long result = 1;
    for (unsigned int i = 2; i <= n; i++) {
        result *= i;
    }
    return result;
}

Iterative versions use constant stack space and eliminate the risk of stack overflow entirely.

Example 5: Fixing Incorrect scanf Arguments

Buggy code:

#include <stdio.h>

int main() {
    char *name;               // uninitialized pointer
    printf("Enter your name: ");
    scanf("%s", name);        // BUG: writing to random memory via uninitialized pointer
    printf("Hello, %s!\n", name);
    return 0;
}

Fixed code with proper buffer and field width limit:

#include <stdio.h>

int main() {
    char name[100];           // allocated buffer on the stack
    printf("Enter your name: ");
    // Use %99s to prevent buffer overflow (leaves room for null terminator)
    scanf("%99s", name);
    printf("Hello, %s!\n", name);
    return 0;
}

The fix replaces the uninitialized pointer with a stack-allocated buffer and uses a width specifier (%99s) to prevent buffer overflow. For even safer input handling, consider using fgets() instead:

char name[100];
printf("Enter your name: ");
if (fgets(name, sizeof(name), stdin) != NULL) {
    // Remove trailing newline if present
    size_t len = strlen(name);
    if (len > 0 && name[len - 1] == '\n') {
        name[len - 1] = '\0';
    }
    printf("Hello, %s!\n", name);
}

Best Practices to Prevent Segmentation Faults

1. Always Initialize Pointers

Never declare a pointer without assigning it a valid value. Initialize pointers to NULL if you don't have a valid target yet, or allocate memory immediately.

// Good: initialize to NULL
int *ptr = NULL;
// Later, before use:
ptr = (int*)malloc(sizeof(int));
if (ptr == NULL) { /* handle error */ }

// In C++, prefer:
int *ptr = nullptr;
// Or better yet, use smart pointers (see below)

2. Check Return Values of Memory Allocation Functions

malloc, calloc, realloc, and new (in C++ with nothrow) can fail and return NULL. Always check.

int *data = (int*)malloc(1000 * sizeof(int));
if (data == NULL) {
    fprintf(stderr, "Memory allocation failed. Exiting.\n");
    exit(EXIT_FAILURE);
}

3. Use Bounds Checking and Safe String Functions

Prefer functions that limit the amount of data written:

char dest[32];
const char *src = "This is a very long string that will overflow";
snprintf(dest, sizeof(dest), "%s", src);  // safe: truncates to fit dest

4. Set Freed Pointers to NULL Immediately

After free(ptr), set ptr = NULL. This turns any accidental subsequent use into a clean crash that is easier to debug than a corrupted heap. In C++, after delete ptr, set ptr = nullptr.

free(ptr);
ptr = NULL;   // prevent dangling pointer reuse

5. Use Smart Pointers in C++

C++ offers std::unique_ptr and std::shared_ptr that automatically manage memory and prevent use-after-free and double-free errors.

#include <memory>
#include <iostream>

struct Data {
    int value;
    Data(int v) : value(v) {}
};

int main() {
    // Automatic cleanup when ptr goes out of scope
    std::unique_ptr<Data> ptr = std::make_unique<Data>(42);
    std::cout << "Value: " << ptr->value << std::endl;
    // No manual delete needed — ptr is automatically freed
    return 0;
}

6. Enable Compiler Warnings and Treat Them as Errors

Modern compilers can detect many potential segfault causes at compile time.

gcc -Wall -Wextra -Werror -o myprogram myprogram.c
# -Wall: enable all common warnings
# -Wextra: enable extra warnings
# -Werror: treat warnings as errors (forces you to fix them)

7. Use Static Analysis Tools

Tools like cppcheck, clang-tidy, or Coverity can find potential null pointer dereferences, buffer overflows, and other memory bugs without running the program.

cppcheck --enable=all myprogram.c
clang-tidy myprogram.c --checks=* --

8. Compile with Sanitizers During Development and Testing

Always include Address Sanitizer (and optionally Undefined Behavior Sanitizer) in your debug/test builds.

# Development/debug build
gcc -fsanitize=address -fsanitize=undefined -g -O0 -o myprogram_debug myprogram.c

# Release build (without sanitizers for performance)
gcc -O2 -DNDEBUG -o myprogram_release myprogram.c

9. Use Vector and Array Containers in C++

Prefer std::vector, std::array, and std::string over raw C arrays and pointers. These containers manage memory automatically and provide bounds-checked access via at().

#include <vector>
#include <iostream>

int main() {
    std::vector<int> numbers = {10, 20, 30, 40, 50};
    
    // at() throws std::out_of_range on invalid index instead of segfaulting
    try {
        std::cout << numbers.at(10) << std::endl;  // throws exception
    } catch (const std::out_of_range& e) {
        std::cerr << "Index out of bounds: " << e.what() << std::endl;
    }
    return 0;
}

10. Code Review and Defensive Programming

Write functions that validate their inputs. Use assertions to catch programming errors early in development.

#include <assert.h>

void process_data(int *data, size_t size) {
    assert(data != NULL);     // catches null pointer during development
    assert(size > 0);         // catches invalid size
    
    if (data == NULL || size == 0) {
        return;  // graceful handling in production (when asserts are disabled)
    }
    
    // safe processing here
    for (size_t i = 0; i < size; i++) {
        data[i] *= 2;
    }
}

Conclusion

Segmentation faults are a fundamental aspect of systems programming in C and C++. They arise from the direct memory access model that gives these languages their power and performance. Rather than fearing segfaults, developers should treat them as valuable signals that expose latent memory bugs. The key to mastering segfault resolution is a three-part approach: prevention through disciplined coding practices like initializing pointers, bounds checking, and using safe abstractions; detection using modern tools such as GDB, Valgrind, and Address Sanitizer to catch errors early in the development cycle; and systematic debugging that follows the evidence from the crash site back to the root cause. By combining these strategies — and by embracing C++ features like smart pointers, vectors, and RAII when appropriate — you can write robust, crash-free software that fully harnesses the capabilities of native code without suffering from its most notorious pitfalls.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles