Undefined Behavior in C: Common Causes, Examples, and Fixes

Learn to find the first undefined operation in common C programs, repair the missing invariant, and verify the corrected result with worked examples.

KnowledgeGate Team

Exam prep & CS education

Updated 14 Oct 20266 min read

A C program can compile, run, and display output even though the language defines no result for what it did, making undefined behavior dangerous for learners.

A familiar-looking value from one run is not proof: once a program performs an undefined operation, no numeric output can be promised.

The most common cases can be classified, traced, repaired, and tested by checking bounds, lifetimes, sequencing, arithmetic ranges, and library contracts.

What undefined behavior in C actually means

Undefined behavior begins at an operation for which C imposes no required result. A crash, a stale value, a surprising optimization, or apparently correct output can all be observed, but none is guaranteed. A compiler is not generally required to diagnose every undefined operation.

Do not put every portability issue into the same bucket:

Category

Example

What C requires

Undefined

Dereferencing score + 3 for int score[3]

No required result

Implementation-defined

Whether plain char is signed or unsigned

The implementation chooses and documents it

Unspecified

Whether independent left() or right() runs first in left() + right()

Either order is allowed; the sum remains defined if there are no conflicting side effects

For a memory-first treatment of addresses, pointer arithmetic, and tracing, use the Pointers in C for GATE guide. Undefined-behavior diagnosis starts at the first operation whose precondition fails, then repairs that condition before execution.

Fully worked example: the one-past array read

Trace this program first:

c
#include <stdio.h>

int main(void) {
    int score[3] = {12, 24, 36};
    int sum = 0;

    for (size_t i = 0; i <= 3; ++i) {
        sum += score[i];
    }

    printf("%d\n", sum);
    return 0;
}

Trace until the invalid access:

  • i = 0 reads 12, so sum = 12.

  • i = 1 reads 24, so sum = 36.

  • i = 2 reads 36, so sum = 72.

  • i = 3 attempts to read score[3].

A pointer one position past an array may be formed for comparison, but it may not be dereferenced. Stop here: undefined behavior. There is no C-defined final sum or output.

Use size_t n = sizeof score / sizeof score[0]; with i < n. It visits indices 0, 1, and 2, so it prints exactly 72. Before a[i], prove 0 <= i < element_count, or simply i < element_count when i is unsigned.

Memory diagram of int score[3] = {12, 24, 36} with running sums 12, 36, 72 and a dashed one-past score + 3 slot the loop must not read.

Signed overflow, zero divisors, and invalid shift counts

For int x = INT_MAX; int y = x + 1;, the addition is undefined because its mathematical result is outside the range of int. With a common 32-bit int, INT_MAX is 2,147,483,647, while the wanted result is 2,147,483,648. Portable code reads INT_MAX from <limits.h>. Check before evaluating: reject when x == INT_MAX; otherwise compute y = x + 1. Thus x = 41 gives y = 42, while x = INT_MAX takes the rejection branch.

For int marks = 84, groups = 0; int each = marks / groups;, integer division by zero is undefined. Check groups != 0 first. With groups = 7, each = 84 / 7 = 12; with zero, report an application error without executing /.

Finally, consider unsigned bit = 1u; unsigned width = sizeof bit * CHAR_BIT; bit << width;. A shift count equal to the promoted left operand's width is undefined. On a 32-bit unsigned, width = 32. Require shift < width; safe shift = 5 gives 1u << 5 = 32.

Number line placing 2,147,483,648 outside the int range, beside a branch rejecting x == INT_MAX and computing 41 + 1 to give y = 42.

Dangling pointers and object lifetime errors

Consider int *bad(void) { int answer = 42; return &answer; }, followed by printf("%d\n", *bad());. The local object answer stops existing when bad returns. The returned pointer therefore provides no access to a live int, and dereferencing it is undefined. No printed value is defined.

Give the caller ownership instead: void set_answer(int *out) { *out = 42; }. Then int answer; set_answer(&answer); printf("%d\n", answer); prints exactly 42.

Heap storage has the same lifetime rule. Suppose a program allocates one int, checks that allocation succeeded, assigns 73, calls free(p), and then evaluates printf("%d\n", *p);. The last dereference is undefined even if the bytes appear unchanged. Finish every read before free, make ownership explicit, and never dereference the pointer afterwards. Setting p = NULL is a useful defensive aid, but it does not make dereferencing valid. Use Dynamic Memory Allocation in C for allocation, realloc, and deallocation patterns. A use-after-free trace needs the lifetime boundary: free ends access to the object, so the next dereference is invalid.

Ask during review: “Is the pointed-to object alive for the entire access?” Local-scope exit, free, and storage invalidated by a successful realloc are important lifetime boundaries.

Unsequenced side effects and broken library contracts

In int i = 4; int result = i++ + i;, the modification and other access to i are unsequenced, so the expression has undefined behavior. Separate the operations: int old = i; ++i; int result = old + i;. Now old = 4, i = 5, and result = 9.

Library and language contracts matter too:

  • char *label = "GATE"; label[0] = 'L'; attempts to modify a string literal and is undefined. With char label[] = "GATE"; label[0] = 'L';, the writable array contains exactly "LATE".

  • long attempts = 50000L; printf("%d\n", attempts); mismatches the variadic argument and format. printf("%ld\n", attempts); prints 50000.

  • Given char text[6] = "ABCDE", memcpy(text + 1, text, 4) violates memcpy's non-overlap requirement. Use memmove(text + 1, text, 4) instead; the array becomes "AABCD".

The general lesson is to satisfy every standard-library precondition, not merely call a familiar function.

A repeatable diagnostic and repair workflow

For one isolated example at a time, build with:

sh
cc -std=c17 -O1 -g -Wall -Wextra -Wpedantic -Wconversion -fsanitize=address,undefined -fno-omit-frame-pointer ub.c -o ub-demo

Compiler and sanitizer availability and messages vary. Neither clean warnings nor a clean sanitizer run proves the absence of every UB category.

Use a fixed order: reduce the case to a minimal reproducer, identify the first invalid operation, state the missing invariant in plain language, enforce it before the operation, run the corrected program, then test boundary values. Compare -O0 and -O2 only to expose a symptom, never to call one build correct.

The repaired examples produce these defined results: array sum 72, increment 42, division 12, shift 32, caller-owned answer 42, sequenced result 9, writable label LATE, formatted count 50000, and overlapping move result AABCD. During timed practice, apply the same first-invalid-operation rule before attempting an output.

Traps and three exercises with answer keys

  1. Classify int a[2] = {7, 11}; printf("%d\n", a[2]);. Answer: undefined, because a[2] dereferences the one-past position. Read only a[0] or a[1], or deliberately change the required data and array size.

  2. Classify int q = 3; int r = q++ + q;. Answer: undefined because the modification and access are unsequenced. Rewrite it as int old = q; ++q; int r = old + q;. Then old = 3, q = 4, and r = 7.

  3. Compare UINT_MAX + 1u with INT_MAX + 1. Answer: unsigned arithmetic wraps modulo UINT_MAX + 1, so the first value is defined as 0. The signed overflow in the second expression is undefined.

Avoid the classic wrong answers “it always wraps” and “the compiler will always catch it”.

How exams and interviews test undefined behavior

Stable questions ask you to identify the first undefined operation, classify UB versus implementation-defined or unspecified behavior, choose a safe rewrite, or explain why an optimizer assumes valid code avoids signed overflow and invalid dereferences.

If asked for output after UB, answer “no C-defined output”, not a value from one compiler. Stop at the first broken rule and repair its precondition.

The short version

Prove array bounds, keep objects alive, separate side effects, validate divisors and shift counts, guard signed ranges before arithmetic, and match every library contract.

Build the full foundation in the C Language course. Use Coding For Placements for assessment practice, or browse the Coding & DSA course pathway.