Undefined Behaviour & Common Bugs

C · Lesson 30 of 32 8 min read Updated

C makes very few promises about programs that break its rules. When code does something the standard calls undefined behaviour (UB), the C standard places no requirements at all on what happens. The program might crash, print garbage, or work perfectly today and fail after a compiler upgrade. This is the price of C's speed: the compiler is allowed to assume that undefined behaviour never happens.

In this lesson you will learn the difference between undefined, unspecified and implementation-defined behaviour, see why undefined behaviour is more dangerous than it looks, and review the bugs that cause most problems in real C programs, with a clear fix for each.

Three kinds of "it depends"

KindMeaningExamples
Implementation-definedEach compiler chooses and documents the behaviourThe size of int and long; right-shifting a negative number
UnspecifiedThe standard allows several outcomes; the compiler need not say whichThe order in which function arguments are evaluated
UndefinedThe standard sets no rules at allSigned overflow, out-of-bounds access, using freed memory

Implementation-defined behaviour is safe as long as you do not assume it is the same everywhere; that is why you use sizeof and stdint.h. Unspecified behaviour is safe as long as your result does not depend on the choice. Undefined behaviour is never safe.

Why undefined behaviour is so dangerous

You might think signed overflow "just wraps around" like it does in the processor. But the optimiser reasons differently. Look at this attempt to detect overflow:

C
#include <limits.h>

bool will_overflow(int x)
{
    return x + 1 < x;   // relies on signed overflow: undefined
}

The compiler knows that signed overflow cannot happen in a valid program, so for every valid x, x + 1 < x is false. With optimisation turned on, GCC and Clang typically compile this function to return false;, so will_overflow(INT_MAX) says "no". The check you wrote has silently disappeared. The correct version tests before doing the risky operation:

C
#include <limits.h>
#include <stdio.h>

bool will_overflow(int x)
{
    return x == INT_MAX;   // no overflow can happen here
}

int main(void)
{
    printf("INT_MAX: %d\n", will_overflow(INT_MAX));
    printf("41: %d\n", will_overflow(41));
    return 0;
}
Output
INT_MAX: 1
41: 0

The same reasoning lets compilers delete null checks that come after a dereference, or assume that a loop index never goes past an array. Undefined behaviour does not only give a wrong value at one spot; it can change what the rest of your program does.

The most common undefined behaviours

MistakeExampleSafe alternative
Out-of-bounds accessa[n] in an array of n elementsLoop with i < n; pass lengths
Reading uninitialised memoryint total; total += 5;Initialise every variable
Signed overflowINT_MAX + 1Check first, or use ckd_add (C23)
Null pointer dereference*p when p is nullptrCheck pointers before use
Use after free, double freefree(p); p[0] = 1;Set p = nullptr after free
Changing a string literalchar *s = "Pune"; s[0] = 'p';Copy into a char array
Two unsequenced changesi = i++ + 1;One change per statement
Bad shifts1 << 32, x << -1Use unsigned types and valid counts
Integer division by zerototal / count with count == 0Check the divisor first
Returning a local's addressreturn &local;Return a value or allocated memory

Unsequenced modifications

C does not fix the order in which most parts of an expression are evaluated. If one expression changes a variable and also reads or changes it elsewhere, without a sequence point between them, the behaviour is undefined.

C
#include <stdio.h>

int main(void)
{
    int i = 5;
    // printf("%d %d\n", i++, i++);   // undefined: i changed twice
    int first = i++;
    int second = i++;
    printf("%d %d\n", first, second);
    return 0;
}
Output
5 6

Splitting the work into separate statements makes the order explicit. A related case is only unspecified: in printf("%d %d\n", next(), next());, both calls happen, but C does not say which runs first, so the output could be "1 2" or "2 1". Again, call the functions in separate statements when the order matters.

Everyday bugs that are not undefined

Plenty of bugs are perfectly defined C that simply does the wrong thing. This short program has four classic ones:

C
#include <stdio.h>

int main(void)
{
    int marks[5] = {45, 82, 67, 91, 38};
    int passed = 0;
    int total;                          // bug: not initialised

    for (int i = 1; i <= 5; i++) {      // bug: should be i = 0; i < 5
        total += marks[i];
        if (marks[i] = 40) {            // bug: = instead of >=
            passed++;
        }
    }
    double average = total / 5;         // bug: integer division
    printf("Average %.2f, passed %d\n", average, passed);
    return 0;
}

Here is the corrected version:

C
#include <stdio.h>

int main(void)
{
    int marks[5] = {45, 82, 67, 91, 38};
    int passed = 0;
    int total = 0;

    for (int i = 0; i < 5; i++) {
        total += marks[i];
        if (marks[i] >= 40) {
            passed++;
        }
    }
    double average = total / 5.0;
    printf("Average %.2f, passed %d\n", average, passed);
    return 0;
}
Output
Average 64.60, passed 4

Compiling the first version with -Wall -Wextra warns about the assignment used as a condition and, depending on the GCC version and optimisation level, about the uninitialised total. The off-by-one loop and the integer division compile silently, which is why testing and sanitizers matter too. Other frequent bugs from this course:

  • A missing break in a switch (warned by -Wextra).
  • Comparing strings with == instead of strcmp.
  • Comparing double values with ==.
  • Using sizeof on an array parameter, which gives the pointer size.
  • Forgetting the & in scanf, or the + 1 for a string's '\0'.
  • Ignoring return values from scanf, fopen and malloc.
  • A stray semicolon after if (...), for (...) or while (...).

A defence plan

No single habit prevents every bug, but together these catch most of them:

  1. Compile with -Wall -Wextra -pedantic, and fix every warning. Consider -Werror in your own projects.
  2. Run tests with -fsanitize=address,undefined (see Debugging with gdb & Sanitizers).
  3. Try GCC's -fanalyzer for problems the normal warnings miss.
  4. Initialise every variable and every pointer when you declare it.
  5. Use size-limited functions: fgets, snprintf, strtol, and C23 ckd_add for overflow.
  6. Check every return value that can signal failure.
  7. Keep functions small and use const, so each piece is easy to reason about and test.

Common mistakes

  • "It works on my machine." Undefined behaviour can look fine in one build and fail in another. Correct output is not proof of correct code.
  • Writing overflow checks after the overflow. Test the inputs before the operation.
  • Relying on argument evaluation order. Split calls into separate statements.
  • Turning off warnings to get a clean build. The warning is usually right.

Key points

  • Undefined behaviour has no rules; the compiler may assume it never happens and optimise accordingly.
  • Implementation-defined behaviour varies by compiler but is documented; unspecified behaviour allows several outcomes.
  • The common UBs are out-of-bounds access, uninitialised reads, signed overflow, null and dangling pointers, and unsequenced changes.
  • Check before risky operations; never test for overflow by causing it.
  • Many bugs are defined but wrong: off-by-one loops, integer division, = versus ==, missing break.
  • Warnings, sanitizers, static analysis and safe library functions together prevent most C bugs.

Exercise

This program should print the city name backwards, but it has two bugs, one of them undefined behaviour. Find both and fix them.

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

int main(void)
{
    char city[6];
    strcpy(city, "Lucknow");
    int len = strlen(city);
    for (int i = len; i >= 0; i--) {
        printf("%c", city[i]);
    }
    printf("\n");
    return 0;
}
Show solution

First, "Lucknow" needs 8 bytes (7 letters and '\0'), but city has only 6, so strcpy overflows the array: undefined behaviour. Second, the loop starts at len, which is the position of the '\0', not the last letter, so it prints an invisible null character first: an off-by-one bug.

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

int main(void)
{
    char city[] = "Lucknow";
    int len = (int) strlen(city);
    for (int i = len - 1; i >= 0; i--) {
        printf("%c", city[i]);
    }
    printf("\n");
    return 0;
}
Output
wonkcuL

Letting the compiler size the array from the literal removes the overflow, and starting at len - 1 prints exactly the 7 letters in reverse.

Quick Quiz

1. What does the C standard say about a program that reads past the end of an array?

2. Why might an optimising compiler remove the check if (x + 1 < x) for an int x?

3. Which of these is unspecified rather than undefined?

4. What is the safe way to check whether a + b overflows an int?

Learn C with a mentor

Free lessons get you started. Our Java, C & C++ Programming training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call