
Teaching C - mpweiher
http://blog.regehr.org/archives/1393
======
robertelder
In my quest to learn C very well over the past few years, I've come to the
conclusion that C is best understood if you think about it in terms of the way
that an assembly language programmer would think about doing things. An
example of this would be if you consider how switch statements work in C.
Switch statements in C don't really compare to switch statements that you find
in other languages (eg.
[https://en.wikipedia.org/wiki/Duff%27s_device](https://en.wikipedia.org/wiki/Duff%27s_device)).

The issue that many students face in learning low level C, is that they don't
learn assembly language programming first anymore, and they come from higher
level languages and move down. Instead of visualizing a Von Neumann machine,
they know only of syntax, and for them the problem of programming comes down
to finding the right magic piece of code online to copy and paste. The idea of
stack frames, heaps, registers, pointers are completely foreign to them, even
though they are fundamentally simple concepts.

~~~
pcwalton
> I've come to the conclusion that C is best understood if you think about it
> in terms of the way that an assembly language programmer would think about
> doing things.

I don't agree. That leads people to incorrect conclusions like "int addition
wraps around on overflow" (mentioned in the article), "null pointer
dereferences are guaranteed to crash the program", "casting from a float
pointer to an int pointer and dereferencing it is OK to do low-level tricks",
and so forth. C is a language that implements _its own_ semantics, not the
semantics of some particular machine. Confusing these two ideas has led to
lots and lots of bugs, many detailed in John's other blog posts.

It might be useful to teach these intuitions to beginning programmers who
_already know_ assembly language before learning C (though are there more than
a vanishingly few number of those anymore?) But teaching assembly language as
part of, or as some sort of prerequisite for, teaching C strikes me as a waste
of time and likely to lead to wrong assumptions that students will eventually
have to unlearn.

~~~
haberman
I agree with both of you to some extent. Despite the fact that C implements
its own semantics, those semantics are downright bizarre and hard to gain
intuition about unless you have some mental model of the machine.

For example, here are some of the things that confused me when I first learned
C:

    
    
        - why isn't there an actual string data type? (just char*)
        - why do some people use "char" to store numbers?
        - whats the deal with pointers?
        - why are pointers and arrays kinda sorta interchangeable?
    

Until I learned how things work at the assembly language level, I could not
gain an intuitive understanding for why C works the way it does.

~~~
bogomipz
Why do some people store chars to store numbers?

~~~
douche
They need a char-sized number, and memory used to be a tightly-budgeted
commodity.

Reminds me of this story of a game developer who magically got their game
under the limit at the 11th hour

[http://www.dodgycoder.net/2012/02/coding-tricks-of-game-
deve...](http://www.dodgycoder.net/2012/02/coding-tricks-of-game-
developers.html)

~~~
Kristine1975
Although char should not be used to store numbers, since its signedness is
implementation-defined (gcc has the switch -funsigned-chars I think to make
them unsigned instead of signed). signed char/unsigned char are ok.

------
dbcurtis
Let me add the perspective of an old dinosaur that learned to program before C
had been invented.

C maps nearly 1:1 onto simple processor and memory models, and most
importantly, gets out of your way and lets you get on with solving your system
programming problems. Before C, just about any meaningful system programming
task required a dive into assembly language. In that context, C was a huge
win. It is also what makes C the langauge of choice for embedded development
today.

Of course, system programming problems are not the bread-and-butter of most
develpers today -- and a good thing, too. We can now build on top of solid
systems and concentrate on delivering value to the customer at much higher
levels of abstraction: the levels of abstraction that are meaningful to
customers.

I dearly love Python because it allows me to work at levels of abstraction
that are meaningful to the user's problem. I dearly love C when I want to
wiggle a pin on an ARM Cortex-M3.

In my mind, CS education should start by teaching problem decomposition and
performance analysis using a language like Python that provides high levels of
abstraction and automated memory management. Then, just like assembly language
was a required CS core course back in my day, students today should spend a
semester implementing and measuring the performance of some of the data
structures that they have been getting "for free" so that they understand
computing at a fundamental level. Some will go on to be systems programmers,
and will spend more time at the C level. Some won't ever look at C again, and
that is OK.

In the end, CS education is about how to solve problems through the
application of mechanical computation. The languages will evolve as our
understanding of the problems evolve and our ability to create computing
infrastructure evolves. CS educcation should be about creating people who can
contribute to (and keep up with) that evolution.

~~~
ryao
> I dearly love Python because it allows me to work at levels of abstraction
> that are meaningful to the user's problem. I dearly love C when I want to
> wiggle a pin on an ARM Cortex-M3

What keeps you from wriggling that pin in Python?

Turing completeness dictates that any Turing complete language is equivalent
to any other Turing complete language. In addition, you can do high levels of
abstraction in C. The preprocessor and void pointers allow you to do some
rather nice things. Structured programming also let you build things up. The
advantage that Python enjoys is that it comes with a large number of library
functions already provided and there are easily discoverable third party
libraries. There are other differences, although every difference has a trade
off. Garbage collection bloats memory requirements. Being interpreted means
errors that can be caught in advance at compile time occur at runtime.

~~~
bo1024
> Turing completeness dictates that any Turing complete language is equivalent
> to any other Turing complete language.

Sorry to pick on you, but this is a good example of misuse of this fact in an
argument where it's not really relevant. Turing-completeness only relates to
functions that take some string as input as produce a string as output (since
that's all the turing machine model can do). In contrast, real-world
programming languages interact with a machine or operating system, and not all
languages provide the same interfaces or even run on the same machines.

I can easily implement a Turing-complete language whose only allowed system
calls are reading from stdin and writing to stdout. Despite being Turing-
complete, it will never be able to spawn threads, connect to a network socket,
or even allocate memory on the heap.

------
parr0t
I'm currently at uni studying CS and recently finished my 'Programming in C'
unit. The teacher from the get-go said it would be challenging compared to
other languages that we had used to date (mainly Java) and that quite a few
students struggle with it. Once I got my head around pointers and debugging
through GBD/Valgrind the unit came immensely enjoyable and rewarding.

We didn't use any fancy IDE's and were told to stick to VIM, we also had to
compile with the flags -ansi -Wall -pedantic which alerted you to not only
errors but warnings when we compiled our code if it didn't meet the C90 (I
think) standards. It was a lot of work crammed into 13 weeks but it had one
assignment which I thoroughly enjoyed.

Tic Tac Toe (Ramming home using pointers, 2D arrays, bubble sort for the
Scoreboard).

Debugging a bug-riddled program (My favourite).

Word Sorter (Using dynamic memory structures, memory management by having no
leaks, etc).

The debugging one was very different from most other assignments I had done at
uni to date and the teacher said he recently introduced this assignment
because the university had received feedback that students debugging skills
weren't the greatest. They could write what they were asked to just fine, but
when it came to debugging preexisting issues quite a few struggled. We got
given a program with around 15 bugs and you got marks depending on what was
causing the bug and a valid solution to fix it. This forced us to use tools
such as GBD and Valgrind to step through the program and see where the issue
was and to be much more methodical.

I really enjoyed C and when I find a bit of time outside of work and study I'd
like to explore it more.

------
fisherjeff
A long-standing gripe of mine: When I clicked through to his example "cute
little function" in Musl, I found myself mentally adding comments to work
through all the "cuteness". If that's the case, IMO, it's either _too_ cute or
needs more comments - not sure how much time I've spent picking apart kernel
code just to figure out what the hell some of it does, but it's definitely not
time well spent.

EDIT: Meant to add: fantastic article, wish my Intro to C instructor had read
it...

~~~
jakobegger
I spent about twenty minutes looking at this function trying to figure out
what it does. It looks like it uses a neat trick to compare native integers
instead of bytes for speed, but I just couldn't figure out what it does. Then
I realised it's part of the standard library and "man memchr" told me what it
does...

~~~
fisherjeff
I mean, it's definitely a neat way to optimize comparisons for the CPU's word
size. The problem is with completely unexplained things like:

    
    
      #define ONES ((size_t)-1/UCHAR_MAX)
    

Reading through the loop, it seems as though it will create, e.g., 0x01010101
for a 32-bit machine with 8-bit bytes. And sure enough, if you calculate
((2^32)-1)/255 that's exactly what you get. But I never would've known that
without going through the code and proving to myself that the definitely of
ONES actually makes sense.

If you write code like this and there are never any bugs then fine, I guess.
But there will bugs.

~~~
Kristine1975
The whole ONES... HASZERO shenanigans is somewhat explained here:
[https://graphics.stanford.edu/~seander/bithacks.html#ZeroInW...](https://graphics.stanford.edu/~seander/bithacks.html#ZeroInWord)
as well as the entry after it.

Edit: And most likely in the excellent book "Hacker's Delight" as well.

------
fdej
The core of what makes C elegant is that basically everything that looks
atomic is atomic, in the sense of taking O(1) space and memory (at least until
C99 introduced its abominable variable-length arrays, and perhaps some other
features I'm forgetting about).

Absent macro obfuscation, it is easy to reason about what a snippet of C does
and how it translates down to machine code, even taken out of context. In C++,
something as innocent as "i++;" could allocate heap memory and do file I/O.

The downside is that C code can become quite verbose, and to do anything
useful, it takes a lot of ground work to basically set up your own DSL of
utility functions and data structures. For certain applications, this is an
acceptable tradeoff and gives a great deal of flexibility. I think teaching
this bottom-up approach to programming can be quite useful - in a way, it
mirrors the SICP approach, albeit from a rather different angle.

The question is, why are there not more languages that have the same paradigm,
but also add basic memory safety, avoid spurious undefined behavior, provide
namespaces, with a non-stupid standard library, etc.?

~~~
nickpsecurity
Building on pjmp's comment, this quote...

"The question is, why are there not more languages that have the same
paradigm, but also add basic memory safety, avoid spurious undefined behavior,
provide namespaces, with a non-stupid standard library, etc.?"

...basically just described Modula-3. It meets your requirements, was easy to
read, had concurrency, had decent stdlib which had some formal verification,
and could act as low-level as you needed with "UNSAFE" keyword. Brilliant
design given all tradeoffs it balanced. It had some commercial uptake and was
used in CVSup for FreeBSD.

[https://en.wikipedia.org/wiki/Modula-3](https://en.wikipedia.org/wiki/Modula-3)

Note: Important to not ignore it once you see "garbage collection." The GC was
optional with a single keyword determining whether you or it handles a
specific variable. Let's one pick and choose their battles with fate. :)

Note 2: The Obliq distributed programming language was an interesting project
based on Modula-3. The SPIN OS, written in Modula-3, let you link code into a
running kernel in a type-safe and memory-safe way for reducing context
switches for performance.

~~~
sn9
Much of the code in the fantastic _C Interfaces and Implementations_ [0] is
inspired by Modula-3. It's a fantastic book to work through after finishing
K&R for anyone wanting to learn how to write safe and reusable C code.

(Regular HN readers will recognize the author of the top review on Amazon.)

[0] [http://www.amazon.com/Interfaces-Implementations-
Techniques-...](http://www.amazon.com/Interfaces-Implementations-Techniques-
Creating-Reusable/dp/0201498413)

~~~
nickpsecurity
Wow. Interesting to see the two converge that way. That's the glowing review
I've seen of a book in a while. Guess I'm going to have to get it just in
case. :)

------
latenightcoding
Great post! Univiersites tend to teach a very small subset of C, just enough
to make a tic tac toe application or something silly.

I learned C by myself many years ago but it's only until recent I have been
using it for big projects.

Reading Redis' source code was a great aide, xv6 is also amazing to learn
systems programming.

Learn C The Hard Way is also a good read, but not as your main book, since it
goes too fast. Other invaluable resources are: Beej's Guide to Network
Programming and Beej's Guide to Unix Interprocess Communication

A good advanced book is Advanced Programming in the Unix Environment

~~~
satysin
Books I have read so can personally recommend are (in no particular order)

    
    
        Programming in C
        C Primer Plus
        K&R (obviously)
        21st Century C
        Modern C (also mentioned in this post)
        Understanding and Using Pointers in C

~~~
gcr
I loved "Expert C Programming: Deep C Secrets" by Peter Van der Linden. Great
deep dive on the minutiae of important details (the difference between arrays
and pointers as function arguments, etc)

~~~
krylon
I do agree, that book is really great, both very in-depth and at the same time
entertaining to read.

Unfortunately, it appears to have been out of print for a while now.

Another book I can highly recommend is "The New C Standard: A Cultural and
Economic Commentary"
([http://www.knosof.co.uk/cbook/cbook.html](http://www.knosof.co.uk/cbook/cbook.html)).
It takes apart the C language standard (C99) pretty much sentence by sentence,
explains what it means and also contrasts how C99 is similar to or different
from other languages (C++ mostly, but also, say, Fortran or Pascal).

------
nickpsecurity
Although a C opponent, I find this to be a good writeup. I hope more C
students see it. Particularly, the author focuses on introducing students to
exemplar code, libraries with stuff they can study in isolation, and making
habit of using checkers that knock out common problems. This kind of approach
could produce a better baseline of C coder in proprietary or FOSS apps.

Only thing I didn't like was goto chain part. I looked at both examples
thinking one could just use function calls and conditionals without nesting.
My memory loss means I can't be sure as I don't remember C's semantics. Yet,
sure enough, I read the comments on that article to find "Nate" illustrating a
third approach without goto or extreme nesting. Anyone about to implement a
goto chain should look at his examples. Any C coders wanting to chime in on
that or alternatives they think are better... which also avoid goto... feel
free. Also, Joshua Cranmer has a list there of areas he thought justified a
goto. A list of great alternatives to goto for each might be warranted if such
alternatives exist.

Only improvement I could think of right off the bat on the article outside
including lightweight, formal methods like C or stuff like Ivory language
immune to many C problems by design that extract to C. Not saying it's a
substitute for learning proper C so much as useful tools for practitioner that
are often left out. Astre Analyzer and safe subsets of C probably deserve
mention, too, given what defect-reduction they're achieving in safety-critical
embedded sector.

------
acbart
Although this is an interesting post, I'm disappointed from a pedagogical
point of view. The article covers these topics, in this order:

1\. What book do we assign?

2\. What should we lecture?

3\. What sort of code review work should we have students do?

4\. What kind of assignments should we use? But only to say that he won't
cover it in the article!

This is the almost the exact opposite order of what is most useful in terms of
learning. Yes, some people (especially auto-didactic and well-focused
students) are able to learn tremendous amounts on their own through books. But
they are a relatively poor tool for teaching, compared to active learning
methods. Lecture can be great, but usually is passive and worse than useless.

I want to acknowledge the importance of defining what you will teach and what
successful (end-of-course) students look like and how to assess them. After
you've decided that, it is proper to devise assignments and assessments, and
then to decide on lectures and supplemental materials that support students in
completing the assignments and assessments successfully. The time students
spend should be active and practical - not that readings can't be provided,
but they should be on-point and meaningful. Proper application of
Instructional Design principles and theories of learning can make a world of
difference for students.

But kudos for thinking about it, kudos for thinking about feedback mechanisms,
and kudos for

PS: Obviously, I believe C has a great place in the curriculum - shouldn't
leave undergrad without it!

------
s_m_t
I love K&R, 21st century C, Understanding C pointers, Deep C secrets but I
think they are a little complicated for beginners. I wouldn't bother opening
them until you have written a couple of small programs in C or you have good
experience in other languages.

When I first learned C in highschool I got a few books on C which all seemed
to have the word 'Beginner' in the name. 'Absolute Beginners Guide to C' is
one I remember in particular. I think having multiple books is pivotal because
as a beginner if you encounter an explanation that doesn't make sense to you
it is very hard to reason around it. You probably have very little prior
knowledge, almost everything you know and learn up to the point where you get
stuck will be contained in that single book, and if you don't know any other
languages you can't make any connections to help yourself out. The reason the
second, third, or fourth book is so important is that it will have a slightly
different explanation that might make something click in your brain.

------
haberman
I thought this was an excellent post. C has changed in lots of important ways
in the last 10-20 years. The changes are both convenient (far better tooling)
and inconvenient (much less forgiving of undefined behavior). Those of us who
use C professionally have had to pick up most of these changes by osmosis.
This was a really great run-down on how you'd bring a newbie up to speed with
the state of the field.

------
kbenson
'"even what seems like plain stupidity often stems from engineering trade-
offs"'

This has truly become something I try to keep in mind, considering a) I've
later, sometimes long after starting on someone else's code base, learned a
useful rationale for why they did some of the previously more inscrutable
things in their code, and b) ended up writing a few things like that myself.

Documentation is key to understanding these systems, but it isn't sufficient.
Often you are presented with a nicely documented mega-function, which while
anyone can read through, but is very hard to reuse a portion of when needed.
In breaking it apart into smaller chunks, you necessarily scatter some of the
reasoning about why a particular approach was taken from where it was
originally used, or at least where the weird behavior is required. You can
either reproduce large chunks of the documentation at many different points in
the code base, and hope it doesn't get out of date as the systems it describes
in other files is slowly changed, or keep the documentation as fairly strictly
pertaining to the code immediately around it, in which case the knowledge of
how the systems interact can get lost.

Whenever you encounter code that seems to make no sense, it's better to assume
there's some interesting invisible state that you need to grok, than that the
programmer was an imbecile or amateur. The latter may be true, but assuming
that from the beginning rarely leads to a better outcome.

Edit:

I'll share my favorite example of this. At a prior job, we had a heavily used
internal webapp written in Perl circa 1996. It was heavily modified over the
years by multiple people, but by the time I was looking in on it in 2012, it
was a horror story we used to scare new devs. The main WTF was that it was
implemented as one large CGI which eschewed all use of subroutines for labels
and goto statements, of which there were copious amounts. The really confusing
part was that they were used exactly as you would expect a sub to be used,
just with a setting a few variables and a jump instead, so we always scratched
our heads as to the reasoning for this. There was even a comment along the
lines of "I hate to use goto statements, but I don't know a better way to do
this, so we're stuck with this."

Fast forward a couple years, and I'm migrating the webapp to a newer system
and Perl, and I discover the reason for this. At some point it was converted
to be a mod_perl application, and the way mod_perl for Apache works is to take
your entire CGI and wrap it in a subroutine, persist the Perl instance, and
call the subroutine each request. The common problem with this is that because
of this any subroutines within your CGI can easily create closures if they use
global variables. The goto statements really were intended to be used just
like subroutines, because they were likely switched to in an attempt to easily
circumvent this problem. Now, there are better methods to combat this, such as
sticking your subroutines in a module, and having your CGI (and then mod_perl)
just call that module, which is what I ended up converting the code to do, but
the real take-away is that the original decision, as impossible to defend as
it seemed, was actually based in a real-world trade-off, and at the time it
was done may have actually been the correct call.

------
feklar
The Harvard CS50 course on edx does a pretty good job of teaching C, esp if
you do the recommended reading/psets of "hacker level" which is from the book
Hacker's Delight 2.

There is some initial magic, where they have you import cs50.h which is full
of black box functions in the beginning but other than that it's a good
example of teaching beginner C.

------
satysin
If part of a CS course I think C is an excellent first language. Perhaps not
for someone wanting to learn about software development on their own though.

It seems to me that while we know _how_ to teach C properly today not many
places do because they don't do as they say.

~~~
jandrese
One thing C does is force you to think about how the computer actually uses
memory. So many languages abstract away the memory management and you end up
with stuff like java programs that constantly and aggressively thrash the hell
out of the processor cache by creating and (later) destroying objects for
everything they do. Cache misses are one of the biggest performance killers on
modern CPUs. You also see programmers do stuff like add characters to a string
one at a time, even though each addition creates a whole new string and
discards the old one.

If you do stuff like that a lot you can easily end up wondering why your
program is so slow even though the profiler says that there are no standout
slow parts. Everything is just uniformly slow because their coding style
doesn't consider the amount of work each statement maps to at the machine
level.

~~~
_ph_
There are plenty statically compiled languages which don't abstract away the
memory management, and are still way more robust to use than C, just start
looking at Modula-2. Programming in C makes a lot of sense in many situations
compared to C++ or Python or Javascript, just to name a variety, but not so
much when compared to many other languages, which really compete in the same
space as C, but did not catch the same attention.

~~~
jandrese
Learning C has the advantage of opening up a ton of Unix software for you.
Modula-2 not so much.

------
lunchTime42
There is not one C. There are multitutdes of C. C is a recombination of the
Programming language with the Compiler with the Plattform with the Code
Convention of choice with the librarys chosen with the OperatingSystem (if
there is one).

And C needs knowledge in all fields recombined to be really used freely. Know
one of those fields not - and you will be like a wanderer on a frozzen lake,
doomed to trust those who know to guide you by ramming posts of no return
where the ice gets thin.

Its also about taking a sledgehammer to all those certaintys people have about
computers from marketing and personal experience as consumers.

------
orionblastar
I find that many books that teach C either assume the reader knows how to
program, or are too complex for them to understand.

I was going to write a Kindle book in the beginner's guide to C using
Code::Blocks and its IDE because it is FOSS cross platform software. I found
out it is a lot harder than I thought it was.

I learned C in 1987 at a community college still have the book on it that is
written for Microsoft C, and we used Turbo C and Quick C for some of the
assignments. Most of the programs I wrote can still compile and those that get
errors or side effects can be debugged easily.

------
andrewfromx
CS degree from pitt.edu 1996 and C was not required. But a friend an I took it
as an elective. We did not want to get out of school with CS degree and no C.

~~~
aianus
If not C, what did they have you use for systems programming? Like, in your
Operating Systems course?

~~~
steveklabnik
Pitt grad here, though a decade after the OP. Pitt has 3 courses related to
this stuff:

    
    
      * CS 447: http://cs.pitt.edu/schedule/courses/view/447
      * CS 449: http://cs.pitt.edu/schedule/courses/view/449
      * CS 1550: http://cs.pitt.edu/schedule/courses/1550
    

447 and 449 are required, 1550 is optional.

447 is almost entirely MIPS assembly, and goes into hardware architecture as
well
[https://people.cs.pitt.edu/~childers/CS0447/](https://people.cs.pitt.edu/~childers/CS0447/)

449 is a C class, using K&R 2nd edition:
[https://people.cs.pitt.edu/~jmisurda/teaching/cs449/2164/cs0...](https://people.cs.pitt.edu/~jmisurda/teaching/cs449/2164/cs0449-2164-syllabus.htm)

1550 is more specifically about operating systems, using Tanenbaum's book:
[https://people.cs.pitt.edu/~jmisurda/teaching/cs1550/2164/cs...](https://people.cs.pitt.edu/~jmisurda/teaching/cs1550/2164/cs1550-2164-syllabus-11am.htm)

So at least today, there's one class that's all C stuff. Almost all of the
rest was Java, while I was a student.

------
foyk
"This claim that positive signed overflow wraps around is neither correct by
the C standard nor consistent with the observed behavior of either GCC or
LLVM. This isn’t an acceptable claim to make in a popular C-based textbook
published in 2015."

Perhaps someone could explain what I'm missing. It's _exactly_ the behavior
that I see using gcc-4.8 and Apple llvm-7.3.

~~~
strcat
Read the linked content in the post about undefined behavior. Signed overflow
is undefined, not implementation defined. Clang and GCC treat it as undefined
such without -fwrapv. That means they assume it cannot happen and feed
information into optimization passes and code generation based on that
assumption. It's worse than the result potentially being different: a program
with signed overflow may crash, corrupt data or worse and it happens in
practice. One common example is overflow checks often being optimized out if
they do it by trying the operation and then checking for overflow. As the
compilers get smarter, the problems will grow. They barely do any integer
range analysis right now...

------
awinter-py
first piece in a while to make me optimistic about college curriculum
priorities.

Not sure it's possible to teach green frosh 'why does industry use an old
language' and the static analysis ecosystem (easier to teach skills than
wisdom). But I applaud these people for trying. This feels like real
programming.

------
Paul_S
Knowing assembly is a good first step and a prerequisite to be useful in an
embedded project.

------
lil1729
All good points. But teach all these in one semester? Poor students..

------
ape4
For all assignments, tell the students to use the most appropriate language.
Plot twist: all assignments are for high level applications and C isn't the
most appropriate.

