> C can teach you useful things, like how memory is a huge array of bytes, but you can also learn that without writing C programs. People say, C teaches you about memory allocation. Yes it does, but you can learn what that means as a concept without learning a programming language.
The problem with those takes is that it always come from the people that already know it and had a lot of baggage that distorts it’s view that no one else should learn.
Due to several reasons I always worked with high level languages and last year I started to learn C and C++ and how beautiful was to know exactly how are you allocating resources in your application that I felt quite good programming mentally and intellectually.
When you come from a world of resource overprovisioning (CPU, memory) when you start to get your head about those languages that puts performance first, you feel good about it in the first moment but in the second after you feel that your whole experience was subpar due to lack of a thought process.
> ...I started to learn ... how beautiful [it] was to know exactly how are you allocating resources in your application...
Of course, as the author implies there's an MMU, register renaming, not to mention a whole OS involved, so how "exactly" are we talking about? As Blake wrote, there's "a world in a grain of sand".
(Of course I'm excited for you and share your pleasure in understanding memory layout and its implications. Computing is really fun the less abstract it is. And that sheer depth means we can plumb those depths -- or heights -- as far as we like).
It is as close to hardware as your userspace program is allowed to get.
There is definitely a whole bunch of lower layers, but you cannot really get to them, so I think it's pretty fair to call C "a lowest layer language for userspace after assembly". And probably for kernel programming, too - C gets into implementation-defined behavior when there are MMUs involved, but that's not a big practical problem.
(if you work at Intel and have access to secret microcode definitions than C is not a high level language... but cases like this are super rare)
Thanks for the words. Maybe it's about the whole experience that I got in the beginning of career that several experienced folks told me "See, learn Python/Ruby 'cause it has more jobs available and pay more". It was true on my demographic at that time.
Along the time I was not exposed to those "low level" languages and it had the effect that I never had that perception. That's why I felt that my experience was subpar: In a point in time, I was living in a complete different epistemic universe even being on the technology.
From the article they also cite that C has wrappers around it to make the higher level code be faster. So I would also additionally say that you should learn how you can extend your program by learning how your language interact with C / C++
Now do it the other way around - write a complex project using MicroPython while being conscious about memory allocations, because mark-and-sweep GC takes 200ms (6 frames at 30 FPS budget) to go through 8MB of SPI PSRAM. It's a nightmare that makes you cry for C :)
I'm confused as to why it's such a big deal to learn C. It's not a very large or complex language. If you already know how to program, there isn't that much to learn to understand C (pointers aren't complex).
It's also a fine beginner language, especially in a structured university setting imo.
Yes, you probably don't need to learn it. But it can come in handy. For instance, there's a wealth of useful software out there in C. It's nice to be able to read its source, hack on it, or leverage it from your higher-level language via FFI.
C syntax is limited and therefore "simple". Building advanced data structures and programs with your limited toolset does require some out-of-the-box thinking.
This is partially why C is a common teaching language. Because you have to build a lot of tools that are easy to take for granted in other languages. Take a Map or Dictionary for example. C doesn't technically speaking provide one, so you learn to implement them pretty quickly and often. Technically speaking C doesn't even have a primative string type, you just create an array of chars, which by the way has to be a pointer. While this is super easy to do once you've seen it once or twice, it teaches you how strings work in almost every other language.
The reason you can trivially loop through a string in python, is because it works the same way, they have just abstracted it a tiny bit from you.
Now to the author's point... does that really make you a better developer to know that? Therefore is it really needed? No probably not needed. But these exercises and experience does help solidify more complex topics later on. So you don't need C, but I still think there's benefit in learning it.
> Take a Map or Dictionary for example. C doesn't technically speaking provide one, so you learn to implement them pretty quickly and often.
I take issue with the "quickly and often" part, but you do learn to implement them! And that provides valuable insight when it comes to other languages.
I've had senior devs who, like past me, didn't understand why removing an element from a vector or map invalidates iterators or indices on it. I learned precisely why when implementing vectors in C: the usual implementation of those collections shifts everything around to maintain element contiguity in memory, so now your iterator/index is pointing to a different element (which incrementation will skip) or possibly past all the elements!
And when you're presented with these fancy data structures in Java or Python, that feels unintuitive and wrong. "Come on; I should be able to just iterate through this and remove certain elements as I go," like you would remove objects from a shelf. The shelf doesn't rearrange the objects after you take one out! But the reason it feels like an affront to common sense is that you've been trained to take the fancy data collections for granted and not forced to understand the complexity behind them that makes them efficient.
Of course, the real answer is to do that with some functional algorithms, like `std::remove_if()`, instead of via iteration. That way, you'll really be in the cloud-scraping ivory tower and never have to think about what's going on below. https://youtu.be/ggijVDI6h_0 :p
This is a huge pain point. If you're writing small programs that fit in a couple files in a single directory, then you can get by with manual invocations of clang or gcc at the command line. If you want to build anything of moderate complexity, then you need to get familiar with all the complexity of a separate build system like Make/CMake/Bazel/etc. Java has a similar problem - once your program grows beyond ~1 directory, you end up needing to learn Maven/Gradle/etc.
Build and testing tooling is one thing that younger languages have generally done much better. Rust, Go, Dart and others all have standard build tools integrated with the language that scale to large projects, with standard testing frameworks integrated with those tools. Scripting languages like Python have import features within the language itself. Much lower cognitive overhead.
This is a huge pain if you want to support multiple systems, which most of the biggest products do.
But if you (1) choose a single OS, (2) have "few" (say under 20) files and (3) rely only on OS-provided libraries, then things are actually pretty simple. For example, you only support Ubuntu 20.04 or Fedora 39. Then your makefiles are nice and easy to read, and you just rely on pkg-config for the libraries.
Man, what a bold claim. I could say all kinds of things about the number of books written about C, the size and complexity of its compilers, the failure of even veterans to avoid mistakes, the constant disagreement amongst even experts about the most fundamental things like realloc, but my only argument--indeed the only argument I need--is that the standard [0] is literally hundreds of pages.
> pointers aren't complex
Pointers are so complex they're responsible for the vast majority of security bugs across all of computing history, and they're so powerful and overloaded that they can't be fixed without bonkers hardware interventions like CHERI. You might say, "nah it's buffers" but when you have a pointer, everything is a buffer! You might further say "nah it's random undefined behavior", but it's very, very hard to use a pointer without invoking undefined behavior. Here's a quick thing I googled:
int func(int **a)
{
*a = NULL;
return 1234;
}
int main()
{
int x = 0, *ptr = &x;
*ptr = func(&ptr); // <-???
printf("%d\n", x); // print '1234'
printf("%p\n", ptr); // print 'nil'
return 0;
}
Did you know that the language doesn't specify whether the deference on the left of `*ptr = func(&ptr);` comes before the address-of on the right, thus this behavior is undefined? Can you really be arguing that this isn't complex?
Look I love C, but it's deeply complex and a lot of that is pointers.
They are complex if you are coming from the Python world. To truly know their recursive declaration syntax, you have to see it from a compiler's perspective. Pointer arithmetic isn't something you do often even in C++. It's a crude kind of reference that can be mutated arithmetically and doesn't provide any safety whatsoever. It maps well to the indirection mechanisms and addressing modes provided by the underlying hardware.
Honestly I find Python's value vs. reference semantics fairly confusing, and it's not always obvious whether an operation is going to make a copy or modify an existing instance of an object. Good API design and docstrings mitigate this, but it's one more source of complexity to think about. I run into this much less frequently in C++ code - it's normally pretty clear from parameter and return types whether I'm dealing with a copy or a reference.
This might just be because C++ broke my brain into assuming:
Object foo = other_object;
is a copy operation, and taking a reference or pointer would require extra characters (e.g. copy by default). Most other languages are the opposite: assignment creates a reference by default, and making a copy would require extra characters (e.g. .copy() in Python or .clone() in Java). That's my biggest mental adjustment when moving back-and-forth between C++ and Python.
huh? I work with pointers all the time, and I don't think I ever cared about assembler addressing modes.. Who cares if you pre-increment in the same instruction or if you need a separate operation for that? Who cares if offset is computed as part of the load or separately? This does not change how the program is built or debugged.
(Unless you are talking about x86 real mode segment registers. Luckily, we can leave this behind and never talk about them again :) )
The point may be that time is a scarce resource and you better focus on what's important for you. C may be it, but I think Ned's point is that people might be learning C thinking it will give them a skill that it actually doesn't.
I find thinking about time like that ends up counterproductive. You can read K&R and code/read a little C and never build a single thing in it and have it be worth it. For instance, I wrote C in school but basically never use it professionally or for my hobbies (directly at least). But I'd say learning it was worth it - my school curriculum didn't "waste" my scarce resource of time. You do end up learning to think about computers in a different way.
Pointers aren’t complex, but C makes them look complex with the counter-intuitive syntax. Therefore, I don’t think C is a good language for learning pointers.
C might not be strictly necessary as a language one would write in.
But all of those problems that C++, Rust or Zig solve - these all are discussed in terms of C. Toolchains, OSes, concepts of static and dynamic linkage are all defined in terms of whatever few abstractions C provides.
And sometimes all you have is a good old C compiler. Because C is everywhere.
It is not necessary to know the language but it sure is useful to see beneath and beyond.
With C, if you can understand the lifetime of variables on the stack, how to manage pointers to the memory allocated on the heap, etc, it'll help with learning Rust, Zig, etc. Implementing your own malloc/free, memory arenas, reference counting, vtables, etc, and doing so all in C will lead to a concrete understanding of how these features work in other languages and ultimately to more intuitive designs and implementations.
I always think that these "C is a high level language / virtual machine and it doesn't teach you how your computer works because your computer isn't a PDP-11" style takes are very silly.
C is a high level language that maps very straightforwardly to what the hardware does and programming on a microcontroller without an OS let's you eliminate a ton of abstractions that otherwise take years to comprehend.
If someone wants to understand what's going on "behind the scenes" then it's going to be very useful for the to know C before trying to learn how the Cpython interpretor or JVM work.
The tone of this article is a bit combative and incurious for my taste.
If you want to understand "how a computer works" - well sure C has gone from low-level to a high-level language. But the model of "how a computer works" - from Python or Ruby or JavaScript - that's very often expressed in C: kernels and libraries, in multiple layers. There's so much rich information in C code bases on why something behaves like it does. Whole histories of computing are expressed in C - types and APIs that tell you why something might work on one computer and not on another.
So sure, don't learn C if you don't need to, but it's not hard to understand if you're motivated. And it really does expose you to how a computer works in ways you won't find in higher-level languages.
Personal PoV: I think it is important to learn the C abstraction because lots of senior people in the industry talk in that abstraction level and if you're not able to communicate on that level, it may be a communication hurdle for you.
Lots of people certainly can survive without knowing any C, I'm just saying that it is not an irrelevant thing and if you have the time to invest in learning a bit more than the basics, it is not a waste of time.
Sure, I never said it was irrelevant or a waste of time. It can be useful and interesting. It's just not necessary, and it doesn't tell you how computers really work.
First, just wanted to say I follow your blog for a long time and use software you created for many years, so I respect you a lot!
I think we're just violently agreeing. You posted a few reasons people may feel compelled to use as reasons to learn C that are misleading. I'm just adding to that from my personal experience that learning C, even if you are not going to use it every day, also has a collateral advantage to help when talking to people who "think in C" as an abstraction.
I really enjoy C programming. I am a beginner. I wrote a JIT compiler in C recently. Why didn't I write it in Rust?
The semantics of C really fit how what I think about what computers do: that we move around things between boxes of memory and between registers. That computers is largely logistics between "places". Edit: It's a bit like a factory as in factorio
I didn't write it in Rust because I find the semantics of C++, Rust, Haskell and other functional languages to be much more abstract and harder than this simple idea of stateful movement.
If your goal is to learn about how program execution works or memory and other low-level things I suspect learning how to build a compiler targeting a VM (and building the VM) is a good way to learn these things. You'll end up learning how to emit VM instructions to store data and the address of the program counter, jump to the function code, then jump back and unpack all that data, etc. You'll learn how to implement those VM instructions for a target hardware platform. You'll learn how difficult it is to design a good instruction set that covers the platforms you want to support.
The nand2tetris course is a pretty good one; you'll design the hardware itself in the first part and in the second you'll build a compiler/VM/etc for it.
And then you'll gain an appreciation for how C works and why it's useful.
And also why higher-level languages are useful.
I don't think you need to learn C any more than you need to learn separation logic or category theory though.
Update The problem with "learning C" is that it has a ton of warts and you might injure yourself in the effort to master it. The language is coupled tightly to libc, the specification is small as far as modern ISO standards go but it still leaves a lot to the imagination (and the implementors). It's really quite a complex language to learn well due to all of the sharp edges.
And of course, C isn't the royal road. There are lots of other models for thinking about programs that aren't based on imperative procedures. It's just a language.
If you want to think about programming and PLT... there are a lot more sophisticated tools.
The problem with those takes is that it always come from the people that already know it and had a lot of baggage that distorts it’s view that no one else should learn.
Due to several reasons I always worked with high level languages and last year I started to learn C and C++ and how beautiful was to know exactly how are you allocating resources in your application that I felt quite good programming mentally and intellectually.
When you come from a world of resource overprovisioning (CPU, memory) when you start to get your head about those languages that puts performance first, you feel good about it in the first moment but in the second after you feel that your whole experience was subpar due to lack of a thought process.
Of course, as the author implies there's an MMU, register renaming, not to mention a whole OS involved, so how "exactly" are we talking about? As Blake wrote, there's "a world in a grain of sand".
(Of course I'm excited for you and share your pleasure in understanding memory layout and its implications. Computing is really fun the less abstract it is. And that sheer depth means we can plumb those depths -- or heights -- as far as we like).
There is definitely a whole bunch of lower layers, but you cannot really get to them, so I think it's pretty fair to call C "a lowest layer language for userspace after assembly". And probably for kernel programming, too - C gets into implementation-defined behavior when there are MMUs involved, but that's not a big practical problem.
(if you work at Intel and have access to secret microcode definitions than C is not a high level language... but cases like this are super rare)
Along the time I was not exposed to those "low level" languages and it had the effect that I never had that perception. That's why I felt that my experience was subpar: In a point in time, I was living in a complete different epistemic universe even being on the technology.
Deleted Comment
It's also a fine beginner language, especially in a structured university setting imo.
Yes, you probably don't need to learn it. But it can come in handy. For instance, there's a wealth of useful software out there in C. It's nice to be able to read its source, hack on it, or leverage it from your higher-level language via FFI.
This is partially why C is a common teaching language. Because you have to build a lot of tools that are easy to take for granted in other languages. Take a Map or Dictionary for example. C doesn't technically speaking provide one, so you learn to implement them pretty quickly and often. Technically speaking C doesn't even have a primative string type, you just create an array of chars, which by the way has to be a pointer. While this is super easy to do once you've seen it once or twice, it teaches you how strings work in almost every other language.
The reason you can trivially loop through a string in python, is because it works the same way, they have just abstracted it a tiny bit from you.
Now to the author's point... does that really make you a better developer to know that? Therefore is it really needed? No probably not needed. But these exercises and experience does help solidify more complex topics later on. So you don't need C, but I still think there's benefit in learning it.
I take issue with the "quickly and often" part, but you do learn to implement them! And that provides valuable insight when it comes to other languages.
I've had senior devs who, like past me, didn't understand why removing an element from a vector or map invalidates iterators or indices on it. I learned precisely why when implementing vectors in C: the usual implementation of those collections shifts everything around to maintain element contiguity in memory, so now your iterator/index is pointing to a different element (which incrementation will skip) or possibly past all the elements!
And when you're presented with these fancy data structures in Java or Python, that feels unintuitive and wrong. "Come on; I should be able to just iterate through this and remove certain elements as I go," like you would remove objects from a shelf. The shelf doesn't rearrange the objects after you take one out! But the reason it feels like an affront to common sense is that you've been trained to take the fancy data collections for granted and not forced to understand the complexity behind them that makes them efficient.
Of course, the real answer is to do that with some functional algorithms, like `std::remove_if()`, instead of via iteration. That way, you'll really be in the cloud-scraping ivory tower and never have to think about what's going on below. https://youtu.be/ggijVDI6h_0 :p
Writing simple programs was fine enough, but the whole matter of linking libraries and writing Make files was a headache.
Build and testing tooling is one thing that younger languages have generally done much better. Rust, Go, Dart and others all have standard build tools integrated with the language that scale to large projects, with standard testing frameworks integrated with those tools. Scripting languages like Python have import features within the language itself. Much lower cognitive overhead.
But if you (1) choose a single OS, (2) have "few" (say under 20) files and (3) rely only on OS-provided libraries, then things are actually pretty simple. For example, you only support Ubuntu 20.04 or Fedora 39. Then your makefiles are nice and easy to read, and you just rely on pkg-config for the libraries.
Man, what a bold claim. I could say all kinds of things about the number of books written about C, the size and complexity of its compilers, the failure of even veterans to avoid mistakes, the constant disagreement amongst even experts about the most fundamental things like realloc, but my only argument--indeed the only argument I need--is that the standard [0] is literally hundreds of pages.
> pointers aren't complex
Pointers are so complex they're responsible for the vast majority of security bugs across all of computing history, and they're so powerful and overloaded that they can't be fixed without bonkers hardware interventions like CHERI. You might say, "nah it's buffers" but when you have a pointer, everything is a buffer! You might further say "nah it's random undefined behavior", but it's very, very hard to use a pointer without invoking undefined behavior. Here's a quick thing I googled:
Did you know that the language doesn't specify whether the deference on the left of `*ptr = func(&ptr);` comes before the address-of on the right, thus this behavior is undefined? Can you really be arguing that this isn't complex?Look I love C, but it's deeply complex and a lot of that is pointers.
[0]: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3054.pdf
They are complex if you are coming from the Python world. To truly know their recursive declaration syntax, you have to see it from a compiler's perspective. Pointer arithmetic isn't something you do often even in C++. It's a crude kind of reference that can be mutated arithmetically and doesn't provide any safety whatsoever. It maps well to the indirection mechanisms and addressing modes provided by the underlying hardware.
Don't mistake the complexity with ease of making mistakes though.
This might just be because C++ broke my brain into assuming:
is a copy operation, and taking a reference or pointer would require extra characters (e.g. copy by default). Most other languages are the opposite: assignment creates a reference by default, and making a copy would require extra characters (e.g. .copy() in Python or .clone() in Java). That's my biggest mental adjustment when moving back-and-forth between C++ and Python.It really helps if you understand assembler addressing modes. Then it makes total sense.
(Unless you are talking about x86 real mode segment registers. Luckily, we can leave this behind and never talk about them again :) )
But all of those problems that C++, Rust or Zig solve - these all are discussed in terms of C. Toolchains, OSes, concepts of static and dynamic linkage are all defined in terms of whatever few abstractions C provides.
And sometimes all you have is a good old C compiler. Because C is everywhere.
It is not necessary to know the language but it sure is useful to see beneath and beyond.
C is a high level language that maps very straightforwardly to what the hardware does and programming on a microcontroller without an OS let's you eliminate a ton of abstractions that otherwise take years to comprehend.
If someone wants to understand what's going on "behind the scenes" then it's going to be very useful for the to know C before trying to learn how the Cpython interpretor or JVM work.
If you want to understand "how a computer works" - well sure C has gone from low-level to a high-level language. But the model of "how a computer works" - from Python or Ruby or JavaScript - that's very often expressed in C: kernels and libraries, in multiple layers. There's so much rich information in C code bases on why something behaves like it does. Whole histories of computing are expressed in C - types and APIs that tell you why something might work on one computer and not on another.
So sure, don't learn C if you don't need to, but it's not hard to understand if you're motivated. And it really does expose you to how a computer works in ways you won't find in higher-level languages.
Lots of people certainly can survive without knowing any C, I'm just saying that it is not an irrelevant thing and if you have the time to invest in learning a bit more than the basics, it is not a waste of time.
I think we're just violently agreeing. You posted a few reasons people may feel compelled to use as reasons to learn C that are misleading. I'm just adding to that from my personal experience that learning C, even if you are not going to use it every day, also has a collateral advantage to help when talking to people who "think in C" as an abstraction.
Peace.
The semantics of C really fit how what I think about what computers do: that we move around things between boxes of memory and between registers. That computers is largely logistics between "places". Edit: It's a bit like a factory as in factorio
I didn't write it in Rust because I find the semantics of C++, Rust, Haskell and other functional languages to be much more abstract and harder than this simple idea of stateful movement.
https://github.com/samsquire/compiler
The nand2tetris course is a pretty good one; you'll design the hardware itself in the first part and in the second you'll build a compiler/VM/etc for it.
And then you'll gain an appreciation for how C works and why it's useful.
And also why higher-level languages are useful.
I don't think you need to learn C any more than you need to learn separation logic or category theory though.
Update The problem with "learning C" is that it has a ton of warts and you might injure yourself in the effort to master it. The language is coupled tightly to libc, the specification is small as far as modern ISO standards go but it still leaves a lot to the imagination (and the implementors). It's really quite a complex language to learn well due to all of the sharp edges.
And of course, C isn't the royal road. There are lots of other models for thinking about programs that aren't based on imperative procedures. It's just a language.
If you want to think about programming and PLT... there are a lot more sophisticated tools.