What's the difference between debug and release builds?

This is a very common question that arises on the newsgroups and a lot of people don't fully understand the differences between debug and release builds, so it's sometimes wrongly answered. The differences fall into several camps, any one of which can kill you. So, presented in the order of most likely problem cause -> least likely, these are the major differences:

Heap Allocator

The debug heap allocator puts guard bytes around the memory for objects that you allocate on the heap (e.g. using "new"). If you write beyond the end of an object - a common error in C/C++ when people forget that arrays are indexed from zero and get their indexes off by one - then you will probably get away with it in debug mode, but in release mode you'll definitely cause corruption, and probably a crash sometime later.... just far enough later to completely confuse you as to the cause.

Whilst we're on the subject of array overwrites, a dead giveaway for a local (stack) variable overwrite, by the way, is the function that crashes when it returns. This happens because someone wrote past the end of an automatic-declared array and corrupted the stack frame. OK, end of digression.....

ASSERT Behaviour

Many C programming books tell you to "ASSERT the world". You typically put ASSERT statements in to sanity-check your code, such as checking that parameters have sensible values, and that handles are not null. Unfortunately, folks sometimes put code in an ASSERT that they shouldn't - code which is FUNCTIONAL in nature, i.e. active code for their app. To take an example:

ASSERT (OpenMyWindow () != NULL);

instead of

hWND = OpenMyWindow();
ASSERT (hWND != NULL);

Code in an ASSERT is omitted in release builds, so if you wrote the first example, your window would open in debug mode, but not in release mode. This would be fairly obvious, but of course real-world instances of this bug tend to be much more subtle....

If you want to use this checking mechanism for functional code, use the VERIFY macro instead. 

Debug Padding

Debug mode pads your code out with large amounts of its own stuff (compare the size of a debug EXE with the corresponding release EXE). If you have a wild pointer which scribbles somewhere it shouldn't, there's a chance that in debug mode it will scribble on something which does no harm. But you're unlikely to be so lucky in release mode.

Initialization

The debug version of the memory allocator will initialise memory allocated from the heap (e.g. using "new") with the magic value 0xCD. The release version of the allocator does not do this - initial contents are undefined.

NOTE: the guard bytes I mentioned above in the section on the heap allocator are initialised to 0xFD, and freed memory is written with the value 0xDD by the debug version of the deallocator.

People frequently have the impression that local variables are initialised in debug builds : not so. You have the option to introduce this behaviour for debug builds yourself, by using the /GZ compiler switch, which will initialise local variables with 0xCC bytes (if you initialise the variable explicitly, your value of course overwrites the 0xCC). But the /GZ option is not on by default for debug builds in VC++. /GZ's primary purpose is to catch uninitialised pointer errors, since the pointer will point to 0xCCCCCCCC when you use it.

Prototypes

If you use MFC ON_MESSAGE macros, then the function prototypes MUST match a particular style, typically something like :

afx_msg LRESULT <class>::OnMyMessage (WPARAM wParam, LPARAM lParam);

In debug mode, if your prototype doesn't match the required format, the compiler will silently fix it and the code will work. The release mode compiler WON'T fix it and your code can (probably will) go wrong. Wrong in new and interesting ways :-).

Optimization

Most people will switch the optimizer on for release builds. But not all optimizations are safe, and this may catch you out if your code does anything unusual. Remember that you can always selectively turn optimizing off for a region of code using #pragma directives. You are unlikely ever to encounter this unless you specifically use aggressive optimisations (I always use "minimize size", since it gets you all the safe speed optimisations anyway).

Further Reading

The last resort in this kind of scenario is to run the release version of your code under the debugger. This IS possible, though the results are often hard to interpret, and of course your code may fail in the field in circumstances which you just can't repro back at your desk. But if this idea floats your boat, the full skinny on how to run release code under the debugger can be found here