Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

I took a look at the original K&R book not long ago, and was surprised how sparse of a reference it was.

For non-Unix platforms, the only header mentioned is <stdio.h>, but it does includes such functions/macros as isupper, system, calloc, cfree, exit, and _exit.

For Unix, there is some mention of other headers, but many functions don't seem to be associated with headers at all since they return `int` and as such don't need a declaration. Headers are mostly for constants, typedefs, and structs.

I also noticed that: long float is equivalent to double, unsigned is only allowed on plain int, and scanf %h is a top-level specifier instead of a modifier.



There's one other thing that's different in those early Cs, there was no union, and structs worked differently. Effectively a field in a struct was just an offset, and all offset names were part of a global namespace of all field offsets - you could use any field name with any struct pointer - which is how you did the equivalent of unions - the v6/v7 unix kernels used that for block device structures, they all began with the same first fields (a block queue) followed by per-device stuff


K&R C was almost perfect, with its simplicity, minimalist elegance, and power. It would have been perfect if it was not for the addition of “->” which was unnecessary; it was done perhaps to simplify the compiler.


K&R C didn't have a `void` return type nor even check for the number of function arguments. Later C versions may have been bloated, but K&R's simplicity was often at odds with a minimal level of protection against the human stupidity. (Or you have a different definition of K&R C, which is normally used to call what was described in the first edition of TCPL in 1978.)


It's weird to see you downvoted for this mere opinion. I agree - C has collected a LOT of garbage over the years. C++ is even worse and if you've ever used Smalltalk or Common Lisp, it barely even seems like OO.


`->` is completely redundant with `.`. I suspect it may be a relic from an earlier version of C that did not have a complete type system.


"->" is taken from the IBM PL/I language, which was an important source of various syntax elements of C (the other 2 main sources being BCPL and Algol 68).

It was added because the indirection operator "*" has the wrong position in C, it is prefix instead of being postfix, like in the languages where it had first appeared, i.e. Euler and Pascal, which had inherited it from Euler (both are Wirth languages).

Had "*" been postfix, ".*" could have always been used instead of "->". As it is, "->" eliminates a pair of parentheses that is needed when the indirection is followed by other postfix operators, like array indexing or structure member selection, which happens very frequently.

IIRC, in one of his papers about the history of C, Dennis Ritchie has recognized several mistakes in the design of C and one of them was the prefix "*", besides others like the precedence of "&" and "|".

In PL/I "->" was the only indirection operator, while CPL had no explicit indirection, the indirection was implicit whenever a pointer was used in an expression, like with the C++ references. The CPL pointers (which were named references, "pointer" is a term introduced later by PL/I) could use a special assignment operator, which assigned the address of an expression, not its value.

While "->" is somewhat redundant, its sole purpose being to eliminate some parentheses, it is no more redundant than e.g. the existence of both "for" and "while" or of "do ... while" when "break" exists, or of the comma operator, or of several other redundant features of C, which offer alternative ways of writing the same thing, with slightly shorter variants for certain use cases. While C has much less redundancy than Perl (which is proud of it), it still has much more than I consider good for a programming language.

When structures had been added to C, they had realized that the wrong position of "*" requires extra parentheses, but they must have had plenty of code written in late B and early C that had used "*", so instead of redefining "*" they have chosen the easier path of taking the structures from PL/I together with all their syntax elements used in PL/I, i.e. the keyword "struct" and the operators "." and "->".


Hey array indexing is postfix, so let's just use the old "arrays decay to pointers" staple, but obviously in reverse:

    typedef struct {
        int a;
        int b;
    } foo;

    int main(void) {
        foo one = { 1, 2 };
        foo * const p = &one;
        p[0].b = 13;   // Look ma no -> !
        printf("Now we have { %d, %d }\n", one.a, one.b);
        return 0;
    }
Of course one could go all nasty and flip the base and index:

        0[p].b = 13;
But hey that's just being silly.

In all seriousness, thanks a lot for the historic review of C syntax! Very interesting, as someone who really likes C enough to (ab)use it whenever possible.

I know that a->b is equivalent to (*a).b, but still the difference makes sense to my brain which first learned assembly. It's much "lighter-feeling", not having to evaluate the "entire" structure just to get to a single field. :) Weird, but I really like this little piece of syntactic sugar. Yum.


or just "(*p).b = 13;"


Well sure but that was the point of my comment's parent.


The first thing the D semantic processor does is convert a `while` into a `for`, and the `for` semantic code then does all the work.


The do-while isn't representable as a for-loop because of the `continue` statement.


Yes, I believe there was a variant where struct members had global identifier scope. This means that in order to compile x->mem or x.mem, you don't need to know the type of x at all: mem alone tells you the offset to use. In this context, the distinction between -> and . matters because you don't have the type to tell whether an indirection is needed.

It's surprising that unified -> and . wasn't widely added as a compiler extension. Most compilers already do the adjustment as part of error recovery, and most of them accept much more dubious type errors by default (things that never were part of any C standard). GDB handles it just fine, though.


My recent implementation of C, ImportC, treats . and -> the same. No errors or warnings emitted.

(The reason is ImportC re-uses the D semantic code, and D doesn't have ->)


Personally I like the distinction between them as it gives me context for the object being a pointer, which helps with "at-a-glace" reading of clean code.


I discovered long ago that the -> . dichotomy makes it essentially impossible to refactor code by switching a type to/from a value/ref type. It is a major factor in C being a very difficult language to refactor code in. Long lived C code still retains its original design mistakes because of that.


It was published in 1978. Surprising that in 45 years, a couple of things have changed?




Consider applying for YC's Summer 2026 batch! Applications are open till May 4

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: