I believe you have come across the term “Undefined Behavior” (UB) in many programming books and blogs. However, many beginners and even some experienced developers struggle to fully understand what it actually means.
If you are one of them, this blog post is for you. In this article, you will learn the concept of undefined behavior in C and C++ programming in a clear and practical way.
So, without wasting any time, let’s get started.
During development, you have likely encountered situations where your code did not behave as expected. If you are new to programming, you will almost certainly face such scenarios in the future. These issues are often caused by undefined behavior, and they can be extremely difficult to debug sometimes becoming a nightmare for developers.
Undefined behavior means that anything can happen. Your program might fail to compile, execute incorrectly (either by crashing or silently producing wrong results), or sometimes even appear to work perfectly as intended. In simple terms, when the outcome of a program is unpredictable, it is said to exhibit undefined behavior.
It is the programmer’s responsibility to understand the situations in which code can lead to undefined behavior especially when working with C and C++, where such cases are common and often occurs.
Now, let’s understand the UB and look at some examples of C/C++ code that result in undefined behavior.
What is Undefined Behavior?
Undefined Behavior (UB) refers to program behavior for which the C/C++ standard provides no guarantees or requirements. When UB occurs, the language standard does not specify what the program should do.
Once a program triggers UB, any outcome is possible, including:
- The program may crash.
- It may produce incorrect results.
- It may appear to work correctly.
- It may behave differently across:
- Compilers.
- Optimization levels.
- Platforms.
- Different runs of the same program.
Why Does Undefined Behavior Exist?
Undefined Behavior is not a flaw in C/C++; it is a design choice. It exists to give compilers the freedom to optimize efficiently by assuming that programs never perform invalid operations. When that assumption is violated, the program’s behavior becomes unpredictable.
It allows compilers to:
- Generate highly optimized code.
- Avoid expensive runtime checks.
- Make strong assumptions about program correctness
These assumptions enable aggressive optimizations that would otherwise be impossible.
Example,
int x;
if (x == x)
{
// Compiler may assume always true
}
If x is uninitialized, this is UB. The compiler may optimize based on assumptions that break your logic.
Common Examples of Undefined Behavior in C and C++:
Here is a list of examples to explain undefined behavior in C/C++. It will save you a lot of time and make your life easier.
1. Accessing Array Out of Bounds:
C/C++ does not perform bounds checking, which can silently corrupt memory, especially in embedded systems. Accessing an array out of bounds results in undefined behavior (UB).
#include<stdio.h>
//a lookup table
int lookupTable[5] = {0};
int readLookupTable(int index)
{
const int value = lookupTable[index];
return value;
}
int main()
{
/*
Undefined behavior for index 5
because it is out of array bounds
*/
readLookupTable(5);
return 0;
}
2. Returning the Address of a Local Variable:
Accessing a variable outside of its lifetime results in undefined behavior. Consider the below example,
#include<stdio.h>
int *foo()
{
//Local variable
int var = 5;
//Returning address of the local variable
return &var;
}
int main()
{
int *ptr = foo();
//undefined behavior.
printf("%d", *ptr);
return 0;
}
What happens:
- var is a local variable, stored on the stack.
- When the function foo() returns, its stack frame is destroyed, and var no longer exists.
- The pointer returned (&var) now points to invalid memory.
Solution: Use a static local variable instead of a regular local variable to safely return a pointer from a function.
int* foo()
{
static int var = 5; // persists across function calls
return &var; // safe to return, but shared among all calls
}
3. Using a Pointer After Freeing Memory:
If a pointer points to memory that has been freed (or whose lifetime has ended), any use of that pointer is undefined behavior.
#include <stdio.h>
#include <stdlib.h>
int main()
{
// Allocate dynamic memory for 5 integers
int* ptr = malloc(sizeof(int) * 5);
if (ptr == NULL)
{
return -1; // Allocation failed
}
// Free the allocated memory
free(ptr);
// Undefined behavior: using ptr after free
*ptr = 2; // Memory has been released, cannot write to it
return 0;
}
What happens:
- After free(ptr), the memory is no longer valid for reading or writing.
- Using ptr after freeing it can crash your program or cause unpredictable behavior.
Best practice:
Set pointer to NULL after freeing to avoid accidental use.
free(ptr); ptr = NULL; // safe guard
4. Modifying a string literal
If a program attempts to modify a string literal, the behavior is undefined. Consider the code below.
char* ptr = "aticleworld"; ptr[2] = 'I'; // Undefined behavior
What happens,
- In C/C++, string literals are usually stored in read-only memory, so you should not try to change them. If you do, it can lead to undefined behavior.
- Your program might crash, corrupt memory, or sometimes even seem to work, depending on the compiler and system.
Correct approach:
Use a character array if you want to modify the content:
char ptr[] = "aticleworld"; // Array, not literal ptr[2] = 'I'; // Safe
5. Signed integer overflow:
Signed integer overflow in C/C++ leads to undefined behavior. Consider the example below, in which everything works fine as long as the variable data does not exceed INT_MAX.
int foo(int data)
{
return data + 1 > data;
}
What happens,
- Signed overflow (e.g., data == INT_MAX) is undefined in C/C++.
- Unlike unsigned overflow (which wraps around), signed overflow may lead to unexpected results, crashes, or optimizations that break assumptions.
Safe alternative: use unsigned integers for arithmetic where wraparound is acceptable, or check before overflow:
if (data < INT_MAX)
{
data += 1;
}
6. Uninitialized Local Object:
A local object (with automatic storage) has an unpredictable value if it is not explicitly initialized or assigned before use. Using such an object in your code leads to undefined behavior.
int main()
{
int p; // uninitialized
if (p)
{
printf("Hi\n");
} else
{
printf("Bye\n");
}
}
What happens,
- Local variables (automatic storage) are not initialized by default. Using them before assigning a value gives indeterminate behavior, which is UB.
- Depending on the compiler, p could have any garbage value.
Safe practice:
Always initialize local variables: int p = 0;
7. Division or modulo by zero:
In C and C++, the binary / operator gives the quotient, and the binary % operator gives the remainder when the first value is divided by the second. If the second value is 0, using / or % causes undefined behavior.
int data = 1; return (data / 0); // UB
What happens,
- / and % require a non-zero second operand. Dividing by zero is undefined.
- Could lead to crash (SIGFPE), incorrect results, or silent memory corruption.
Safe check:
if (denominator != 0)
{
result = numerator / denominator;
}
8. Dereference of null pointer:
Attempting to dereference a null pointer causes undefined behavior because the unary * operator is not defined for null pointer values.
int* ptr = NULL; *ptr = 2; // UB
What happens,
- Dereferencing a null pointer leads to undefined behavior. The program may crash immediately, or in rare cases, it might appear to work.
- In above expressions like *ptr, the dereference happens before any checks, so if ptr is NULL, the undefined behavior occurs right away.
Safe approach:
Always check if the pointer is not NULL before dereferencing it.
int foo(int* ptr)
{
if (ptr)
{
data = *ptr;
}
else
{
// handle null pointer
}
}
9. Accessing pointer after realloc:
The realloc function is different from malloc and calloc. It may either resize the existing memory block or allocate a new one and free the old block. Therefore, in the below code, if you try to use ptr1 after passing it to realloc, undefined behavior (UB) will occur.
int *ptr1 = malloc(sizeof(int)); int *ptr2 = realloc(ptr1, sizeof(int)); *ptr1 = 1; // UB
What happens,
- After calling realloc, the original pointer (ptr1) may no longer be valid.
- So, even if it appears unchanged, you must not use ptr1 anymore. Accessing it results in undefined behavior (UB).
Safe approach:
Always use the return value of realloc (ptr2 in this case) and check for NULL before using it.
int *ptr1 = malloc(sizeof(int));
int *ptr2 = realloc(ptr1, sizeof(int));
if (ptr2 != NULL)
{
ptr1 = ptr2; // update pointer
*ptr1 = 1; // Now safe
}
Why Undefined Behavior is dangerous in embedded systems:
Undefined Behavior (UB) is very dangerous in embedded systems because these systems often run important code that affects the real world. UB can make the system do unexpected things, crash, or give wrong results, and it is usually very hard to find and fix.
Here is why UB is especially risky for embedded devices:
1. UB Can Break Determinism
Embedded systems often rely on deterministic execution to meet real-time deadlines. Undefined behavior undermines this predictability, potentially causing catastrophic failures. Here is why,
- Timing unpredictability: UB can change or reorder code, making execution times inconsistent and breaking real-time deadlines.
- Silent failures: UB may not crash immediately but cause intermittent faults, e.g., using freed pointers.
- Safety hazards: Missed deadlines or corrupted outputs in critical systems (automotive, pacemakers, avionics) can be catastrophic.
- Optimization pitfalls: Compilers assume UB never happens, so optimizations can produce unpredictable behavior.
2. UB Can Lead to Security Vulnerabilities:
Memory corruption, buffer overflows, and dangling pointers are common UB issues.
For example:
int arr[5]; arr[10] = 42; // Out-of-bounds write -> UB
This can overwrite critical memory in microcontrollers, leading to silent failures or even remote exploits in connected devices.
3. Produce Inconsistent Behavior:
Undefined behavior in embedded systems can lead to different outcomes depending on the compiler, optimization level, or even small unrelated changes in the code. This unpredictability is particularly dangerous in safety-critical systems.
For Example: Division by zero
int a = 5; int b = a / 0; // Undefined behavior!
Possible results of this UB:
- Crash or exception: Some compilers generate a hardware trap, stopping execution.
- Garbage value: The CPU may return an unpredictable value for b.
- Silent continuation: Some optimizations may remove the division entirely, making the program “appear to work,” hiding the bug.
4. UB Can Make Debugging a Nightmare
Because UB is non-deterministic, bugs can disappear or appear only in certain conditions:
- Works in simulation, fails on hardware.
- Works with one compiler version, fails with another.
- Makes testing almost useless if the UB is not caught early.
5. UB Can Corrupt System State
In embedded systems, the firmware interacts directly with hardware registers, memory-mapped I/O, and timing-critical operations. Undefined behavior in this context is especially dangerous because it can modify system state in unpredictable ways.
Key risks:
- Unexpected register writes: UB could make the compiler generate code that writes to hardware registers unexpectedly. For instance, writing past the bounds of an array might overwrite a nearby memory-mapped control register.
- GPIO misbehavior: Miscompilations from UB could toggle GPIO pins unintentionally, turning LEDs on/off, activating motors, or even causing short circuits in extreme cases.
- Watchdog resets: Accessing memory out of bounds might corrupt the stack or control flow, triggering a watchdog timer reset. This might happen in safety-critical control loops, e.g., motor control, where an unexpected reset could cause mechanical damage or injury.