I wanted a new challenge.
As an experienced programmer, the basics of the language were unsurprising (as expected). Variables, conditionals, and loops presented no surprises as I've seen these concepts time and time again in other languages. I will admit that understanding pointers was initially as confusing as when I first encountered them more than 15 years ago. This confusion reinforced the fact that C is not a language for the faint of heart -- there are a number of challenges that a programmer has to overcome when working in a lower-level language.
These challenges weren't without their benefits. During our course of study, we became proficient with a number of language features and related tools:
- Memory allocation — Once I moved past writing trivial programs, heap allocation became a necessity. Not only did I learn how to use malloc and free appropriately, but also when to use realloc to create dynamic data structures and calloc to create robust allocations for storing string data.
- Makefiles — Zed's introduction to make and subsequent discussion of its advanced features really helped to de-mystify the tool for me. The additional documentation and alternate applications helped me understand its utility for building C programs and other automated tasks.
- Function pointers — It was interesting to me to see that such a low-level language contained functional programming concepts. I found function pointers a useful refactoring technique when dealing with different collections of structs. For example, when sorting an array of integers or strings, I was able to use functions with the same signature and behavior but with different implementations.
- C pre-processor — From the beginning, Zed made heavy use of the C pre-processor to create macros for debugging, iterating, and otherwise simplifying the code that I needed to write. After gaining more experience with the language, I learned when to choose a pre-processor macro over a subroutine (or vice versa) to refactor and improve readability of the code.
- Variable argument functions — Surprisingly, this language feature came in handy more often than I thought. Internally, this is how the printf family of functions is implemented. I used it to create functions like max_length(char *s1, char *s2, ...) to get the overall maximum length of n number of strings, and array_push(Array *array, ...)` to add elements to an array based on its underlying storage type.
- Valgrind — I have no idea how you would create a stable program without this tool. Time and time again, I would write what I thought was robust code, but valgrind would call me out on my mistakes. Whether it was a simple memory leak or an off-by-one error when accessing a segment of memory, I was able to quickly diagnose those mistakes and correct them. Without valgrind, these errors could easily slip by undetected.
Overall, the challenges presented with C really forced me to think deeply about how I organized my code and interacted with the machine. Understanding that balance between bare-metal performance and human understandability definitely revealed the language's sweet spot. Even in an industry where older technologies are constantly rendered obsolete, that balance is the reason developers of major modern software projects continue to choose C for their implementation language.
If you're interested in (re-)learning the language yourself, I recommend LCTHW as a good starting resource. Working through the exercises as a group helps keep everyone motivated and generates some fun ideas for other programs to write.