Skip to Content
Course content

226: Undefined Behavior: Common Pitfalls

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Imagine you're following a recipe for a cake, but at one critical point, the instructions simply say: "Now, do something with the oven." It doesn't tell you to bake it, it doesn't tell you to preheat it, it just says "do something." One time, you might decide to bake the cake and it turns out great. The next time, you might accidentally set the oven to 500 degrees and burn your house down. The third time, you might just leave the door open and the cake stays raw. You followed the instructions—literally—but because the instructions were vague, the result is completely unpredictable.

That is exactly what Undefined Behavior (UB) is in C++. The C++ Standard is the recipe. When it specifies "Undefined Behavior," it's essentially telling the compiler, "I'm not specifying what happens here; you can do whatever you want." This is where things get scary. Your code might work perfectly on your machine with debug flags on, but the moment you ship it to a client with optimizations enabled, it crashes—or worse, it silently corrupts their database.

The "It Works on My Machine" Trap

The most dangerous thing about UB isn't that it crashes your program; it's that it often doesn't. I've spent days chasing bugs that only appeared in Release builds because the compiler's optimizer saw a piece of UB and decided to "optimize" that entire branch of logic out of existence. Since the standard says UB "cannot happen," the compiler assumes you'd never write code that does it, and it rearranges your logic in ways that make no sense to a human but are perfectly legal for a machine.

Dancing with Dead Memory

One of the most common ways I see developers trip up is the "use-after-free" scenario. You allocate some memory, you delete it, and then—somewhere else in the code—you try to access it again. To the compiler, that pointer is still a valid address, but the memory it points to has been handed back to the system.

int* data = new int(42);
delete data; 
// ... some other code runs here ...
std::cout << *data; // UB: This is a dangling pointer.

Maybe *data still prints 42. Maybe it prints a random number. Or maybe the operating system has already reclaimed that page of memory, and you get a segmentation fault. You're gambling with the state of the heap, and eventually, the house wins.

Stepping Outside the Array

We've talked about arrays before, but it's worth revisiting them through the lens of UB. Accessing an index outside the bounds of an array is a classic pitfall. Unlike languages like Java or Python, C++ won't throw an IndexOutOfBoundsException by default. It will just happily calculate the memory address where that index would be and try to read or write to it.

int scores[5] = {10, 20, 30, 40, 50};
scores[10] = 100; // UB: Buffer overflow

You might have just overwritten a different variable in your program, or you might have corrupted the return address on the stack. This is exactly how many security vulnerabilities (like buffer overflow attacks) work. You're writing into memory you don't own.

The Chaos of Uninitialized Values

Lastly, let's talk about the "garbage" value. If you declare a local variable without initializing it, it contains whatever random bits happened to be at that memory location previously. Reading from it is UB.

void checkStatus() {
    bool isReady; // Uninitialized
    if (isReady) { 
        // This branch might execute, or it might not.
        // It depends on what was in memory before this function was called.
    }
}

I've seen bugs where a program worked perfectly for three months because the memory happened to be zeroed out by the OS, only to fail the moment the program started using more RAM and the memory became "dirty." Don't trust the ghosts in your RAM—always initialize your variables.




📋 Practical Task

Fixing the Use-After-Free Bug in a Simple Cache

You are reviewing a colleague's code for a very basic caching system. The goal is to store a value and retrieve it. However, there is a critical Undefined Behavior bug where a pointer is being used after the memory it points to has been deleted. Your task is to fix the Cache class so that it no longer accesses dangling pointers.

#include <iostream>
#include <string>

class Cache {
    int* cachedValue;

public:
    Cache() : cachedValue(nullptr) {}
    ~Cache() { delete cachedValue; }

    void store(int val) {
        if (cachedValue) delete cachedValue;
        cachedValue = new int(val);
    }

    void clear() {
        delete cachedValue;
        // BUG: cachedValue is not set to nullptr here!
    }

    void printValue() {
        // This will trigger UB if clear() was called first
        if (cachedValue) { 
            std::cout << "Cached Value: " << *cachedValue << std::endl;
        } else {
            std::cout << "Cache is empty." << std::endl;
        }
    }
};

int main() {
    Cache myCache;
    myCache.store(100);
    myCache.clear();
    myCache.printValue(); // This is where the UB happens
    return 0;
}

Requirements:

  • Modify the clear() method to ensure that printValue() can safely determine if the cache is empty.
  • Ensure that calling clear() multiple times in a row doesn't cause a double-free error (which is also UB).
  • Verify that the output of the program after your fix is "Cache is empty."
Rating
0 0

There are no comments for now.

to be the first to leave a comment.