Undefined Behaviour & Common Bugs
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"
| Kind | Meaning | Examples |
|---|---|---|
| Implementation-defined | Each compiler chooses and documents the behaviour | The size of int and long; right-shifting a negative number |
| Unspecified | The standard allows several outcomes; the compiler need not say which | The order in which function arguments are evaluated |
| Undefined | The standard sets no rules at all | Signed 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:
#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:
#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;
}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
| Mistake | Example | Safe alternative |
|---|---|---|
| Out-of-bounds access | a[n] in an array of n elements | Loop with i < n; pass lengths |
| Reading uninitialised memory | int total; total += 5; | Initialise every variable |
| Signed overflow | INT_MAX + 1 | Check first, or use ckd_add (C23) |
| Null pointer dereference | *p when p is nullptr | Check pointers before use |
| Use after free, double free | free(p); p[0] = 1; | Set p = nullptr after free |
| Changing a string literal | char *s = "Pune"; s[0] = 'p'; | Copy into a char array |
| Two unsequenced changes | i = i++ + 1; | One change per statement |
| Bad shifts | 1 << 32, x << -1 | Use unsigned types and valid counts |
| Integer division by zero | total / count with count == 0 | Check the divisor first |
| Returning a local's address | return &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.
#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;
}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:
#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:
#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;
}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
breakin aswitch(warned by-Wextra). - Comparing strings with
==instead ofstrcmp. - Comparing
doublevalues with==. - Using
sizeofon an array parameter, which gives the pointer size. - Forgetting the
&inscanf, or the+ 1for a string's'\0'. - Ignoring return values from
scanf,fopenandmalloc. - A stray semicolon after
if (...),for (...)orwhile (...).
A defence plan
No single habit prevents every bug, but together these catch most of them:
- Compile with
-Wall -Wextra -pedantic, and fix every warning. Consider-Werrorin your own projects. - Run tests with
-fsanitize=address,undefined(see Debugging with gdb & Sanitizers). - Try GCC's
-fanalyzerfor problems the normal warnings miss. - Initialise every variable and every pointer when you declare it.
- Use size-limited functions:
fgets,snprintf,strtol, and C23ckd_addfor overflow. - Check every return value that can signal failure.
- 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==, missingbreak. - 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.
#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.
#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;
}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?
Undefined behaviour means the standard imposes no requirements at all on the result.
2. Why might an optimising compiler remove the check if (x + 1 < x) for an int x?
The optimiser may assume undefined behaviour never happens, which makes the condition impossible.
3. Which of these is unspecified rather than undefined?
Both arguments are evaluated, but the order is not specified; the other two are undefined.
4. What is the safe way to check whether a + b overflows an int?
The check must happen before, or without, performing the overflowing operation.
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
