Few programming languages can match C for sheer speed and machine-level power. This statement was true 50 years ago, and it’s still true today. However, there’s a reason programmers coined the term “footgun” to describe C’s kind of power. If you’re not careful, C can blow off your toes—or someone else’s.
Here are four of the most common mistakes you can make with C, and five steps you can take to prevent them.
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
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
How 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
If 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
Many 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.
Be 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
Where the compilers themselves fall short, other tools step in to fill the gap—especially when it comes to analyzing program behavior at runtime.
Cppcheck runs static analysis on C source code to look for common mistakes in memory management and undefined behaviors (among other things).
Valgrind 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.
These 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
Since 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
Some 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 choice.
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 option handling.