The awesome power of C cuts both ways. Here’s what to watch out for, and how to keep your C programs on the straight and narrow.
Common C mistake: Not freeing
malloc-ed memory (or freeing it more than once)
This is one of the big mistakes in C, many of which involve memory management. Allocated memory (done using the
malloc function) isn’t automatically disposed of in C. It’s the programmer’s job to dispose of that memory when it’s no longer used. Fail to free up repeated memory requests, and you will end up with a memory leak. Try to use a region of memory that’s already been freed, and your program will crash—or, worse, will limp along and become vulnerable to an attack using that mechanism.Note that a memory leak should only describe situations where memory is supposed to be freed, but isn’t. If a program keeps allocating memory because the memory is actually needed and used for work, then its use of memory may be inefficient, but strictly speaking it’s not leakage.
Common C mistake: Reading an array out of bounds
Here we have yet another of the most common and dangerous mistakes in C. A read past the end of an array can return garbage data. A write past an array’s boundaries might corrupt the program’s state, or crash it completely, or, worst of all, become an attack vector for malware.
So why is the burden of checking an array’s bounds left to the programmer? In the official C specification, reading or writing an array beyond its boundaries is “undefined behavior,” meaning the spec has no say in what is supposed to happen. The compiler isn’t even required to complain about it.
C has long favored giving power to the programmer even at their own risk. An out-of-bounds read or write typically isn’t trapped by the compiler, unless you specifically enable compiler options to guard against it. What’s more, it may well be possible to exceed the boundary of an array at runtime in a way that even a compiler check can’t guard against.
Common C mistake: Not checking the results of
calloc (for pre-zeroed memory) are the C library functions that obtain heap-allocated memory from the system. If they aren’t able to allocate memory, they generate an error. Back in the days when computers had relatively little memory, there was a fair chance a call to
malloc might not be successful.Even though computers today have gigabytes of RAM to throw around, there’s still always the chance
malloc could fail, especially under high memory pressure or when allocating big slabs of memory at once. This is especially true for C programs that “slab-allocate” a large block of memory from the OS first and then divide it for their own use. If that first allocation fails because it’s too big, you may be able to trap that refusal, scale down the allocation, and tune the program’s memory usage heuristics accordingly. But if the memory allocation fails untrapped, the entire program could go belly-up.
Common C mistake: Using
void* for generic pointers to memory
void* to point to memory is an old habit—and a bad one. Pointers to memory should always be
unsigned char*, or
uintptr_t*. Modern C compiler suites should provide
uintptr_t as part of
stdint.h. When labeled in one of these ways, it’s clear that the pointer is referring to a memory location in the abstract instead of to some undefined object type. This is doubly important if you’re performing pointer math. With
uintptr_t* and the like, the size element being pointed to, and how it will be used, are unambiguous. With
, not so much.
Avoiding common C mistakes — 5 tips
do you avoid these all-too-common mistakes when working with memory,
arrays, and pointers in C? Keep these five tips in mind.
Structure C programs so that ownership for memory is kept clear
you’re just starting a C app, it’s worth thinking about the way memory
is allocated and released as one of the organizational tenets for the
program. If it’s unclear where a given memory allocation is freed or
under what circumstances, you’re asking for trouble. Make the extra
effort to make memory ownership as clear as possible. You’ll be doing
yourself (and future developers) a favor.
This is the philosophy behind languages like Rust.
Rust makes it impossible to write a program that compiles properly
unless you clearly express how memory is owned and transferred. C has no
such restrictions, but it’s wise to adopt that philosophy as a guiding
light whenever possible.
Use C compiler options that guard against memory issues
of the problems described in the first half of this article can be
flagged by using strict compiler options. Recent editions of gcc, for instance, provide tools like AddressSanitizer (“ASAN”) as a compilation option to check against common memory management mistakes.
warned, these tools don’t catch absolutely everything. They’re
guardrails; they don’t grab the steering wheel if you go off-road. Also,
some of these tools, like ASAN, impose compilation and runtime costs,
so should be avoided in release builds.
Use Cppcheck or Valgrind to analyze C code for memory leaks
the compilers themselves fall short, other tools step in to fill the
gap—especially when it comes to analyzing program behavior at runtime.
runs static analysis on C source code to look for common mistakes in
memory management and undefined behaviors (among other things).
provides a cache of tools to detect memory and thread errors in running
C programs. This is far more powerful than using compile-time analysis,
since you can derive information about the program’s behavior when it’s
actually live. The downside is that the program runs at a fraction of
its normal speed. But this is generally fine for testing.
tools aren’t silver bullets and they won’t catch everything. But they
work as part of a general defensive strategy against memory
mismanagement in C.
Automate C memory management with a garbage collector
memory errors are a conspicuous source of C problems, here’s one easy
solution: Don’t manage memory in C manually. Use a garbage collector.
Yes, this is possible in C. You can use something like the Boehm-Demers-Weiser garbage collector
to add automatic memory management to C programs. For some programs,
using the Boehm collector can even speed things up. It can even be used
as a leak-detection mechanism.
The chief downside of the Boehm garbage collector is that it cannot scan or free memory that uses the default malloc. It uses its own allocation function, and it only works on memory you allocate specifically with it.
Don’t use C when another language will do
people write in C because they genuinely enjoy it and find it fruitful.
On the whole, though, it’s best to use C only when you must, and then
only sparingly, for the few situations where it really is the ideal
If you have a project where execution
performance will be constrained mainly by I/O or disk access, writing it
in C is not likely to make it faster in the ways that matter, and will
probably only make it more error-prone and difficult to maintain. The
same program could well be written in Go or Python.
Another approach is to use C only for the truly performance-intensive parts
of the app, and a more reliable albeit slower language for other parts.
Again, Python can be used to wrap C libraries or custom C code, making
it a good choice for the more boilerplate components like command-line