C Arrays and C Pointers
One thing that always confuses is people learning the C programming language is the duality between arrays and pointers. In your C program you might create an array, which is a block of storage:
Then when you call a subroutine with the array as a parameter, the array magically transforms into a pointer to itself:
You are taught that array definitions are different from pointer definitions:
Except in places where they aren’t:
Now, when you were first learning the C programming language, you probably asked yourself: why do arrays and pointers have this confusing relationship; why can’t arrays just be arrays and a pointers just be pointers? (Or maybe you didn’t ask yourself. While studying programming, perhaps you have come to believe that computer languages are by nature complex and arcane things full of unknowable mysteries, and that true programmers have skills that are a cross between those of tax accountants and Harry Potter. And all this stuff about arrays and pointers merely reinforces your belief.) Well, it turns out there is a reason why arrays and pointers act as they do, and this reason is rooted in the history of the C programming language.
The Language that Came Before C
When Dennis Richie at Bell Labs designed the C programming language in the early 1970s, he based it on another language named B, which had been designed by colleague Ken Thompson.
A program written in B would be reasonably legible to a C programmer because B is essentially a simpler version of C. The most notable difference between B and C is that B is a typeless language, that is, variables do not have explicit data types like character or integer. Or, rather, there is exactly one data type: the machine word, which is equivalent to C’s integer type ‘int’.
Now you might think that working with things like character strings would be difficult when your only data type is int, but B provides some help. For example, characters treated as small integers (e.g., ‘A’ is identical to 65), and a string is an array of words with four characters packed per word. To work with the characters in a string, you use the library functions char( string, index ) and lchar( string, index, replacement ).
Arrays are a problem because you obviously cannot pack an array into a single machine word. B solves this problem by defining an array to be a pointer to a block of storage, and having program code deal with the pointer, which does fit in a word. This means that when you define an array, B sets aside storage for both the array itself and the pointer to it:
When you work with an array in B, you are always working with the pointer. This makes it straightforward to pass an array to a subroutine: you pass the pointer to the block of storage unchanged, and both the caller and callee work with the same thing, namely that pointer. This also keeps B’s calling mechanism simple: parameters are always passed by value with no danger of trying to copy blocks of storage into subroutines.
C Tries to Keep Things Tidy
With C came a full suite of data types including arrays as fully-fledged objects. This has advantages, for example, the program does not create pointers unnecessarily, and multi-dimensional arrays are easy to build. However there is a downside when calling subroutines. Because C passes parameters by value, passing an array to a subroutine theoretically means copying the entire array into the subroutine. Not only is copying a block of storage expensive for all but the tiniest of arrays, it is generally not a useful thing for a program to do.
There are a couple of ways around this call-by-value-with-arrays issue, but both involve additional syntax. One way is to allow both call-by-value and call-by-reference parameters in subroutine calls. C++, for example, supports both modes; programmers can select call-by-reference by adding an “&” to the parameter declaration. Another way is to require the programmer to explicitly generate pointers to arrays and pass them into the subroutine, but this requires the programmer to clutter the subroutine code with dereferencing “*” operators.
With C, Ritchie tried to find a middle ground by designing a mechanism for subroutine calls that normally uses the traditional call-by-value, but automatically switches to call-by-reference for arrays. This solves the call-by-value-with-arrays problem and avoids explicit syntax for call-by-reference parameters. This permitted C to deliver the benefits of arrays as first-class objects, while offering a familiar, uncluttered syntax to B programmers (and to Fortran programmers as well) who were migrating to C.
This design fix, with what might be called “hidden dereferencing”, has proven effective in routine program code. It is when a programmer wanders off the beaten path that hidden dereferences can come out hiding and bite not only students but also professional programmers.
Back to the Future with Java
Other languages like Pascal, which appeared around the same time as C, and C++ support both call-by-reference and call-by-value parameter-passing modes with explicit syntax to distinguish the two (“var” and “&”, respectively). Interestingly, Java, a language of more recent design, goes back to B’s idea of always using references (i.e., pointers) to deal with objects.
Although Java’s object references — whereby you always work with references to objects rather than directly with the objects themselves — echoes the array pointers of B, Java’s references are more sophisticated than B’s pointers. B programmers had to worry about memory leakage (a block of memory with no pointer to it), dangling pointers (a pointers referring to a block of memory that has been deallocated), and other such horrors. Java has tamed the wild pointers of B through built-in mechanisms such as garbage collection and automatic array-bounds checking.