Ask HN: How do I learn C properly?

432 points by buchanae ↗ HN
I have 15+ years of experience in many languages: javascript, python, go, etc.

I can write C, but I'm not confident I'm doing it the right way. There are now decades of forums and blogs I can search through, but I'd love to have a single, clean, clear source of truth for recommendations on how to write C code.

I've started reading "Modern C" by Jens Gustedt. I'm not sure if this is the closest to a modern source of truth.

Note, I'd like my C code to work on Windows, which possibly restricts the "right way" to features provided by C89. But again, I'm not sure, the internet is super clear on this.

Thanks for the tips!

207 comments

[ 3.6 ms ] story [ 247 ms ] thread
Perhaps better than a book, can anyone authoritatively point to a few small-and-readable but best-in-class open-source C projects to use as a reference?
Lua and Sqlite sources are excellent.
Linux as well. The comment culture of C devs really help with understanding all of it.
There was a book featured in a HN post about a year ago - an older Linux source with extra commentary, used by students in China, I think (book is in English)
* tweetnacl.cr.yp.to

Crypto library in the size of a hundred tweets [ https://twitter.com/tweetnacl ] by the only person with a genuine claim to being able to write safe C & company.

* https://github.com/mit-pdos/xv6-public

&

* https://github.com/mit-pdos/xv6-riscv

UNIX v6 clone in ANSI C by influential Plan 9-era Bell Laboratories employee and now influential Google employee Russ Cox, along with influential computer virus author and son of one of the original UNIX authors Robert T. Morrison; entire source code fits in under a hundred pages of well-typeset documents [ warning, old copy, you should generate a modern one: https://pdos.csail.mit.edu/6.828/2011/xv6/xv6-rev6.pdf ].

not recent projects but everything that DJB implemented is absolutely elegant, low on bugs and great as a lesson for how to write secure C. e.g. qmail, djbdns, daemontools, there is a lot of ideas there you can learn from: https://cr.yp.to/

It also helps to build up (and refactor) your toolset over time, memory handling wrappers, logging, I/O, daemonize (https://github.com/jirihnidek/daemon) etc, so that you don't have to keep reinventing the wheel.

if I'd recommend one book then it's: https://en.wikipedia.org/wiki/Advanced_Programming_in_the_Un...

I would read K&R, it’s a right of passage. Look at some of the larger open source projects for how to organize and manage a large c project.

And practice pointers. Get really comfortable with how memory works.

while not a definitive guide, https://matt.sh/howto-c is a good example of how to approach "modern" c.

c99 is fully implemented in windows with msvc (2015), gcc, clang, and intel's compiler, so "right way" should not need to involve c89. most are c11 compliant as well.

Pretty obvious answer, but for those who are new to C, the C Programming Language by Brian Kernighan and Dennis Ritchie is a good place to start
Best paired with a bunch of modern resources about how undefined behavior works.
I would recommend "Learn C the Hard Way" by Zed Shaw to get you started for something practical, because it goes over how C is written in the real world. Of course, situations will vary. The original book about C by K&R is an excellent book too.
Zed makes it clear in that book that he hates C and doesn't use it anymore due to its undefined behaviour.
Zed isn't actually very good at understanding undefined behavior and has demonstrated this loudly and repeatedly. I would steer clear of his book, personally.
> Zed makes it clear in that book that he hates C and doesn't use it anymore due to its undefined behaviour.

Really? Which part in the book specifically if you can cite the passage.

Learn C the Hard Way is considered by the community to be, not exaggerating, one of the worst C books ever written.
I'd just type in the examples from K+R, try them out, and modify them to do new things.
Do this but get the answer book too, because it's easy to get stuck in here.
K+R is pretty ancient.
That doesn't make it worthless; it's still probably the best book to learn C today if supplemented correctly.
Becoming proficient in a language usually involves: - writing code with it long enough, - collaborate in a project based on it, - reading code other people wrote, - keep yourself up to date by following the language evolution, - go more in depth in language internals, compilers, etc...

I wrote C for 10+ years (mostly bare metal FW), yet I am still amazed of how little I know about it. Recently for example I learnt of all the things the dynamic linker does in Linux, how symbols memory addresses are resolved at runtime using PLT, ....

The good point about C is that it can be used for very different kind of projects, so the choices are a lot.

The C FAQ used to be a good place to start. Note, used to be:

http://c-faq.com/

What did you want to do on Windows? I think this depends as much on the compiler as the code.

Second this. Read it all more than once; the insights it will give you into C are so valuable.

I'll also recommend C++ FQAs [1]. It's C++, yeah, but it's relevant to C in many places, and since a lot of systems programming now uses C++ as the lingua franca, it's a good, entertaining resource.

1: http://yosefk.com/c++fqa/

I'd say there's no proper way. Every project has its way of writing C. With the use of macros, sometimes it looks a bit like a DSL for every specific project.

My advice: think about what kind of software you want to write and look for similar projects, libraries, or contribute with a new module, functionality, adapt something to your needs...

I would advice AVOID C macros at all cost. They are so weak and hard to debug and problematic.

I write in several languages and read lots of C projects made by others and find the affirmation hard to believe: C is so simple that is very easy to read if the writer of code has a minimum of competency.

You can say that of projects like C++, specially things like C++11 that are languages by committee so complex that you can affirm that every C++ programmer is different or find programmers that could not understand each coder's code. Not so with C.

That happens to me with C++ and lisp code. It takes a while for me to understand what the author uses(or abuses) before being able to understand the code. That does not happen with C code.

The Linux kernel uses lots of macros, many of them with lowercase names that are barely distinguishable from function names. Given the quality of that codebase, I think not using macros is a bit silly.
Besides all that is mentioned already to read or do. I would suggest to try writing something you coded in python or go previously in C.
C is a very simple language. I have used just the official old book to create a compiler for it:

https://www.amazon.com/Programming-Language-2nd-Brian-Kernig...

The best thing you can do is finding some open source code that interest you, read it and write it.

For example, I write my own controllers for CNCs, and there are lots of code for controlling stepper motors out there. I also learned OpenGL and sockets this way long time ago.

On the other hand, C is a terrible language for any big project unless you automate it some way.

I use mostly lisp in order to write C code automatically. C is too low level for writing any substantial code. Lisp has things like Macros(nothing to do with c macros) that can make you write code 10x, to 50x faster easily.

> The best thing you can do is finding some open source code that interest you, read it and write it.

Has reading source code in a language that’s unfamiliar to you shown to be of any real benefit to learning? It seems you need at least a little bit of foundational experience with it before your brain can even parse what you’re seeing in a beneficial way.

I'd tend to agree here also. I didn't start reading source code until a couple years in because I couldn't really put it to use. It becomes 10x more valuable when I was deep into my career learning from the big shots.

It _can_ help no doubt but not sure that's the best way to start. Especially with a language like C. It's not so straight forward without some training or CS education.

> It becomes 10x more valuable when I was deep into my career learning from the big shots.

Yeah well OP said they've 15+ years of experience so..

> Especially with a language like C. It's not so straight forward without some training or CS education.

I agree with pritovido, when they say C is a very simple language. Sure, it has grown its quirks and gotchas along with some cruft (the kind of things you wouldn't learn about in a CS curriculum anyway), but at its core it really is very simple.

> Has reading source code in a language that’s unfamiliar to you shown to be of any real benefit to learning?

Absolutely! You start finding idiomatic patterns and "oh, so that's how they do it" kind of things. Find library functions you never knew about but now you do. Find weird things, and look them up in a manual/reference/SO/chatroom. Things that you might find in a book, but a book that covers all the idioms and practice and weird things is gonna be as thick as the bible, and it'll get outdated (it doesn't help that most books are focused on teaching the basics rather than showing off how to architect your application well). Things that you can learn the hard way by why not look and see and learn?

> It seems you need at least a little bit of foundational experience with it before your brain can even parse what you’re seeing in a beneficial way.

Nah, you just need programming experience (in a similar paradigm) in general. A lot of what you learned from languages before will translate.

Also OP mentioned that "I can write C" and "I've started reading 'Modern C'", so it's not like they're looking for their first Hello World snippet.

OP said they've got 15+ years of programming experience. At that point, picking up a new language is all about learning the vocabulary and idioms, plus the few unique or tricky or quirky things that don't show up in other languages (or that aren't obvious from looking at code). The fastest way to get to that vocabulary is to look at real code.

>Has reading source code in a language that’s unfamiliar to you shown to be of any real benefit to learning?

Yes, I think so. The parent has experience with other languages, like python, javascript and go. C has a very similar paradigm. If it were clojure it would be different.

I find exposition to new languages real usage the best way to learn them. I went to the UK to learn english, Germany to learn German, could be painful at first, but works, specially if you know a little about what is all about.

Your brain makes sense of everything by itself in a magical way. That is the way you learn your native language.

For me there's almost no wisdom to assimilate when reading source code until I've given a fair shot at using the language and experiencing its obstacles. Then seeing other people overcome them is where almost all of the a-ha moments are.
(comment deleted)
You use Lisp to write C (on Microcontrollers)? Would appreciate more info on that.
Not the OP, but as I understand it NASA's JPL has been doing this for decades for space missions.
That a horrific mistake that MIT nitwhits used to teach students at the end of 6.001 - how to write opaque, horrible lisp programs in languages such as "C".
> C is too low level for writing any substantial code.

Do people not realize that projects like Linux and pretty much every car's ECU software (pretty "substantial" in my opinion) is written in C before claiming this? I'm not saying these software are 100% bug free, but this claim isn't accurate either. You _can_ write substantial C code when you have a good understanding of the tooling around it. Mere learning the syntax isn't enough.

I'm so tired of hearing about what you can't do in C and the "only thing" it's good for. You can't write big systems. It's only for embedded.

It's such a crackup, because the number of developers who haven't got the memo on this is really staggering. How many lines of code is GTK? Or Postgres? Or Nginx? Never mind languages and operating systems.

I suppose Vim (~335,000 lines) or git (in the 200,000 range) is not "large" in comparison to some things, but I suspect that's actually the kind of number people think is "too big for C."

People also seem not to realize that the reason that Python library is so fast, is because it's actually wrapping C code. And that code is very often not doing some frighteningly low-level thing, but just doing ordinary stuff.

The "steel man" version of this argument is that, if you're embarking on a new codebase which you have good reason to expect will grow to be large, have a very good reason to write it in C, or use a more appropriate language.

There some reasons to do it, but fewer than there were when most of the projects you've name-checked were started.

I'll skip the part where we review what the other options are, this is HN, we've all seen that thread before.

Knowing how some big software systems lived through more than a decade each, I'd claim that the less they used C++ and the more used C++ compilers to write a C-like code they were better. The worst parts of the project were exactly those that used the "cool" C++ features.

C is much better for really long-lived projects than C++ is. C++ is a maintenance nightmare in comparison.

Linux kernel is also a good and famous big long-lived project which I claim would have never even survived had it been written in C++. Linus seems to have believed the same.

> C++ is a maintenance nightmare in comparison.

c'mon, all the super large projects - adobe software, ableton live, blender, autodesk, 3DS max, maya... all the major game engines (UE, CryEngine, Frostbite, etc), are written in C++, not in C. LLVM & clang are C++, and GCC is all C++ in new code with a fair amount of old code ported from C. All the major web browsers are C++ - and I'd wager most minor too.

If what you said was true we'd see much more massive projects in C and much less in C++ yet here we are.

I think you might be missing the main point of what this person is saying, which is:

"I'd claim that the less they used C++ and the more used C++ compilers to write a C-like code"

And I bet that's true. I doubt there's layers and layers of template metaprogramming in Ableton Live, or that Maya is a study in inheritance. Game engines, certainly, are C++ written as so-called "better C."

"Yet here we are" -- a place where people reinvent C and call it "data-oriented programming." /s

> I doubt there's layers and layers of template metaprogramming in Ableton Live, or that Maya is a study in inheritance

I doubt both of your assertions - public Ableton code uses templates fairly liberally ( https://github.com/Ableton/link/search?q=template&unscoped_q...) - and due to backtrace exposure I know that this is also the case for private implementation things.

For Maya just look at the plug-in API :

https://help.autodesk.com/view/MAYAUL/2017/ENU/?guid=__files...

https://help.autodesk.com/view/MAYAUL/2017/ENU/?guid=__files...

https://help.autodesk.com/view/MAYAUL/2017/ENU/?guid=__cpp_r...

command pattern, factories, etc... it's textbook OOP.

Also, most projects in the wild nowadays are C++11 or later, with actual use of C++11 language features. e.g. look for example OpenAGE, an AOE2 reimplementation - https://github.com/SFTtech/openage/blob/master/libopenage/jo...

That kind of thing is as far as "C with classes" as is possible.

> most projects in the wild nowadays are C++11 or later

Which means either that their codebase is less than a decade old, or there were serious rewrites involved. Which fits my initial statement: historically, long term (T > 10 years), successful projects should have better used C++ as C-like as possible (or just used C).

I'm not claiming it's not possible to have successful projects using C++ lasting long, just that it would be, long-term (really long term), less overhead to use C for big projects.

Êdit: My personal bias: I was involved in two long-lasting projects, where just dealing with boost dependencies used up more maintenance time than it would have been needed for experienced developers to develop the code which didn't depend on boost. But of course, those who used boost didn't have such an experience to make such calls, and in initial development "look how fast we have a feature x" wins.

I personally consider Google's C++ style guide limiting Boost use as one of the best examples of some influence of some experienced developers on the policies.

> or there were serious rewrites involved

why do you assume that ? if someone was using, say, "Modern C++ Design: Generic Programming and Design Patterns Applied" from Alexandrescu (released in 2001!) as a guideline when writing one's codebase, "migration" to C++11 is maybe replacing some custom types by std ones for stuff like mutexes and threads, etc. but all the architecture would stay pretty similar.

> Which fits my initial statement

I really don't see how - all those projects I listed are successful, you seem to be saying that it is despite of C++ but I don't see any argument towards that. I personally don't know anyone who would willingly go back to writing large-scale projects in C after working in C++.

I take your point about the age of some of these projects (though it's not like there weren't any "more appropriate" languages around when most of them started). And I'll also admit to agreeing with the idea that choice of language is nearly always a sociological matter before it's a technical one.

But I'm not really arguing that C is the best choice for large projects. I'm just pointing out the fact that many, many people just completely ignore that advice. They are ignoring that advice today -- right now. With new projects. And they've "seen that thread before" too. It's not just legacy, or lack of awareness, or whatever.

Maybe it comes down to what the "very good" reason actually is. Because given the amount of C hacking going on, I wonder if the "good reasons" we hear about nearly every day on HN aren't the most important ones for the quite vast number of people merrily hacking away in a language that is now decades old.

As the CVE database proves, just because one can do it, doesn't mean one should do it.

Liability can't come soon enough.

As someone who works on a significant C codebase (IOS-XR), my 2 cents:

1. Most of the large userspace C codebases that still exist today didn’t have many alternatives when they were started.

2. Any non-trivial C codebase relies much more on runtime testing than other languages. In other words, with other languages, you can get away with fewer tests while achieving the same level of quality.

Note that you should be striving for comprehensive tests regardless of language; this is just an observation.

That, and I dislike the low-level vs high-level differentiation. It is relative, and more of a spectrum, IMO, and not that it is particularly useful anyway.
(comment deleted)
I'd check out Zed Shaw's book [0]. It's opinionated, but I think that's a good thing for something like C.

It's broken up into exercises that you start working through straight away, and you start early with valgrind.

[0] https://learncodethehardway.org/c/

The author of that book doesn't really like C, and it shows. And valgrind is usually not the first tool you should be using to debug memory issues–try Address Sanitizer instead.
Like anything else, set yourself a goal for a project. What kinds? C today is typically used for developing embedded systems, kernel internals, or device drivers.

Depending on how deep you want to go. The most in depth way to understand C is to learn assembly on an ISA first.

First step is making sure you're using the flags

`-Wall -Werror -pedantic` and then one of `-std=c89` or `-std=c99`

(Or equivalent in whatever Windows C compiler)

Why would you suggest older versions?
In my opinion, one of the main reasons C looks old and not practical to beginners is the standard libs API.

But it is perfectly possible to use C as a language with a more modern and easier to read API, but you'll probably have to build your own.

In addition to the other recommendations, make sure you fully understand the memory model of your target architecture, the stack, the heap, the linkage.

Also, understand the undefined behavior, the compiler optimizations and how it can affect your code if you forget about the UB or if you ignore the compiler warnings, and read about the `volatile` keyword.

And a personal tip, please tell the compiler not to use strict aliasing (I think it's stupid) - you will get rid of some UB without much sacrifice.

Just to be clear, heap and stack are not part of C.
Then why does it make a difference whether you dereference a pointer to the stack or the heap at any point in your program?

The hardware representation of an int isn't part of C either but it most certainly had an effect on how your program will run.

> Then why does it make a difference whether you dereference a pointer to the stack or the heap at any point in your program?

Ok, I'll bite. What difference does it make?

Because stack pointers are short lived, while heap pointers can be longer lived.

Say you have a queue of pointers that some thread is chewing on, you can't stick a pointer to a stack variable into that queue and then exit the scope of that stack variable, but you could put a longer lived heap pointer into that queue and exit the scope of the pointer, essentially passing ownership of the pointer to whatever's chewing on the other end of the queue.

The C standard does not talk about a heap or the stack; you could implement a "heap" by handing out pointers from the top level stack frame if you wanted to.
Is this important? By far the common case in C is using heap allocation as implemented by your libc or crt. It's pretty essential knowledge.
Stack pointers can live just as long (for example, define some objects in main(), and they will exist for the duration of the program; hand out pointers like candy). An allocated object can be deallocated whenever.

Dereferencing said pointers isn't any different, no matter how the pointee was allocated:

    void myfun(void *foo) {
        // do things with foo.
        // how it was allocated is all the same to me
    }
As far as C is concerned, stack and heap do not exist. The validity of reference to an object is defined in terms of lifetime, which stems from its storage duration (static/automatic/allocated).

And these are concepts that just define the semantics of the language. For example, an object with automatic storage duration might never hit stack or heap or any other part of RAM; it could live in registers, or be optimized out altogether. It could be in the bss segment. The semantics also do not forbid the implementation from using mmap() or malloc() (or similar) for objects with automatic storage duration.

Using stack for automatic variables and heap for allocated variables is just one implementation technique, which should not give rise to any observable differences in the meaning of a legal program (i.e. one that doesn't go into undefined behavior) as interpreted by the abstract machine, which the standard defines.

I guess they're not "exclusive" to C, but they're very important in C in a way they aren't in most languages. So I think op's advice here is good.
> Also, understand the undefined behavior, the compiler optimizations and how it can affect your code if you forget about the UB or if you ignore the compiler warnings, and read about the `volatile` keyword.

PSA: volatile generally doesn't do what you want it to; you almost never want it. volatile does not "fix" undefined behavior.

> And a personal tip, please tell the compiler not to use strict aliasing (I think it's stupid) - you will get rid of some UB without much sacrifice.

This prevents the compiler from performing a number of optimizations.

I learned a few basic levels of C using my subscription to pluralsight.com.

But... I quickly switched to Rust as I see it being the C of the future as it continues to develop. That's just me though. (I have similar opinions towards things like Kotlin or Go.)

The books I am reading in the comments below are great resources.

I would focus on sources that use C11 or C18, since there are some niceties in the newer standards.

As a reference, I like QEMU's source code[1]. It's huge, but the style and practices found in any file will help you get a grip on good C.

[1] https://github.com/qemu/qemu

You can compile with clang on Windows, so you can use later standards.
I liked "C Programming: A Modern Approach"[1]. It has been several years but as I recall it was well suited for self study and was pretty explicit in calling out places it was talking about C99 as opposed to C89.

[1]: https://www.amazon.com/C-Programming-Modern-Approach-2nd/dp/...

You might also take a look at this Minecraft clone in C[1]. Uses sqlite to persist state so you can see how you might interact with a database as well. Quite modern and readable in that most functions outside main.c are <20 lines long. It's also cross platform and should work on Windows, Mac and Linux.

[1]: https://github.com/fogleman/Craft

The proper way to learn C is to get a good book, read it, and do exercises along the way.

Here’s an excellent book: C Programming: A Modern Approach, by King.

Avoid online tutorials, and know that there are many poor books on C, including the recommended here “Learn C the Hard Way”. It has factual problems; see under “Stuff that should be avoided” here: http://iso-9899.info/wiki/Main_Page

Note also, that unlike some other languages, C is not a language that you should learn by “trying things out”, due to the nature of “undefined behavior”.

Another recommendation would be to ask for CR on IRC.

Good luck!

I’m always skeptical of telling people to not just try something out in programming. Yes, don’t just try to code up the next unix without knowing any c. But definitely getting your hands dirty isn’t going to instill any super bad mental/coding practices.
C is a particularly weird language: it's entirely possible for things to work but

- secretly introduce security vulnerabilities

- stop working on a different compiler or after a compiler upgrade

A generation of C programmers learned by trying things out, and they left us with superstitious beliefs about volatile (and also with imprisoned Uyghurs in China). We have the evidence and learning C by trial and error doesn't work. If you're going to write C (and there are definitely places where it's the right thing to use), learn it with more caution than you'd learn other languages.

Ok I’ll bite, what on earth is the connection between Uyghurs and C?
Presumably the fact that iOS had vulnerabilities that allowed for watering-hole style zero-day attacks on Uyghurs in China.
By that point you should be well beyond just messing around with c.
Should be, yes, but again we have the evidence: the people whom Apple hires to write kernels regularly make these sort of mistakes. (Let alone the people that Microsoft hires, or the people who write various third-party Android drivers.) So whatever form of "should" that we have right now isn't working.
You can learn by playing around, but its very helpful to have a guide in the begining.

I had to start coding for work in C and without some good footing it was frustrating dealing with older code and creating new things with my very limited experience. (I had c++ which isn't the same.)

Having spent a week going a book and getting up to speed helped greatly and increased my enjoyment, reduced my frustration fwiw.

> Note also, that unlike some other languages, C is not a language that you should learn by “trying things out”, due to the nature of “undefined behavior”.

I disagree. You have to learn the ways in which things fail in any language you learn, especially a language like C.

The point is that you don't learn all the ways in which things fail in C by trying. At best, you learn the way in which they fail (or fail to fail!) on your specific platform/compiler/version/compiler options combo.

If you want to know how things can fail (or, what things can fail in unspecified ways), you have to do a bit of reading and no amount of trying and experimenting will conjure that knowledge.

There isn't a guide to learn how things fail on platform, compiler, version, options combo. Probably the only things that force you to systematically research and accumulate such knowledge is by writing a language VM in C or use C as an intermediate representation. Even writing a C compiler itself won't teach you that.
> There isn't a guide to learn how things fail on platform, compiler, version, options combo.

Yeah, that's the point. Chasing that knowledge is pointless. Instead, read the spec and find out what is legal and what is not. What is defined and what is not. Then you can stop worrying about how things fail and start worrying about not doing those things.

Trying c out with a known solid testing framework seems like a much more practical and useful way to start. Telling people to read a text wall before doing any practice at any task is a great setup for failure. Math doesn’t do that. Physics doesn’t do that. Chemistry doesn’t do that. Literature doesn’t do that. Philosophy doesn’t do that. Etc. and of those philosophy is almost the study of text walls. But the best philosophy courses tend to start with a Socratic dialogue which is basically live action philosophizing.
Well, we also have lots of historic evidence (nearly every application written in C ever) that traditional/intuitive teaching methods don't work for C.

All people have learned from the last 40 years of C is how to write ridiculously insecure applications.

Reading the standard may not be the optimal choice, or even the first choice, but common teaching methods are nearly guaranteed to produce crap results in the specific case of C programming.

I broadly agree. More so than any other other language, you cannot produce a good C (or C++) programmer by try-it-and-see alone. It's vital to have a grasp of the way the language is defined.

Point of disagreement: no amount of C expertise makes a hand-written C codebase safe. Vulnerabilities and undefined behaviour are often found even in code written by top-flight C programmers.

To me it's really both. Do one first, notice it works sometimes and you're not sure whether you should publish it (at that point, don't :) -- ) and then look at your own product through the lense of what you're learning. Eg. there are different kinds of UB and different problems it causes. There's a lot of entertaining literature and people in certain communities that can get you on the right track.

One of them was certainly this one: https://devblogs.microsoft.com/oldnewthing/20140627-00/?p=63...

Since UB can be a bad beast to tackle while learning C and C++, I would suggest to frequently compare what you learn with cppreference.com, and check that you understand that what you think matches what the standard dictates. Cppreference is not the standard, of course, but is sufficiently similar and much easier to read.

If at some point you want to really become a master, then switch to the actual standard.

C is great for learning about how computers work at a low level. In my college, we started by writing a simple compiler. This should force you to understand pointers and memory which, as others have mentioned, are fundamental. You'll have to both know how the assembly works and to write correct C code. So writing a simple compiler will force you to understand it two different ways at once. My opinion is you'll learn more by diving in and doing things, especially with your existing programming background.

But, for your own sanity and everyone else's, do not start new projects in C! (Aside from purely academic ones for you to learn.) The point of a programming language is to help humans write safe and correct code. In this sense, C has completely failed. The syntax itself is just begging you to make a subtle mistake. And due to lack of memory safety, even the most well tested and scrutinized projects in C/C++ (such as Chromium) suffer from buffer overflows or memory leaks, potential security vulnerabilities that are automatically prevented by design in other languages. If you need to do something low level with any sort of confidence, use a memory-safe language like Rust, which can even do interop with existing C libraries and code.

(Edit: typo)

It depends on what you're doing. Low level stuff pretty much has to be C. But for applications I agree there are much better alternatives these days.
Yeah, there may be some situations where C is necessary as a wrapper for assembly if you're _extremely_ memory constrained or something like that. But I wouldn't have any confidence in it working as intended unless it's dead simple.
> The syntax itself is just begging you to make a subtle mistake.

C's syntax is fine (if a bit ugly around declarations), it's really the memory safety that gets you.

Radical suggestion: read the C standard.
Asking someone to read a multi-hundred page standards document just to learn the language is wholly unreasonable.
Well, OP asked for source of truth. Nothing comes as close as the standard itself.

Honestly, I think any programmer writing C today should have it around for reference. And yeah you kinda need to read it too, or you won't be able to refer much.

That said, no need to read it cover to cover. There's stuff one can earmark as being there but ignore until it's actually needed (for example: the grammar and all the library functions).

And speaking of tedu, I would recommend to the OP that they get in the habit of checking out the OpenBSD man pages for libc functions.

I actually have very few $200 PDFs lying around.

Do you really go purchase the standard documents from ISO? Or does your employer? I'd love to have the actual standard "around for reference," but 700-page technical documents from ISO are not cheap.

The next best thing is the latest draft standards, which are available online for free (and I always have them on my laptop, PC, and personal server). Differences between them and final official standard are not important. Search for N1256, N1570. Or grab them here. http://www.open-std.org/jtc1/sc22/wg14/www/standards

(There are more readable html versions floating about)

> Well, OP asked for source of truth. Nothing comes as close as the standard itself.

An unfortunate reality is that standards are not always strictly followed (whether intentionally or not). C is almost certainly better about following the standards than other pieces of tech (notably web browsers), but I doubt the C compilers are perfectly compliant if you look hard enough.

(comment deleted)
It sounds like you're not asking, "How do I learn the language?" but "How do I know I'm doing it right?"

I think Gustedt's book is superb if you're just trying to learn the language, and it does a good job of presenting the most up-to-date standard. I admire the K&R as much as anyone else, but it's not exactly an up-to-date language resource at this point, and it doesn't really provide any guidance on the design and structure of systems in C (that's not it's purpose).

You might have a look at Hanson's C Interfaces and Implementations: Techniques for Creating Reusable Software. That's focused on large projects and APIs, but it will give you a good sense of the "cognitive style" of C.

21st Century C is very opinionated, and it spends a great deal of time talking about tooling, but overall, it's a pretty good orientation to the C ecosystem and more modern idioms and libraries.

I might also put in a plug for Reese's Understanding and Using C Pointers. That sounds a bit narrow, but it's really a book about how you handle -- and think about -- memory in C, and it can be very eye-opening (even for people with a lot of experience with the language).

C forces you to think about things that you seldom have to consider with Javascript, Python, or Go. And yes: it's an "unsafe" language that can set your hair on fire. But there are advantages to it as well. It's screaming-fast, the library ecosystem is absolutely gigantic, there are decades of others' wisdom and experience upon which to draw, it shows no signs of going away any time soon, and you'll have very little trouble keeping up with changes in the language.

It's also a language that you can actually hold in your head at one time, because there's very little sugar going on. It's certainly possible to write extremely obfuscated code, but in practice, I find that I'm only rarely baffled reading someone else's C code. If I am, it's usually because I don't understand the problem domain, not the language.

I second every book recommendation here. Another great one is The Standard C Library.
> the library ecosystem is absolutely gigantic

I can call out to C libraries from other languages, so that's not a big deal.

In addition, I think that probably the JVM ecosystem is larger and that Python may be as well.

You also find certain things at the bottom (like glibc, for example) where the C ecosystem isn't as diverse as you think it is.

For Linux userland, there are also musl and uClibc-ng. Other platforms have their own libcs.
Do any of these resources help with learning how to deal with:

1. The lack of abstractiom in C? 2. All of the pitfalls and gotchas: Undefined Behaviour, use after free, etc?

I've been writing Rust for a couple of years now, so I'm quite comfortable with low-level ideas, memory management, etc. But C still scares me because it seems so easy to make mistakes, and I also often find it hard to see the forest for the trees.

Perhaps C's just not for me?

Yes. Or, at least, the second part.

I'm not sure what you mean by "lack of abstraction in C."

"Abstraction" is not a straightforward property of a language; it's a thing you create with programming languages. It's true that some languages make it easy to express certain kinds of abstractions, but C has it's own way of abstracting things, and it's certainly not absolutely obvious that its way of doing so is inferior to others.

I recommend Bradley's 'programming for engineers: a foundational approach to C and Matlab'. Simple book with relevant exercises.
If a book on C is described as "simple" I bet it's not about doing things right (which is what OP is worried about.) Doubly so if it covers something in addition to C.
I think both “Expert C” and “Modern C” do a reasonably good job working through memory management patterns, strategy, and constructs. However, I’m using this comment as an attempt discuss another topic you raised in your comment.

I don’t mean for this to be flame bait, but I do think it is worth pointing out and discussing if you desire. The concept of memory management that is presented in a majority of the Rust that I have seen written outside of deep library code and the parts of the low-level implementations of some standard library functions has very little relation to the analogous concepts in C. I’d say that what Rust considers memory management in average code is more similar to programming in C without using any heap allocation. I could be wrong or could just have not seen the right code bases, but there is very little C style byte level ‘data structure’ layout design and then the allocation of those sorts of data using multiple allocation strategies.

I certainly understand that the above mentioned constructs and patterns are really not part of Rust’s idiomatic usage, and that a chunk of C’s allocation options are semi-prohibited because of Rust’s memory model. But, if you are coming from Rust and going into C, the differences are far greater than the similarities in this area.

I’m certainly not questioning your credentials, experience, or ability, I really just feel like this area is lacking any substantial discussion where said discussion is not focused on the merits/drawbacks of the language design. You clearly know Rust and seem to be open to learning or working with C, so it isn’t about Rust v. C, it’s about compare and contrast, and highlighting the fundamental differences that could cause some mental roadblocks.

P.S. Sorry for rambling, Day Light Savings time is kicking my ass.

Not parent but Rust definitely makes you think about a lot of important aspects of memory management like lifetimes and references and ownership and stuff. In fact, it makes it impossible not to think about them: they're enforced by the compiler.

Those mechanics are pretty important but Rust definitely lets you program with values on the heap as well, it just does it in a strongly-typed fashion. I will agree that smart pointers are generally easier to work with, particularly with things like deref coercion, but Rust is philosophically closest to a language like C++ but with a stronger type system, memory safety, and lots of footguns removed.

I think Rust gives someone a better basis for learning and writing C than probably any other modern language.

I grokked C only after dabbling with Forth. C is too high level to know what's going on at the hardware level.
Most processors these days are basically running C virtual machines in hardware anyways.
> I could be wrong or could just have not seen the right code bases, but there is very little C style byte level ‘data structure’ layout design and then the allocation of those sorts of data using multiple allocation strategies.

My experience is mostly in very high level languages, C#, TypeScript, SQL, so that's probably reflected in the style of Rust code that I've been writing, but at least as one data point, yes, it is very possible to write day-to-day Rust code without getting into byte level data layout.

It does feel a lot like writing C code without using a heap. In fact the only case I've dealt with allocating uninitialised heap memory so far has been when interacting with a C API. Creating a new Vec does allocate memory on the heap (once values are pushed into it), but as a user it feels like passing around any other stack allocated struct. I think this is mostly due to the RIIA-style Drop and ownership system, as there is no extra code to free a value that owns a heap allocation vs a stack-only value.

The implementation of std::mem::drop is a fun example of this: https://doc.rust-lang.org/std/mem/fn.drop.html

There is an old but great book called "C traps and Pitfalls" on that subject.

Otherwise you're left alone with yourself and your tooling. Things like valgrind help a lot. But I'll admit that I too find large C projects alarming; it may be better to write "C with classes" code in a C++ environment, so at least you have classes and namespaces as an organisational tool.

Do you think that van der Linden's Expert C Programming is still worthwhile?

There's also the comp.lang.c FAQ http://c-faq.com/ , which is also very old now but still seems like a no-brainer to me.

I think Expert C Programming is a great book (really, as much a classic as the K&R), but I'm not sure I'd put it on the OP's list. At least not right now.

It says something about C (I'm not sure what) that it's possible to learn quite a bit about C from a book that was written in 1994. But it was written before C99, and it shows.

If you're a C hacker and you haven't read it, definitely do. It's great.

Another great way to learn about C pointers is to reverse engineer games, live memory editing and scanning in particular requires a lot of global/heap pointer detective work.