I dislike answering my own questions. To that end, I have accepted the answer that is supported by the most historical sources. (Retrocomputing is about actual history, after all.) However, I'm not yet satisfied that the other answers completely answer the question.
Dennis Ritchie, the creator of the C language and co-creator of Unix, presented a history of C at an ACM conference in April 1993. The conference proceeding is available online as The Development of the C Language, and is quoted below.
C evolved from the short-lived B language, which in turn derived from BCPL. These two prior languages were typeless. All variables were word-sized values, which could represent signed integers, unsigned integers, characters, or pointers. Pointers could be used to reference the elements of arrays, strings, or data structures. The type that a word represented depended on the operators being used on it. As Ritchie noted,
Both languages are typeless, or rather have a single data type, the 'word,' or 'cell,' a fixed-length bit pattern. Memory in these languages consists of a linear array of such cells, and the meaning of the contents of a cell depends on the operation applied. The +
operator, for example, simply adds its operands using the machine's integer add instruction, and the other arithmetic operations are equally unconscious of the actual meaning of their operands.
Ritchie's paper goes in depth about how the syntax for arrays developed. In BCPL, memory was accessed with the syntax pointer!offset
. B changed memory access to a unary prefix operator *
. Arrays could then be accessed with the expression *(array+offset)
. To "sweeten such array accesses", Ritchie added the now-familiar syntax array[offset]
to B.
Structures didn't exist in BCPL or B; they were introduced in C. Ritchie doesn't discuss the history of structures as much as he does arrays. However, it does seem that early versions of C continued the practice that the meaning of a value was determined by the operators that used it. Ritchie notes:
Beguiled by the example of PL/I, early C did not tie structure pointers firmly to the structures they pointed to, and permitted programmers to write pointer->member almost without regard to the type of pointer; such an expression was taken uncritically as a reference to a region of memory designated by the pointer, while the member name specified only an offset and a type.
and
Compilers in 1977, and even well after, did not complain about usages such as assigning between integers and pointers or using objects of the wrong type to refer to structure members.
He doesn't explicitly explain the origins of .
and ->
, but it is reasonable to conclude that their purpose was to simplify expressions (much like []
did for arrays). As many have noted, pointer->member
is a shorter way to write *(pointer+member)
. Similarly, variable.member
is a shorter way to write *(&variable + member)
.
The critical thing here is that these two operations are not the same. Considering that embryonic C continued the tradition of leaving the determination of type to operators, two different operators were therefore needed. Embryonic C was not sophisticated enough to "know" the type of operands.
As C matured, later compilers would have enough information to make the distinction. However, Ritchie notes the importance of backward-compatibility for existing code, even as the language was evolving:
As should be clear from the history above, C evolved from typeless languages. It did not suddenly appear to its earliest users and developers as an entirely new language with its own rules; instead we continually had to adapt existing programs as the language developed, and make allowance for an existing body of code.
(*structure).member
, if you like that more. (I don't, very probably K&R didn't, either. It is a bit awkward to handle because of C operator precedence, and that might answer your question)a.b
could be interpreted as(*a).b
if a is a pointer-to-struct, would it also be automatically be interpreted as(**a).b
if a is a pointer-to-pointer-to-struct? Just to point out a possible consequence…^
to be omitted in the expressionsp^.x
andp^[i]
, and using types to disambiguate.->
looks like it's pointing somewhere, so it's appropriate with pointers.