Why and how I use C
This article is meant to address the constant C-nile accusations I've gotten for using C as my primary language. It's a little annoying, so I've decided to talk about it here.
Reasons why I use it:
- Well, I'm a simple man who just likes simple things, and C is a small and simple language.
- C is mature, with a massive ecosystem and a well-established standard that rarely changes.
- As a systems programmer, it enables direct access to key OS functionalities through low-level APIs.
- Well-written C code can be compiled and run on virtually any platform, making it highly portable across systems.
- C produces fast and efficient machine code with minimal runtime overhead.
- From the programmer's perspective, it provides a clearer and more direct interpretation of the machine, such as instruction activity and execution.
How I use C
C is known as an unsafe language due to its lack of built-in safety features such as bounds checking, type safety, and automatic resource management. This has led to the famous saying, "it is easy to shoot yourself in the foot."
Well, since most of C's unsafety relates to memory issues and undefined behavior (UB), what if I told you that you could literally just write safe C code and it's simple? "You can just do things," they said. C is as unsafe as any other language because 100% safety doesn't really exist. "Even Rust?" Yes, even Rust.
This is not a pro-C evangelism article, so I won't lie. Achieving safety in C is a bit tricky, especially when working with teams and on really large code bases, but that doesn't cancel out the fact that it is entirely possible to do so. So here are some tips on how I write safe C.
Static Allocations
One way I improve safety in my C programs is by relying on static allocations. I don't even remember the last time I dynamically allocated memory for anything. With static allocations, data structures and variables are fixed-sized, therefore determined at compile time and reserved in global or stack memory. The lifetimes of such variables are fixed; they exist for either the duration of the program (static/global variables) or the function (stack variables).
This approach is convenient because variables have predictable lifetimes, and I get to avoid any potential dynamic allocation issues such as memory leaks, double frees, or use-after-free errors. "You can just not do things."
Also, since all storage is pre-allocated, there's no chance of heap fragmentation or out-of-memory errors during runtime.