The C Language: A Review

ยท

9 min read

Introduction

I shamed myself into learning a "real" programming language. By luck, the YouTube algorithm blessed me with videos of notable programmers like John Carmack and Jonathan Blow trash talking JavaScript. As a self-taught developer who spent a lot of time with JavaScript, I was hurt but confessed they had valid points. Thus, they drove me to learn a lower level programming language and I settled with C.

Other than shame, I learnt C to:

  • Improve as a programmer due to C's difficult learning curve.

  • Gain an edge in standing out as a software developer applicant in the current brutal tech job market (my unproven theory).

  • Better understand what goes on under the hood when using higher level languages like JavaScript.

  • Challenge myself to push my limits and escape my comfort zone.

Looking online for C learning resources, I settled with Zed Shaw's Learn C The Hard Way course as my main curriculum for two reasons: 1) the author's track record as an exceptional programmer and 2) the fair $30 USD price tag. Although cheaper and free resources for learning C exist, I wanted buy-in from myself to fully commit.

I finished ~92% of the course over 51 days full-time with secondary resources (e.g., YouTube videos, books, ChatGPT, etc.) to aid my learning. Delighted with how much I learned in this short time by this point, I stopped here. This post summarizes the key takeaways I got given my JavaScript background.

Disclaimer: I am minimally competent at C and not a C expert, so there may be imprecise or wrong things said in this post.

What I Learned

The following details the best of what I newly learned or improved my understanding of.

Repetition

Repeatedly hand-typing the C syntax rather than copy-pasting code or using IDE-generated code. When learning something new, this is vital to become fluent and internalize the basics. In hindsight, it's obvious but often neglected by beginners. They try to shortcut to advanced topics to "save" time but really waste more time long term.

Compilation And Makefile Build Configs

As someone who rarely works with compiled languages, this was a good refresher. I found C's source code -> compile -> binary file build pipeline interesting in contrast with the higher level, interpreted languages (JavaScript, Ruby, Python, etc.) that I am used to working with. I loved how fast it was to build, test, and run C programs compared to in JavaScript where it is much slower.

C Arrays And Strings

In C, both char arrays and strings are treated the same. Note that is specifically for char arrays; C does not treat non-char arrays like strings; all char arrays are like strings but not all arrays are like strings.

Before C, I always thought arrays and strings were oddly similar and now I knew for sure. What I didn't know was that C strings are broken due to their null-byte terminated nature; there's no way to reliably check if a string is valid by itself. Thus, they can cause a whole slew of bugs and security flaws if used without an explicitly bound max length.

C Pointers

The concept of pointers was a topic I feared for a long time because of its infamous learning difficulty reported by many CS students online. However, the basic concept is very simple. Pointers can be thought of as the key of a hash map's key-value pair, the planted arrow sign that points to a building, or as the barcode sticker label on a physical product's box.

Any variable (including pointers) can be modeled as a "box" with two things: 1) a memory address (like a sticker label on the box's outside) and its associated value (like a physical object inside the box). The address is a big integer that the computer uses to find the variable wherever the variable is referenced in the code. The value is an instance of whatever data type the variable was declared e.g., a char value "a", an int value "-1", etc. In brief, a pointer is simply a variable that stores another variable's memory address.

Pointers are good for saving memory especially when you have a function that must work on some big and complex variable argument; rather that copy the whole variable in memory, you can use a pointer to the variable which lets you access and update the original variable directly.

Related to the last bullet point, both arrays and strings in C use pointers under the hood; they are syntactic sugar for pointers that make it easier to reason with.

C void Pointers

My first reaction to void pointer types in C was connecting them to the any type in TypeScript. In TypeScript, a variable declared with the any type can be assigned a value of any primitive or non-primitive type as its name suggests. I concluded that this was the equivalent of and possibly inspired by C's void * pointer type.

Manually Managing Memory

I learned to manual memory management with built-in functions malloc(), calloc(), and free(). Having only used auto garbage collected languages, doing this was both punishing and enlightening. It added more memory checking, tear down, and exception handling lines to my code. I became very grateful for all the programmers who designed and wrote memory garbage collectors for most modern languages ๐Ÿ˜‚.

Stack Versus Heap Memory Allocation

Before C, I had a vague idea but did not understand this concept well. Having learnt C, it's now easy. Both the stack and heap store defined things in code (e.g., variables, functions, etc.) in the computer's physical memory (i.e. RAM).

The other basic ideas are that 1) statically given memory goes on the stack and that 2) dynamically given memory goes on the heap. Static means the amount of memory given for something in code that does not change over time, while dynamic means the amount may change over time. Normally, variables that call malloc() or calloc() (to dynamically allocate memory) are on the heap while variables that don't are on the stack.

There were some other notable things about stacks and heaps:

  • Stack memory space is more limited than heap memory, so creating memory-expensive variables is better done on the heap. This can help avoid overloading the stack (i.e. stack overflow) which will crash the program.

  • Due the heap's dynamic nature, the heap should store any complex data structure variable whose size may change over the C program's life.

  • Pointers typically point to variables stored on the heap.

goto And Labels

Upon exposure, I realized the goto and error label pair could be the try-catch block's precursor. In modern programming languages, try-catch blocks handle exceptions.

Using A Debugger

Before picking up C, I'd always resort to printing out values or writing tests to help me debug code. With C's obscure error messages, undefined behaviour, and infamous segmentation faults, my previous tactics didn't always cut it. To fix my broken C programs, I spent time stepping through them line-by-line with gdb, a CLI debugger. This added a new valuable tool added to my skill set.

C Macros

This blew my mind the first time I encountered this. Macros allow creating constants and functions that 1) can be referenced and called like normal variables and function and 2) are replaced with the real code they define -- i.e. strings or numbers for macro constants and function bodies for macro functions -- during compilation.

Basically, macros generate code. Macro functions are like normal functions can both take parameters of any type and return values of any type -- without having to explicitly define those types in the function parameter list or body, similar to normal functions in JavaScript.

Automated Testing

Although I wrote tests in other languages before, it was still interesting to learn another way to do it in C. The C course went over how to write automated tests for all sorts of C programs using a set composed of a Bash script, a custom C unit test library, and a Makefile.

Creative And Defensive Programming

While not specific to C, the concept of creative and defensive programming from Zed's C course changed my views on the programming process and writing better programs. In a nutshell, this concept means to quickly hack your way to a minimally working program piece by piece before ever refining and better securing the code.

Prior to learning this, my process as a perfectionist meant trying to get the code right, well designed with best practices, and all that jazz on the first write. Unsurprisingly, this method wasted much time and effort; I was prematurely overthinking code design and second-guessing myself constantly, trapped by fear and uncertainty.

Since learning this mental model, I became bolder to write scrappy and borderline horrible working code before cleaning up for any new software projects I work on.

Network Programming

I never knew how hard it was to write a network server program until I did it in C.

Before this, the closest thing I did was write back-end web applications that ran on top of server programs. Frameworks like Express.js and Ruby on Rails spoiled me with easy developer experience and hid this pain from me. At that time, I had a vague idea of IP addresses, ports, and sockets but it was fairly muddy.

After 5 days of suffering, I managed to get my duct-taped Linux socket server running on emulated Ubuntu (in WSL1 on Windows 10). The server program is a DIY challenge in Zed's C course that I did with minimal reliance on his solution video and code; I mostly read the Linux man pages for the functions in socket-related C libraries and relied on YouTube videos, ChatGPT, and a book or two on Linux socket programming to figure it out. By far the most challenging parts of this project were 1) understanding the bigger picture of how all these socket functions worked together and 2) finding relevant code examples for using the functions, custom types, and constant values that either weren't in the man pages or hard to find.

Conclusion

Learning C at times was both very hard and almost had me quit, but it was fully worth it. I had several "Aha!" moments that completely changed the way I thought about and approached certain problems in programming. To anyone who's learnt at least one language's basics well enough (e.g. ~6 months to a year), C is the language that will take you to the next level. If interested, I highly recommend paying for and taking Zed's Learn C The Hard Way course.

Disclaimer: Yes, I am biased in recommending Zed's C course having bought and taken it. However, Zed is not compensating me for my recommendation nor have I asked him for any.

ย