Modern C [pdf]
icube-icps.unistra.fr
icube-icps.unistra.fr
I've been writing C since the late 1980s, moved to mostly C++ by the mid 90s, C# in the 2000s, and now I've come back to C. Most recently built some realtime components and drivers, having to drop back to C77. I mention this as I've taught many colleagues along the way and I'm sensitive to the places where beginners tend to get hung up with problems and I've come to anticipate many of the questions along the way. Let me take a moment to illustrate the base of the problems i see:
"Too much, too fast." The best example is right on page 2: a program which demonstrates a complex printf format string, along with arrays and loops. I can't help but sarcastically ask "Are you sure that is how you want to introduce someone to the language?" A beginner's eyes will glaze over.
Seriously, the way to introduce the language is simple examples. Explain the main is the entry point where all programs begin running, and that main returns it's success or failure to the operating system (or other program that called it). 3 lines of code.
Then add a SIMPLE print, if you wish, or a variable declaration. Int. Float. char. again, it MUST be simple. Introduce loops. Then show how to move some functionality out of main into a subroutine/a new method/new function, how to call that function, and return results. Talk about header files, etc.
From there, dive into the rest of the base language... talk up arrays, memory management, heap/stack, pointers, libraries, exceptions, etc.
But this is only my experience, and I'm sure that it is different for others. Kind regards.
Take the Javascript course - a fantastic way to get introduced to the syntax of the language, and I highly recommend it for total noobies. But then you come out of it with no understanding whatsoever about what javascript is. If I asked someone who just finished the course to make an "app" that console.log'd to the console, they wouldn't understand where to start. They wouldn't know that JS is a language run in the browser, that they need an HTML file with a script tag or a node file that they can run in the terminal. They wouldn't know about DOM manipulation, etc.
This reminds me of the Java class I took in highschool - the teacher was going on about ints and floats and loops, and the only questioned I wanted answered, and never got an answer for, was "what does `public static void main` mean?" I think the fact that I never got an answer to questions like that are why it took me nearly 3 years into my career to figure out I should be a developer.
The problem with this question is that there is a ton of stuff you need to understand before you can really answer that question fully. To know what public means, you need to understand classes, and visibility rules for classes. To understand static fully, you kind of need to know how c++ works, since it's equivalent to a bare function in a namespace. Void is the type of the return, which means it doesn't have so that's pretty straight-forward. Main is the name of the function that gets run when you run the program, which kind of requires knowledge of program entry points (assembly) either that or the understanding of what a library is. The simplest thing you can say is that it's boilerplate to signify what function gets run when the program starts, but that doesn't really explain any of the pieces.
There are many more languages that implement a "Hello, world!" with one line of code. If explaining "public static void main" is too hard, maybe one is using the wrong tool.
So the first language should be one that gives the student a firm foundation from which one can become that competent.
I've personally seen excellent introductions to programming using C (CS50x), Python (Think Python, MIT 6.001), and Scheme (SICP).
I'm so glad I learned Lua before taking APCS, otherwise that class probably would have set me back 6 years.
For me and probably many others having it go unexplained was like dangling carrot in front of me. I went home and read more about it.
Which is why Java is a terrible language for an introductory programming class.
For a particular kind of curious student, "just do it because you have to" is a very fast way to them checking out and doing nothing at all.
If a student is going to 'check out' because they didn't get a question answered, then they're probably just going to fail. See your teacher after class, ask a friend, look it up online... If are a 'curious student' doesn't see these very obvious methods of satisfying their curiosity, then they're probably not worth a teacher's efforts.
Python is even worse, because it's so high level and so much happens 'by magic'. Don't get me wrong, it's a great teaching language because it does cover so much ground, but it's terrible as a first introduction for a new programmer.
They need to start with something simple and fairly concrete. Maybe even start with a simulated 'toy' assembler (first semester CS101 = Zachtronics games?) then something like Pascal to teach the basics of control flow and sequential processing.
Once students understand primitive data types, control structures, functions, and compound user-defined data types, they're probably ready to learn some OO.
No matter what you are teaching, whether it is fundamental like reading, physical like a sport, or technical like programming, it is critical to teach ONE THING at a time. Teaching multiple concepts at once muddles the exercise and slows down learning. Breaking large concepts into discrete blocks lets the student focus and then build on that concept as they continue.
That's certainly how I work, and how I've heard experienced teachers explain it.
I think the follow-up C book I read after that was "Learning C". I don't recall the author's name(s) but I think it was from two brothers. Dan, something? (I'll check my bookshelf when I get home tonight and update here...).
I learned C++ initially as just 'C with Classes'. It was informal, by joining a C++ project already underway, and following the senior developer's guidelines. Instruction was informal, and under supervision of others, yet I hadn't made a complete mindshift to OO until probably six months to a year after using it.
I liked "Thinking in C++" (Bruce Eckel @ http://mindview.net/Books/TICPP/ThinkingInCPP2e.html ) quite a lot -- in fact I re-read it several times about six months apart and it seems I always pick up some new nugget of knowledge every time through. That or I forget what I don't use. Possible.
Keep in mind the newest of these is a decade old, at best. Surely not "modern" C. But after completing a basic tour of K&R C, a reader should be ready for the book at the top of this discussion. And that will transport them into this century.
hope this is helpful.
[1] - https://www.amazon.com/C-Programming-Modern-Approach-2nd/dp/...
If you're new to programming, Harvard's CS50x on edx is probably the best introduction to programming online and uses C. You'll learn enough to breeze through K&R and then some.
for (size_t i = 9; i <= 9; --i)
is a pretty terrible example to put in the second chapter. I would not let that line pass code review. There is no need or place for cutesy cleverness in C.EDIT: Ugh, just found this too:
isset[!!largeA[i]] += 1;
Not only is that confusingly cutesy, but largeA[i] is a double. Please DON'T write – or encourage beginners to write! – such smug code!EDIT2: In section 5 is the statement than unsigned integers "can be optimized best." This is flatly untrue on x86 and I suspect many other architectures. Compilers can and do take advantage of undefined signed overflow to optimize signed arithmetic; the same is not possible with unsigned arithmetic. See https://kristerw.blogspot.com/2016/02/how-undefined-signed-o...
Obviously. However, it is an appropriate example if your goal is to teach the intricacies of the C language. You're right that if you provide such an example this early, it could perhaps use some additional commentary.
Not only is that confusingly cutesy, but largeA[i] is a double.
That was the point of the exercise: !! is an idiom to convert to boolean (which happen to be integral in C) and something a C programmer (or JavaScript programmer, for that matter) should be familiar with.
This is flatly untrue on x86 and I suspect many other architectures.
Agreed.
As someone unfamiliar with C - I initially thought it would go on forever. The explanatory text explained why I was wrong and this can be equally parts "clever code" or a "gotcha!" depending how you view it. With your experience you're seeing it as overly clever code. With mine, I'm seeing it as a "gotcha". I don't think it is supporting writing code like that. :)
>The third for appears like it would go on forever, but actually counts down from 9 to 0. In fact, in the next section we will see that “sizes” in C, that is numbers that have type size_t, are never negative.
for (size_t i = 9; i --> 0; )
This has the advantage to be very easy to pattern-match once known. Obviously, for a beginner, I would just do: for (int i = 9; i >= 0; i -= 1)The fewer tricks and patterns you use in C, the higher chance actual bugs have of being caught. Cutesy tricks like "-->" confuse human analysis and gain nothing.
Obviously, I just follow the convention when contributing to an existing project.
The range of indexable array elements is not only constrained by the unsigned type size_t, but also by the signed type ptrdiff_t, so you could always go with the latter instead of the non-ISO ssize_t.
At least it's not
for (size_t i = 9; i >= 0; --i)
:-)edit: Ok, looking at it again the parent example is probably going to overflow or something right?
The other way around: size_t is an unsigned type, so decrementing 0 will wrap around to SIZE_MAX, a value that is positive as well as greater than 9. This means counter to your intuition, the first loop will terminate and the second one won't.
Yes (see other replies), and it's precisely the reason why this code shouldn't pass code review.
At first glance the first example looks like it should fail but in fact works. The second example I provided looks like it should work, but in fact loops infinitely.
The first one is tricky and nifty, but prone to bugs. Using signed counters is better way to go about it.
https://gustedt.wordpress.com/2014/10/14/musl-1-1-5-with-ful...
yeah I hear that often when talking about C11 :]
Who would complain about something as wonderful as C11, outside it not being available for your compiler?
Beats me, but see rest of thread I guess :] In all fairness, sometimes it's the right tool for the job, sometimes it isn't
I mean, is there some subset of C that is safer than what I think of when I think of C? I know about stuff like reference counting techniques, rather than manual memory management, for example, and that goes miles towards safer coding. But, even so, the variety of ways you can shoot yourself in the foot with C are seemingly beyond counting. Are threads and async easier and/or safer now than 10-20 years ago, and with more direct language or standard library support? Is memory management in the standard library safer today? Are there concurrency primitives (beyond low-level interacting with epoll or kqueues or even fork or whatever)?
I mean, it's obviously possible to write reliable, safe, secure, software in C (Linux, Git, SQLite, all come to mind), but how much easier has it gotten? Would anyone choose C for a new systems project with no legacy baggage or dependencies, in a world with Rust and Go?
Rust seems more promising, but it is still not to the point where I am interested in rewriting SQLite in Rust, though I may revisit this decision in future years.
Some current reasons to continue to prefer C over Rust:
(1) Rust is new and shiny and evolving. For a long-term project like SQLite, we want old and boring and static.
(2) As far as I know, there is still just a single reference implementation of rustc. I'd like to see two or more independent implementations.
(3) Rust's ever-tightening interdependence with Cargo and Git is disappointing.
(4) While improving, Rust still needs better tooling for things like coverage analysis.
(5) Rust has "immutable variables". Seriously? How can an object be both variable and immutable? I realize this is just an unfortunate choice of terminology and not a fundamental flaw in the language, but I believe details like this need to be worked out before Rust is considered "mature".
A small syntactic sugar you can trivially implement yourself makes Go a non-starter?
Go doesn't include assert in the language because you're supposed to do better than assert. Assert easily allows lazy programmers to let their programs freely crash without properly handling error conditions. Go prevents you from compiling with unused variables, and that combined with the Go documentation goes a long way towards teaching new Go programmers how they're expected to work.
And neither Go nor Rust could possibly be good fits for SQLite. A Go hello world is bigger than all of SQLite while an idiomatic Rust one is on par, and neither's nearly as portable as the current C implementation, one that is both programatically and battle-tested like pretty much nothing else in the world.
This is what he meant by misuse. Properly-used assertions are meant to document and check conditions that were thought to be impossible by the developer. Not just unlikely, or illegal, but impossible. If a condition is possible, and you check it with assert, that's a bug.
Things like this make me not want to use a language. Want to comment a=b; to a=3;//b temporarily? Too bad, either assign b to 3 or comment out b too, and if b was the only variable to make use of c, same for c, and so on. Same obnoxious nonsense as Java not letting unreachable code exist, making me have to comment out the rest of the function body if I want to put in a return in the start to test something, which happens often enough that it's a pain.
That mild inconvenience (which I almost never face while writing Go code after getting accustomed to the language and getting my editor to run goimports on save) has a big RoI in safety and program quality, which are much loftier goals than short term code-writing convenience.
Variables have been called variables since the dawn of time, i.e., the lambda calculus, which doesn't even have assignment. The name derives from the idea that for every invocation of a function, a variable in its definition may be bound to a different value, hence it "varies" at runtime.
"Immutable variables" is frequently used, true, but the official term is "immutable bindings".
let x = 5;
let mut y = 6;
The let statement binds a variable to a value: * x and y are the variables
* 5 and 6 are values
* let does the binding
You can have an immutable binding, like x, or a mutable binding, like y. But most people turn "a bound variable" into "a variable" (or "an immutable variable") and "a mutably bound variable" into a "mutable variable".Rust looks pretty decent but I'm still in the wait and see stage as well.
I found 5 different ways to read a file on Google, and only one of them still worked. Plus I saw the release notes on the newest version that the syntax that worked is now obsolete in favor of a new operator.
In general, unless you know your source is up to date, I'd recommend ignoring the internet at large completely when it comes to Rust APIs and just focusing on the official docs for the release that you're using. They're plenty good enough, though you do have to get used to navigating them.
I can see the latter, but why the former? Package management that has understanding of language dependencies is a huge productivity booster.
> (5) Rust has "immutable variables". Seriously? How can an object be both variable and immutable? I realize this is just an unfortunate choice of terminology and not a fundamental flaw in the language, but I believe details like this need to be worked out before Rust is considered "mature".
A variable/binding is mutable, the object is not. I don't see the problem.
Have you read about panic (https://blog.golang.org/defer-panic-and-recover)? I'm not meaning to assign you homework, as I know you know more about this than I do, I'm just curious what about assert makes it mandatory for you...panic in go does require you to write your own error check (presumably just an if, for assert-like behavior, but you could do more complex error-handling).
All of your other comments are certainly valid reasons to choose C. Though I like cargo, and I suspect C would be well-served by something similar.
Well, I've done C off-and-on, sometimes heavily, since the days of VT-100s and DEC-Writers. Modern C has always been a thing since the 1970's. Its just that the definition of "modern" keeps changing :)
Yes, things are easier. It is possible to use the compiler features to write code that can be compile-time checked better than in the old days. A good IDE can use that to advantage to flag a lot of errors before you even compile. Yes, memory management can be easier. That said, most of the C I do now is for embedded microcontrollers with no OS underneath -- so memory safety and threads are DIY, and fork() is not a thing.
As someone once said to me about 30 years ago, "C doesn't get in your way." Also, with C, I can, if I wish, get extremely fine-grained control over memory layout. So currently I tend to use C on bare metal, and Python 3.5 whenever I can get away with it, with wee, tiny, C extensions to Python where necessary. C still has a place, but since the advent of Python my motto is: "Life is too short for C++".
I've said for many years "Life is too long to write C++ for a living." And I haven't written C++ for many years.
Note that Go does not entirely compete in the same space, and Rust is only starting to gain traction.
I've seen people consider "systems" programming as relating to building web services. Traditionally systems programming would be more kernel level and utilities/daemons.
Rust might be OK to start using for the latter, but its very much not yet where I'd start using it for new things for either of the traditional setup.
I know everyone is gung-ho about rust, but for the traditional systems programming crowd, its very new. Given how much it changes I would NOT want to bet on using it for at least 5 years. You don't switch just because something is better on memory safety. That is a nice to have thing. But changing 40ish years of things isn't something that you do without planning.
Also, in my opinion Rust doesn't go far enough. I'd rather we move to things like Idris where I can prove much more than just memory safety. Rust is basically Ada/Oberon/Modula for the modern age. Nice, but ultimately not the first time this has happened.
Not to misrepresent the work the Rust guys are doing, it is great what they are doing and I have lots of fun dabbling on it.
But new system programming programming languages tend to be adopted when an OS vendor tells devs, either use it or go code elsewhere.
On OSes that have significant market share, devs tend to learn the new language instead of waving it away.
Idris is closer to ocaml in that regard.
I cough may have already tried using idris in a kernel module. (for fun, not for serious)
This is not to say I doubt the ultimate approach of Idris: with dependent types you can do everything Rust is doing, and more. It's just a matter of project focus and ergonomics.
Rust may be changing, but it's almost entirely _additions_. We've been stable for roughly 18 months now.
Don't take any of my criticism too harshly either, I do like and use Rust for side projects. But it is about 3 years away from where I'd consider using it for anything. I'm a bit more conservative than most here seem to be. Funny part is I'm the maverick in using new stuff compared to the people I work with.
Keep on trying to improve things either way!
Obviously, if one defines "systems programming" more strictly than that, and only mean code that directly interacts with hardware or has hard realtime requirements (which also probably means it must interact directly with hardware), then Go does not make sense in that category. But, C has given up a lot of territory over the past several decades. When I first started programming, C was the language you used for writing almost any real application...if you weren't using assembly. That has changed a lot in the intervening years, and almost no one would think to use C for GUI apps today, for example.
Your comment seems to imply that java hasn't been successful for db development. HBase, Cassandra and ElasticSearch would all disagree.
Yes, there are situations where GC is not appropriate. But I think those situations aren't as common as they are made out to be.
HBase and ElasticSearch, and even Cassandra can be seen as somewhat niche today. This doesn't mean they aren't doing something of value; and it doesn't mean using the same platform for OLTP and OLAP is the way of the future; but it does mean the competition in their space is more limited and less indicative. There are ground-up rewrites of Cassandra in non-GC'd languages out there, for what it's worth.
Go seems to be, IMO, taking the things Java is best at and trying to make something better. Basically, how would you build Java today if you could do it again?
Take Modula-3 (1986), change the keywords to lowercase and re-brand it as "Cool Language X".
Still better Go than C.
http://people.inf.ethz.ch/wirth/ProjectOberon/index.html
http://www.astrobe.com/default.htm
http://wiki.osdev.org/Go_Bare_Bones
It just needs someone to port that Oberon code to Go, maybe people will then stop discussing how suitable Go is for systems programming.
Therefore Go is closer to that space than people want to admit. A change of its runtime or compiler would let it do operating systems. Even Java (JX) and Haskell (House) do operating systems. I'm sure one could that is derived from and similar to a language designed for implementing OS's. :)
While Rust might be feasible, Go can't really provide libraries like SQLite: Go's C compatibility story is disappointing and awkward.
I imagine it is easier to hire C programmers than Rust programmers, at the moment, especially in fields like embedded development.
Last time I checked the compiler didn't support bare-metal and resource constrained builds without using crazy hacks. Although the situation may have improved since then.
From a professional standpoint, you might as well be asking if I have infinite money and time. Nothing happens in a vacuum
But ignoring that, I would say, for me, it boils down to:
1. How important is performance? Is this a case where all that really matter is that it works? That I have a decent algorithm? Or am I going to be doing "real" optimization, even if only for a single platform? Even at just the "is my algorithm good?" stage I would lean toward C/C++ for systems work.
2. And this is the important one: Do I need this code to exist and be usable in one year? Five? Ten? Stuff like Rust might be fine for the one year frame, but for even as few as five I am going to want something established that I know will have support. And that means C/C++ for systems development (python, ruby, and js for scripting, and so forth).
There is a lot of great code written in C, and a lot of crappy code written in C. Because C doesn't protect you from yourself, it exacerbates any design flaws your code may have, and makes logical errors ever more insidious. So in this sense, the quality of the C you write is really a reflection of you as a C programmer, not the shortcomings of the language. Maybe you've been badly burned by C in the past, but keep an open mind and understand that C can be beautiful.
Unfortunately, C does get a lot of hate on HN. I suspect it has to do with this site's demographics. Many (not all) of the HN clan seem to be oriented towards / mostly familiar with web based technologies. I suspect that for many who have tried, going from a web dev environment to a C oriented dev environment feels like a robust shock to the system.
I'd also be willing to bet that there's an age bias at play here; C has been around, like, forever. It is certainly not the new hotness. Most (not all) people that I know who enjoy it and are proficient at it, are 40 or older. Much of the web based dev crowd that hang around HN seem to be in their 20s, and as it is a time honored tradition to poo-poo the ideas / methods / tech of the older generation(s), it's not surprising that C doesn't get a lot of love.
Yes, I realize I'm painting with broad strokes here. It'd be interesting to see a survey or three that correlates age ranges and tech used on a day-to-day basis to see if these assumptions or legit. (Anyone got any survey data up their sleeve they'd be willing to share?)
Me personally - I love it all. C, C++, Java, Python, Javascript, Rust, Haskell, Scheme, etc. Making computers do things for you, and for other people, by writing detailed instructions is quite possibly one of the funnest things in the world. Double bonus for getting paid to do it!
One only needs to look at something like the OpenSSL library to see the problem. You really need to hammer the hell out of C code with something like AFL to get at a reasonable majority of bugs - and you could hammer out every last bug one day and then the next day a compiler starts optimizing away your safety checks. This isn't a theoretical problem, this actually happens. Code rot is a very real problem in C++, to a far more massive extent than any other language.
http://blog.llvm.org/2011/05/what-every-c-programmer-should-...
http://www.kb.cert.org/vuls/id/162289
Personal opinion here, but with few exceptions C/C++ are inappropriate languages for starting new development at this point. I realize the tooling is not there yet but I would rather see something like Rust used in almost all performance-sensitive applications where C/C++ are currently used. Unless you can guarantee that you are operating in a trusted environment and will only ever operate on trusted data, C/C++ is just not the right language for the job.
Yes, it's fast, but at what cost? I would gladly give up a massive fraction of my performance for better security and portability - and that's why I program Java. Not that Java is perfect either, but at least I can be certain that the sands aren't shifting out underneath my programs.
I would actually say that porting the Linux kernel to Rust would be very high on my wish-list at this point. I am well aware of just how enormous that task would be and I might as well wish for a pony too, but it gives me heartburn to think of just how much C code is sitting there operating in the most untrusted of environments on the most untrusted of data. I have every faith in the kernel guys to do it right, but the reality is there is a lot of attack surface there and it's really easy to make a mistake in C/C++. It may not even be a mistake today, only when the compiler gets a little more clever.
While I agree with the sentiment, a problem with Java is that you're dependent on a runtime environment with a fairly consistent history of vulnerabilities, right? [0][1]
> Personal opinion here, but with few exceptions C/C++ are inappropriate languages for starting new development at this point.
Maybe, but now there's SaferCPlusPlus [2]. At least it may be a practical option for improving memory safety in many existing code bases.
[0] http://www.cvedetails.com/product/19117/Oracle-JRE.html?vend...
[1] http://www.cvedetails.com/product/1526/SUN-JRE.html?vendor_i...
[2] shameless plug: https://github.com/duneroadrunner/SaferCPlusPlus
You simply can't just write 'C' without making sure all the details that are necessary to run safely are in scope at all times.
While I agree - the OpenSSL cases certainly show the weakness of the language, there's just no way I'm gonna hang all that on 'C'. Writing protocols and protocol drivers is a fairly tedious sort of skill to attain. We inevitably descend into a counterfactual ... "fantasy" ( sorry; don't mean anything insulting by that - besides I do it too - it is just the nature of counterfactuals ) in which 'C' ends up the villain, when it was a much richer set of failures in play.
Yep. Have a look at the code coming from the OpenBSD crowd. Those folks really know how to wield C. It involves, first and foremost, writing readable and straightforward code, in an attempt to make any bugs obvious. The OpenBSD folks also insist on code review, which also helps.
And wrt tooling: C has some of the best tooling around of any language. GCC, Clang, and Visual C++ can all do some pretty decent static analysis, and then there are tools like lint and Frama-C, and tools like valgrind. Coverity also offers free static analysis for open-source projects. Make use of all the tools available to you. Testing is also important. Shoot for 100% code coverage (see SQLite3, for example, which has a massive test suite).
As you say, one of the requirements is to pay attention to warnings and fix them. In compiler parlance, "error" means "I can't compile this code" while "warning" means "I can compile it, but it's going to misbehave at runtime".
And here's something about undefined behavior: it's possible to know which behavior is undefined and to avoid it! Not every C program is riddled with undefined behavior.
> I don't think anyone can demonstrate that it is virtually impossible to write 100% safe C code.
I think most people come at the other way. Most people are aware that they are fallible and wants tools to help with that. Most people strive for perfection and none will ever actually attain it.
> I don't think anyone can demonstrate that it is virtually impossible to discover errors safely in C code.
There is a huge difference simply moving from C to C++ with exceptions. The type system in C++ can detect several classes of errors at compile time and prevent then grom going into the results.
Then for runtime problems if an underlying functions throws, it cannot simply be ignored. Any programmer can miss a single statement, or worse refactor a function with a void return to one that returns and error code (which then results in every caller ignoring the return value). However, it takes a special kind of malice to use something like carelessly catch(...) in C++ to disregard exceptions so that runtime errors are avoided. C++ with exceptions has more sane defaults because it fails fast and the failing itself doesn't need tests until it starts doing something meaningful.
Now imagine the advances in error detection moving to languages that catch additional classes of errors.
1. The team is average or below since they're affordable or the work kind of sucks. This often happens in practice even with smart coders because the deadlines force them to move too fast with too little QA. Product might still have high impact, though, esp if it's widely-used product or service. The language itself preventing common problems is helpful here.
2. It's a FOSS project made by people that want to get stuff done without learning tons of rules for working around C's issues or stopping every common operation to prevent language itself from killing their project. I'd say vast majority of projects don't need whatever absolute advantages like max performance that C has over safer languages. Again, the language could be helpful.
3. Either of the above given the effects of time where new contributions come in that work against a codebase that fewer and fewer people understand due to organic growth. The language itself can be helpful with a combo of type-safety, programming in the large support, modules, etc. Better support for safer modifications of stuff you barely understand. Rarely a problem for Ada and Eiffel people if the team was even half-competent because the compiler simply demands it.
There's embedded people that can do one-off or steady projects however they like with enough time and tooling to get it right. ArkyBeagle appears to be in a category like that if my broken memory isn't fooling me. Then, there's vast majority of programmers either in the corporate crunch, scratching an itch barely caring, or fixing something they barely understand. Human nature will push defects in from all these directions. The tooling, if designed with human nature in mind, can prevent a lot of them automatically and aid efforts to catch the rest.
Hence, my opposing C language in favor of safer-by-default system languages. Especially those that avoid tedium of constantly watching out for dangers of most-common operations. Gotta work with human nature rather than against it. A hard lesson I learned after years of failed evangelism of high-assurance INFOSEC. Now, I exclusively look for ways to embed it seemlessly into stuff with other benefits listed. Much better responses on that. :)
http://arstechnica.com/security/2016/09/linux-kernel-securit...
As someone who went the "other direction" (Java -> Ruby -> Javascript) I can say that a lot of it has to do with the accessibility of the ecosystem rather than the language itself. This could absolutely just be my filter bubble, but I've noticed that the communities surrounding Ruby, Python, and Javascript seem to go above and beyond the call of duty when it comes to making libraries easy to use, documenting those libraries, building and refining the tools, and so on.
I know there are good tools out there for C development. I know there are good learning materials. I know there are communities out there dedicated to writing good C code (Shout-out to /r/c_programming on Reddit. Love those folks.) But I can't sort out the signal from the noise, because there isn't a lot of discussion about C programming happening in the online spaces I'm familiar with. As a counterexample, there was a _fantastic_ article on here the other day about "writing your own syscall" in Linux. Yes, it contains a lot of hand-holding and overexplanation, but that's useful for me because I haven't built up the mental model to parse a more terse explanation.
In fact, I think this is how having "the new hotness" change every couple years has been helpful _in some respects_- there's an incentive for lots of people to write blog posts, tutorials, and articles about how to properly use the latest and greatest tech, there's active development going on as people forward-port functionality (and therefore plenty of opportunity for devs to make meaningful contributions and have meaningful discussion about "how to write code using this language/library/framework"). For a short period, both the "old hands" and the newbies are in the same boat, and this is unbelievably useful for training up the next generation of developers.
> Me personally - I love it all. C, C++, Java, Python, Javascript, Rust, Haskell, Scheme, etc. Making computers do things for you, and for other people, by writing detailed instructions is quite possibly one of the funnest things in the world. Double bonus for getting paid to do it!
Same here, friend. :) For what it's worth, I wish there were more of this attitude floating around the Internet.
Personally I'm in my mid-20s and quite enjoy working in C. And for things like bit manipulation it's much easier than in higher level languages. I suspect at some point even the smallest MCUs will be able to run Rust or Go, but until that happens there is still a place for C/C++. Haters can hate but that won't change the fact that C is still the most widely supported language for embedded platforms (and Linux, the other elephant in the room).
It's been done at least once before for ideological reasons (and in C none-the-less) by the FSF. It should be even easier to give it a go in modern languages. I bet you can even get funding if you can write a compelling case that the wheel is actually broken!!!
When developing on one platform for an extended period of time, it is human nature to forget which features are implementation defined as you use them day after day and then have unexpected errors/flaws when porting.
Most people who complain about the dangers of C probably have used it in an unprofessional setting without any additional tooling. It's a bit like saying that all RWD cars are dangerous just because you've once driven a '92 BMW, disregarding any technological advancements since.
There was no direct cost to me because I was getting paid to learn this stuff on the job.
Most of these tools require support from an operating system. This is not the case when you do kernel programming. For some reason even existing tools are not popular among kernel programmers [0].
IMO, there are bugs that can be caught well a compile time without my effort, so why should I waste time on catching them at runtime?
I would better make love to compiler instead of having sex with debugger.
That depends on the compiler. It's not true with GCC: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=18501
>it is human nature to forget
High-performance programming is a job the average human does not do. A professional programmer should use spaced-repetition technology to rise above human nature, and use tools like valgrind for extra safety.
Somebody mentioned "scripting languages" - use the ability of scripting languages to construct combinators to write your tests. They migth even emit 'C' code.
This is a hilariously bad attitude for any software that other people will use. When software crashes, people lose work and time. When software has vulnerabilities, bad guys take advantage of them and build stronger botnets. "The danger" isn't like wiping out when you're pulling a stunt; "the danger" is wasting the good guys' time and empowering bad guys.
>There is a lot of great code written in C, and a lot of crappy code written in C
This is true of any mainstream language, so it's completely uninformative and pre-emptively shuts down the possibility of any meaningful language criticism.
Computers aren't just tools, they're also toys. People use computers for entertainment in varied and sundry ways. What is so wrong with somebody wanting to enjoy hacking around in the low level guts of a system? As long as no lives or livelihoods are at stake, what's the problem?
Can't you substitute "C" with just about anything in this sentence?
It's all well and good to talk about how "beautiful" a language is, but when people are literally endangered because of totally preventable security vulnerabilities that don't happen in programs written in other languages, it's hard to sway me as to how important this so-called "beauty" is.
(Note: I'm playing devil's advocate here to some extent. My view is that safety is important, but lack of provable safety is not some terrible Demogorgon that we should hide in fear from. I think a lot of the concern over safety is valid, but in some contexts it's just overhyped.)
I understand that this is... obscure for some reason and I'm not saying it never happens, but let's be realistic....
Many of us were enjoying the danger of getting low level with Think/Quick/Turbo Pascal and Modula-2.
(not to say that Turbo Pascal was the only one to do that, just fond memories...)
If that was the way it was done in TP, the BBC micro (which was mentioned quite a bit in the recent HN thread about BASICs on personal computers of earlier years), also had a similar feature. I did use that one a bit. You had to open a square bracket in the middle of your BASIC program (though probably not in the middle of a BASIC statement), write your assembly code (6502 instruction set), and then close the square bracket, IIRC.
D (language) these days also has the ability to mix in assembly, though I haven't tried it yet.
Edited for typos and wording.
It could be just a block, a complete procedure/function with or without prolog.
Also it was quite comfortable to write, just as a plain macro Assembler with Intel syntax, not those asm functions with strange syntax used in gcc/clang.
We considered having all the assembler in separate files better practice...
That's not true. BCPL language was specifically designed to get something to compile on a machine with no resources to run safety checks, programming in the large, anything. C tweaked it a bit to run on a PDP-7 and then PDP-11. The problems that are in C are there specifically due to the challenges of non-language experts getting a relic of a language to run on machines with no resources.
Later on, Wirth designed Modula-2 that was safe-by-default where possible (eg overflow checks), low-level, easy to read, faster to compile, allowed integrated assembler, and so on compiled also through a PDP-11. They did whole OS's in languages like that. There were numerous languages like that with similar performance to C but way safer and easier to extend. Then there's languages like SPARK 2014 that let you write code that it automatically verifies free of common errors. As in, they can't happen under any circumstances in such apps rather than whatever you thought of during testing.
Having seen those and knowing C's history (i.e. justifications), a number of us know its problems aren't necessary, are easy to avoid with better design, and you still get most of its benefits. Worst case scenario is wanting that but also wanting benefits of C compilers that received a ton of optimization work over the decades or its libraries. In that case, the better language can generate C code as a side effect and/or use a FFI with interface checks added. Still safer-by-default than programming C by hand.
Heck, there's even typed, assembly languages these days you can prove stuff about. Also work like verification of LLVM's intermediate code. So, even for low-level performance or something, C still isn't either the lowest, safest level you can get. It's just the effect of inertia of years of doing stuff with it as a side effect of UNIX's popularity and tons of code that would have to be ported. Again, you can use that without even coding in C at all past some wrappers. So, people liking safety & simplicity of Modula-2, Component Pascal, etc prefer to avoid it since we know the problems aren't necessary at all. Some want extra benefits of stuff like SPARK or Rust, too.
Truth told: system programmers either never need C or need it so few times it's almost totally unnecessary. Others don't need it at all. So, it's "not necessary for vast majority of things application or system developers work on." ;)
I remember reading some of ID's engine code and admiring how well I could follow it and know what's going on. With C++ and other OO languages, it's much harder.
Don't get me wrong, I'm not going to write my next web app in C, and there's some obvious benefits to the features that C++ offers, but C++ ain't beautiful.
Really like that I can create a class that stores all the knowledge of one concept internally and if I wrote that correctly I never need to look inside it again. Even better, if I document the contracts of using a class I can carefully optimize it and have broad performance effects with small code changes.
Things like std::string are just so much easier to work with that their C counterpart and things like std::filesystem::path simplify (or use the boost one if you don't have C++17 yet) so many things and doesn't even have a C counterpart. I point these out as simple examples but in all the code bases I have worked on there are similar examples, like most games a class to represent 2d and 3d points, which are used to define AxisAlignedBoundingBoxes, which are needed for collision detection algorithms which them selves need several classes to describe.
Then I can build systems of a size and complexity I literally could not comprehend without those abstractions. And then the compiler enforces them for me so other people can use them safely as well. Why is it so much harder to do this in C if C is so much "simpler".
The result is that you really need to stick to a subset of the language that has been chosen to work well together. Safely adding to that chosen subset is challenging. And it just takes one developer to create a major headache.
See, for example, https://google.github.io/styleguide/cppguide.html#Exceptions documenting why Google is not willing to allow even something as basic as exceptions to be used in C++ code.
It is particularly easy to make this hidden magic in C. For example, there is no way to express who has ownership of a pointer or when a function expects a pointer or an array. I have seen plenty of C libraries that document things like this, but for each that does there are 3 that don't. For each one does document things like that, they do it differently with different conventions but for the same reasons, but I cannot even use common idioms to be safe I must understand every part of each library I call. It is much more clear what is going own in C++ when a function accepts or returns a std::unique_ptr and I cannot screw it up without trying hard.
What seems more important to me is exposing the relevant parts of the software when needed. If I care about the business logic (Even if its nots a business app... HP and Mana can be considered the business logic of a game) it can be hard to tease that out when lots of "hidden magic" is shoved in my face. But when I need to handle a new file format that the business logic requires indirectly I don't want to mess with the business logic. Having more tools to cleanly express this helps. So have a type that handles this IO while some other type handles business logic is indeed changing magic into "hidden magic", but it is also enforcing separation of responsibilities. Something much harder to do when your only real tool of abstraction if functions.
The only people I have met in real life that stick to anything like the "hidden magic" argument are the same people who advocate for single large functions. These people like their function on the order of hundreds or thousands of lines so they can "see everything". You aren't doing that are you?
Npte though that id hasn't really created an influential game in 15 years and arguably the game of the century (Minecraft) was programmed, badly from what I hear, in Java. People often say that "you can write good C code" without considering what you're giving up in terms of architecture and creativity.
I did find it useful to apply rules like only use uint32_t, double & bool as primitives.
My main wish is that it would be possible to opt into automatic const & restrict, as a compiler flag or pragma, so that something like
https://github.com/maedoc/sddekit/blob/master/doc/C.md#alias...
would be easier to do.
The use of goto and similar jumps in programming languages has been subject to intensive debate, starting from an article by Dijkstra [1968]. Still today you will find people that seriously object code as it is given here, but let us try to be pragmatic about that: code with or without goto can be ugly and hard to follow.
void* foo() {
int handle = get_some_handle();
if (handle < 0) {
goto fail;
}
void* something = some_function(handle);
if (something == NULL) {
goto free_handle;
}
void* something_else = some_other_function(something);
if (something_else == NULL) {
goto free_something;
}
return something_else;
free_something:
free_something(something);
free_handle:
free_handle(handle);
fail:
return NULL;
}
I've seen this pattern frequently in the Linux source code. I think this is an example of a case where usage of goto improves readability and reduces errors.The goto has gotten a bad rap over the years because of Dijkstra's paper. And that paper has unduly influenced a lot of incorrect thinking. There are valid use cases for goto, and this is certainly one of them.
I use it all the time like the example above. Particularly because it makes my life so much easier when developing and debugging embedded C code across various tool chains, some of which have less functionality than others.
[edit - correct typo on Ed's name]
Again, not criticizing, genuinely want to know.
So specifically in the example above, if you called failure-handling functions instead of using goto's then when the function returned you would continue execution on the next line after the function call. In the example above, that's clearly not what you want.
Now you could add some else's after the function calls to prevent execution from continuing. i.e. to get to the appropriate step in the free_* sequence at the bottom, but that starts to look messy. So I have to admit (not being a goto-lover), the above example reads very nicely.
It conforms to the "gotos might be okay if they only jump forward" rule of thumb I've heard.
We do things that are harmful all the time, in limited appropriate situations. Cutting into your abdomen is harmful, but a skillfully used surgeon's scalpel can fix a bigger problem. That's not license to go roll around on a pile of jagged, rusty steel scrap. Missing sleep is harmful, but if you do it once in a while to keep your job or to escape a nighttime flash flood then it's helpful.
Dijkstra was intending to set the norm from which people should mindfully and occasionally deviate. The point wasn't to ban the use of labelled jumps entirely.
With C++ you can ensure that you have your destructors do the tidy up, e.g. a messy example
struct cleaner { cleaner(string *toCleanup) : m_x(toCleanup) { } ~cleaner() { delete m_x; m_x = nullptr; } };
FILE* f = fopen(...);
SCOPE_GUARD({ fclose(f); });
This is mainly used for one-off calls to some native API, where writing a proper RAII wrapper for the managed resource is not worth it."error(handle, "could not open handle", free_something)"
Most code I saw while teaching it were not good cases, but sometimes it's a very interesting technique that can make the code easier to understand and shorter. I think that use is beautiful.
But it doesn't mean there aren't bad use cases.
Regardless, yes, a ton of people do bash on it because of Dijkstra - but at the time, he had a good point. In the industry of the time (as I understand it - I was a kid when he wrote that), there was a lot of "cowboy coding" out there, with goto's "gone wild" - jumping into the middle of everywhere and everything - and producing "spaghetti code".
But as you note, it can be very useful and make things easier to read (for instance, jumping out of deeply nested if-then constructs - though I could also argue a refactor might be the better solution).
I was once part of a discussion in a forum about state machines, and one guy posted a very beautifully done state machine that used no select-case construct, but rather goto statements, but done in a tight way that mimic'ed a select-case construct. I was very impressed at the time; it was some code for PIC Basic IIRC.
In time, I've changed my views from seeing goto as "always bad", to "can be very useful, in some situations - provided you know the risks of the tool".
In other words - think very carefully before you rush into using it; maybe there's a better or cleaner way.
int main() {
goto g;
std::string s;
g: return 0;
}
while this will compile, but destructor is guaranteed to be called: int main() {
{
std::string s;
goto g;
return 1;
}
g: return 0;
}
Now, longjmp is another matter. That thing is basically verboten in any sane C++ environment (and consequently, C libraries that use it across API boundary are very painful to use from C++; R hosting API is a great example of that).But instead of a measured response to make it the last tool you reach for, it's been made into a pariah.
I'm sorry, you cannot use this keyword as it will cause a conflict with Javascript tools.
I wrote a little about a very similar case with Objective-C and the default 'id' argument and return type here: http://blog.metaobject.com/2014/03/cargo-cult-typing-or-obje...
Old ones like Ada and Fortran of course.
There are newcomers like Rust and Go. Are their C api's mature and portable?
Rusts C api is completely mature and portable. In fact if you are writing a library that you want to have a C interface to, Rust is a fantastic choice.
https://blog.heroku.com/see_python_see_python_go_go_python_g...
Modula-2, FreePascal, D
A really, really good one is Advanced Programming in the Unix Environment. But, it's pretty expensive.
K&R is a great resource which covers a lot beyond the syntax, but is obviously dated from the standard's perspective.
Any suggestions on what I should apply C to as a way to learn it?
Just be aware, microcontroller C programming is pretty far out compared to regular systems programming. Lots of tasks involve writing bits to seemingly random memory-mapped registers to change the state of the controller... and forget about including your favorite libraries. You're lucky if the standard library fits on the chip. Its very similar to OS kernel development in that regard.
If you are into hardware as well, get microcontrollers and do some home improvement projects. The easiest way is to get Raspberry Pi or similar, but if you also want to learn a great deal about system programming, try to bring up MCU on your own, starting with bootloader.
I learned C by implementing my own versions of popular unix commands, starting with echo then cat and so on...
Does it advocate good best practices?
Does it talk about pitfalls?
Does it overemphasize new, possibly less widely implemented, features?
Does it do/not do anything else we should know about?
That may come down to opinion. For example, type qualifiers are bound to the left.
Traditionally you would write:
char *var;
They advocate keeping type on the left, name on the right, so: char* var;
A few things like that are covered under "Warning to experienced C programmers".Personally, I prefer it, but have always done what everyone else expects, so there are no fights over styling.
> Does it talk about pitfalls?
Absolutely.
At a glance over, they talk about the unexpected way C treats truthy values (if it ain't 0, it's true), accidentally dereferencing to NULL, and even goes into goto, when it's good, and when it's bad.
> Does it overemphasize new, possibly less widely implemented, features?
Yes. They assume a C11 compiler, and state it in the introduction. At the moment, GCC and clang have some disagreements with how some C11 features should be treated, (GCC accepts a char or a char* for _Generic, clang requires it to be char. clang is more technically correct, but GCC is more flexible), and MSVC is still struggling to implement most of it. [0]
> Does it do/not do anything else we should know about?
I probably need a week to read it more fully, but I'll quote from the end of the introduction:
> Last but not least comes ambition . It discusses my personal ideas for a future development of C. C as it is today has some rough edges and particularities that only have historical justification. I propose possible paths to improve on the lack of general constants, to simplify the memory model, and more generally to improve the modularity of the language. This level is clearly much more specialized than the others, most C programmers can probably live without it, but the curious ones among you could perhaps take up some of the ideas.
[0] https://msdn.microsoft.com/en-us/library/hh567368.aspx
Edit: escaping
char* var1, var2;
Traditional style makes this clear.Good idea in theory but your example shows how bad it behaves in practice.
char* var1;
char var2;
char var2, *var1;
is valid C while in Java this would be illegal char var2, [] var1;
So the syntax is correct and all you are doing is to add style rules that make it less readable. typedef char * pchar;
pchar var1, var2; // Now they're both pointers
I'm not really a fan, but it can address the problem.char var1[10];
and not
char[10] var1;
The rule is simple once you understand it, variables are declared with the same syntax that is used to access it later.
On the 'wow' side, had no idea there was a _Generic macro. Pretty cool.
If he/she knows these concepts well, that means he/she have invested much time, and probably know other things well enough (or can learn them easily).
What you really want is an understanding of how whatever code the candidate may write will map to the underlying hardware. Test for that.
int a;
int b;
ptrdiff_t d = &b - &a;
is undefined. Modern computers, including most embedded platforms, have a flat memory model by now, and could implement the operation above without problem.Another difference I know of is signed integer overflow. Most platforms use a 2's complement architecture, where signed overflow simply wraps around. In C, such an operation is undefined.
Yet another difference relates to pointer aliasing. On most platform, a pointer is just a pointer to a memory slot. In C, it is assumed for optimization purposes that pointers of different types cannot point to overlapping regions. This prevents practical stuff like type punning, for which you have to use unions.
There's no point in testing for C knowledge if they're never, ever going to use it. Sure, they may know some C, but they're not going to have more than a surface, I-recognize-it-when-I-see-it understanding of it.
The author's use of register to avoid aliasing is something I hadn't heard before and seems like a good idea in some cases.
Beyond the learning C aspects, I really hope that some of the author's suggestions for language extensions are implemented.
IMO, the title here is misleading, I don't think new feature is added to C to make it modern.
"This book is organized in levels. The starting level, encounter, will introduce you to the very basics of programming with C. By the end of it, even if you don’t have much experience in programming, you should be able to understand the structure of simple programs and start writing your own.
The acquaintance level details most principal concepts and features such as control structures, data types, operators and functions. It should give you a deeper understanding of the things that are going on when you run your programs. This knowledge should be sufficient for an introductory course in algorithms and other work at that level, with the notable caveat that pointers aren’t fully introduced yet at this level.
The cognition level goes to the heart of the C language. It fully explains pointers, familiarizes you with C’s memory model, and allows you to understand most of C’s library interface. Completing this level should enable you to write C code professionally, it therefore begins with an essential discussion about the writing and organization of C programs. I personally would expect anybody who graduated from an engineering school with a major related to computer science or programming in C to master this level. Don’t be satisfied with less. The experience level then goes into detail in specific topics, such as performance, reentrancy, atomicity, threads and type generic programming. These are probably best discovered as you go, that is when you encounter them in the real world. Nevertheless, as a whole they are necessary to round off the picture and to provide you with full expertise in C. Anybody with some years of professional programming in C or who heads a software project that uses C as its main programming language should master this level.
Last but not least comes ambition. It discusses my personal ideas for a future development of C. C as it is today has some rough edges and particularities that only have historical justification. I propose possible paths to improve on the lack of general constants, to simplify the memory model, and more generally to improve the modularity of the language. This level is clearly much more specialized than the others, most C programmers can probably live without it, but the curious ones among you could perhaps take up some of the ideas."
While C11 is indeed to some degree a polishing of C99, its theme is multi-threading.
Is it possible for C to be a standalone skill, where ones job could be 100% programming in C, or do you need a lot of auxiliary knowledge outside of that?
Yes. Systems programming and Embedded are your best and most visible playing fields, but many large, legacy applications were written in C and continue to be maintained.
> Is it possible for C to be a standalone skill,
No. As others have pointed out, the language + standard library is very spartan and will only take you so far. This will only get you an entry level position, and only in teams that are big enough to have some senior people with spare capacity for mentoring, and a stream of small, self contained tasks for you do while in training.
To be able to work independently you need to have at least some basic knowledge of the whole toolchain: Compiling (you need to know to heart the different steps that are taken by the compiler, and at least its 20% most common cmdline flags), Building (make), program analysis (lint, valgrind), debugging (gdb, or whatever comes with the compiler you are using), 3rd-Party-Libraries (pick 2-3 of: glib, pthreads, antlr, curses, openssl, etc), Standards (MISRA, POSIX - which is at least as much about the API to Unix-like OS as it's about the C language).
From there, there are more tools to help you, but those are typically OS dependent and are not exclusive to C.
C doesn't exist in a vacuum: it has to run on something. And the standard library doesn't get you very far.
You'd need to know at least one OS API as well: POSIX is probably best, maybe Win32, an embedded OS/executive might work also, or perhaps even a bare-metal CPU or two.
C + POSIX covers a lot of the Open Source world, and increasingly more of the embedded market. C + VxWorks is possibly the next best combo.
I personally know Java, (lil bit) Elixir, and Python.
EDIT: I'll also be reading K&R along side it.
If you like it, great, if you don't, you have company[2]
[0] https://learncodethehardway.org/c/
[1] https://web.archive.org/web/20141205223016/http://c.learncod...
If you are in for cutting a tree, go for it.
I personally judge a language by how well it lets you to define abstractions. In C's case, it doesn't let you do that very well.
HN is probably something like 99% web and app developers (I would argue that all are the former, but my definition may be a bit old). In those cases, you actively don't want to ever use C and anyone who does is kind of an idiot. It is the logic by which "systems project" means "script to run on a server" for a lot of people around here.
And that is fine. But just like this place's obsession with Rust would lead to derisive mockery by "real" systems people, so too is C hated here.
It also doesn't help that universities, at least in the US, seem hellbent on teaching along these lines. A few years ago Eclipse was "the thing you use when you are poor or doing Java". These days? I am seeing disturbing numbers of graduates who refuse to use Visual Studio for a project on Windows and insist on boostrapping together something that is only half functional. And talking to my academia friends (and even doing a fair amount of guest lecturing for them), I know where the indoctrination is coming from.
On the other hand, where the language gets promoted, I haven't seen anyone really promote C in a way that would sound modern in any sense, where people who have years of experience with C stick with C89 or use C99 in a C++ compatible fashion (i.e. without any C99 syntax which C++ has not adopted officially like restrict keyword or designated initializers). While that is fine for personal preferences, I think it does a disservice to people who have to learn the language and use the language for various justified reasons of their own and there isn't a unified response in how to really teach C to them.
I think the author really hits the hammer on the head with this part in the introduction.
"In contrast to the ubiquitous presence of C programs and systems, good knowledge of and about C is much more scarce. Even experienced C programmers often appear to be stuck in some degree of self-inflicted ignorance about the modern evolution of the C language. A likely reason for this is that C is seen as an "easy to learn" language, allowing a programmer with little experience to quickly write or copy snippets of code that at least appear to do what it’s supposed to. In a way, C fails to motivate its users to climb to higher levels of knowledge."
But on this book, from what I have read from past revisions, it's very well written and I have even learned some things from it I didn't know existed in C, like using the keyword static in array indices in parameter declarations. While it is not a perfect resource and I don't think anyone new to programming would be able to read this without guidance, it does some things extraordinarily well which I haven't really seen other C books touch on. Its treatment of undefined behavior is top notch and the way that it tries to explain the memory model of C is pretty good as well.
But had this book been written for another "antique" language, like Modern Fotran or Modern Cobol (which both had their last ISO standards most recently in 2008 and 2014 respectively, mind you), I doubt there would be this much polarization in the comments section.
Sniper rifles are improving every year. Yet a rifle from the 40's can kill people too. Both deadly in the hands of an expert and dangerous in the hands of an amateur. It's the same with C. You should be able to understand the computer on that level but you don't need C just to prove it.
This CPU power galore is just a short lived period before people want full phone VR on a battery for two hours or what have you. You can sort of see what's in demand now for $200k jobs and the tools there don't appear to be very high level or user friendly. If anything they all require few heavy courses in statistics.
It was already like that 10 years before C was invented, but their authors didn't want to invest too much resources creating an Extended Algol, PL/I or similar compiler.
ESPOL and NEWP were already available in the 60's.
We both know that is not always true and heavily depends on the type of software though.. Take https://github.com/micropython/micropython for instance: what higher level language but C would allow that to run with the same performance as it has now, on as much different devices? C++ would be a viable answer, but when used e.g. as C + templates it's basically still a form of C and anyway your complaints have been raised in nearly the exact same wordings about C++ laguage as well..
The availability of compilers for a specific architecture is orthogonal to the language.
It could have been written in any other language that compiled to native code, if we had more options available.
"Better tools exist to do this job"
"C is not needed anymore" (Yet no contender has ever come close to it, hehehe --my2c)
There, saved you a ton of reading time.
Those who use C and assembly I imagine would be better equipped to understand the new paradigms. It's best to understand how to implement data structures in their most rudimentary form because implementing them on new platforms becomes easier.
In addition, higher-level idioms become easy to understand if the parts that make up the whole are understood. And underneath all those layers of translation and compilation we have raw assembly and the bare machine.
I also believe efforts like LLVM are actively trying to 'bridge the gap' between both worlds (totally raw VS fully dynamic/scripted). Stuff like emscripten is enabling the "old farts" and the "newfags" to share common ground, and that's amazing... I just hope these youngsters keep learning stuff instead of just piling framework after framework after the new 'hot shit' gets released in a 6 month timeframe.. really, adhd is in full effect, specially in the webdev world, and imho that's hurtful.
o/
Array out of bounds is dangerous even in Excel VB.