10
$\begingroup$

C has the behaviour of doing an implicit reference to element 0 of an array whenever you attempt to use it directly. This extends to passing arrays to/from functions - they get passed by pointer and not by value.

int array[5] = {0};
int* pointer_to_array = array;
int* pointer_2_array = &array[0];
bool is_true = (pointer_to_array == pointer_2_array);

This behaviour has always astounded me. Even more unusual is that wrapping the array in a struct/union circumvents this.

Breaking the question down into parts:

  1. What was the original reason for this in C? I presume C++ just carried it forward.
  2. What justification might a language have for doing this today? (I can't think of an example that comes close aside from Java. Surely there are some?)
New contributor
Icedude_907 is a new contributor to this site. Take care in asking for clarification, commenting, and answering. Check out our Code of Conduct.
$\endgroup$
2
  • $\begingroup$ Early versions of C did not allow passing or returning structs by value, so your "even more unusual" did not arise. $\endgroup$ Commented yesterday
  • $\begingroup$ The pointer part came first, arrays are built out of that, not the other way around. And once you understand that, this behavior seems a lot less unusual or illogical. And, FWIW, this also is much closer to how most systems actually implement this in assembly/machine code, it’s almost always some form of indirect addressing, usually indirect with offset. The only exceptions are cases where that’s not possible, or insanities like the iAPX 432 where you have typing and simple data structures implemented all the way down at the hardware level. $\endgroup$ Commented 11 hours ago

2 Answers 2

19
$\begingroup$

The unusual implementation of arrays in C is described in a wonderful document called The Development of the C Language by Dennis Ritchie. (The more widely used URL seems to have been broken by the current owners of the bell-labs.com domain. 😒)

The short version is that it's a vestige of C's evolution from the earlier languages BCPL and B, which lacked a comprehensive type system, but had a simple way to represent arrays by pointing into a linear array of "cells" in memory.

When C added a more complex type system and more flexible memory layout, this evolved into a rule that an array variable acts as a pointer to the array's first element when directly referred to.


C's direct ancestors are BCPL, and an earlier experiment called B. Both are implemented as an array of "memory cells", and array data-types are implemented as a section of this linear memory:

Because pointers in BCPL and B are merely integer indices in the memory array, arithmetic on them is meaningful: if p is the address of a cell, then p+1 is the address of the next cell. This convention is the basis for the semantics of arrays in both languages. When in BCPL one writes let V = vec 10 or in B, auto V[10]; the effect is the same: a cell named V is allocated, then another group of 10 contiguous cells is set aside, and the memory index of the first of these is placed into V.

[...]

This approach to arrays was unusual even at the time; C would later assimilate it in an even less conventional way.

This layout persisted into the short-lived NB ("New B"), where arrays and pointers were almost identical:

Within procedures, the language's interpretation of the pointers was identical to that of the array variables: a pointer declaration created a cell differing from an array declaration only in that the programmer was expected to assign a referent, instead of letting the compiler allocate the space and initialize the cell.

But this caused problems as the language grew into what Ritchie calls "Embryonic C":

Problems became evident when I tried to extend the type notation, especially to add structured (record) types. Structures, it seemed, should map in an intuitive way onto memory in the machine, but in a structure containing an array, there was no good place to stash the pointer containing the base of the array, nor any convenient way to arrange that it be initialized.

[...]

I wanted the structure not merely to characterize an abstract object but also to describe a collection of bits that might be read from a directory. Where could the compiler hide the pointer to name that the semantics demanded? Even if structures were thought of more abstractly, and the space for pointers could be hidden somehow, how could I handle the technical problem of properly initializing these pointers when allocating a complicated object, perhaps one that specified structures containing arrays containing structures to arbitrary depth?

So the current semantics were introduced:

The solution constituted the crucial jump in the evolutionary chain between typeless BCPL and typed C. It eliminated the materialization of the pointer in storage, and instead caused the creation of the pointer when the array name is mentioned in an expression. The rule, which survives in today's C, is that values of array type are converted, when they appear in expressions, into pointers to the first of the objects making up the array.

This invention enabled most existing B code to continue to work, despite the underlying shift in the language's semantics. The few programs that assigned new values to an array name to adjust its origin—possible in B and BCPL, meaningless in C—were easily repaired. More important, the new language retained a coherent and workable (if unusual) explanation of the semantics of arrays, while opening the way to a more comprehensive type structure.

$\endgroup$
6
  • $\begingroup$ Yes, and also loosing any total size or bounds information for arrays in their conversion to pointer to first element, but also making them type-compatible with pointer to any other array regardless of either array's original bounds. $\endgroup$
    – Erik Eidt
    Commented yesterday
  • 2
    $\begingroup$ @ErikEidt C is not, in general, a type-safe language; it lets you reinterpret memory in all sorts of crazy ways. The history as "simplified BCPL with structured types bolted on" explains a lot of that; and of course generations of hackers (in the older sense) have then exploited that fact for hand-optimizing code. $\endgroup$
    – IMSoP
    Commented yesterday
  • $\begingroup$ Right. Historical explanations aside, it is missing a mechanism to capture bounds, maybe like slices in other language. $\endgroup$
    – Erik Eidt
    Commented yesterday
  • $\begingroup$ Through "pointers [...] are merely integer indices in the memory array" I finally understand why they thought about p[i] and i[p] being equivalent!! さとり! $\endgroup$
    – Pablo H
    Commented 8 hours ago
  • 1
    $\begingroup$ "This approach to arrays was unusual even at the time; C would later assimilate it in an even less conventional way" - what was the usual approach? $\endgroup$
    – Bergi
    Commented 7 hours ago
1
$\begingroup$

One of the big motivations behind C (and perhaps the reason it became so ubiquitous) is that it's more or less a portable assembly language. It provides just enough abstraction for your code to work on multiple platforms, but doesn't provide so much abstraction that you lose the ability to look at the C code and predict what the generated assembly would look like. A number of C's design decisions were made because they mirrored the functionality available at the hardware level.

Regarding arrays, it's important to remember that C is not an object-oriented language. Compound objects like arrays or structs do not actually exist. The only data types that the vast majority of CPUs understand are integers of varying sizes, floating-point numbers, and pointers (which are arguably just integers). Everything else in the C language is syntax sugar.

Arrays don't decay into pointers when you pass them to a function. Your "array" never was anything other than a pointer in the first place. When you work with arrays in assembly, you do your own pointer arithmetic. For example, an array dereference in MIPS assembly language might look like:

lw $4, 8($3)

Register 3 holds the base address of the array. This instruction takes that base address, adds an offset of 8 bytes to it to form the target address, and then copies the value from that target address into register 4. C's syntax is a thin veneer over this. The array/pointer in your code is your base address. The compiler calculates the offset for you based on your array index and the size of each array entry.

Structures work essentially the same way. When you declare a structure, the compiler calculates the offset to each member. The struct in your code is merely a pointer. When you reference a struct member, the compiler simply adds that pre-computed offset to the value of the pointer.

When you think of these things as merely being shorthand for pointer arithmetic, many of C's other details make more sense as well.

  • The offsetof() macro merely pulls values from the compiler's internal table of pre-computed offsets.
  • You can't access the members of a struct when no definition is in scope, because the compiler hasn't computed an offset for that member yet. You can use pointers to a structure after only a forward reference because that's enough information for the compiler to know it's just a pointer like any other.
  • Arrays appear to be passed "by reference" to functions. Technically it's just a pointer, and you're passing the pointer by value like any other integer-like data type.
  • A struct can be passed to a function "by value" because the compiler knows the size of the structure (i.e., how many bytes to copy). Struct assignment b = a is shorthand for memcpy(&b, &a, sizeof(a)).
  • You can declare a loose pointer like int* ptr = 0xFF000000; and then use it like ptr[14] = 0; even though it's not an array. The compiler is simply doing the pointer arithmetic you asked it to do. This is also the basis for many types of type punning.
  • sizeof() doesn't work on dynamically-allocated arrays or arrays passed into a function. A pointer doesn't contain any length information, so the compiler has no way to know the size when given only a base address. sizeof() only works if the array's definition is in the current scope since the compiler can remember how much space it allocated.
  • union is handled essentially the same as struct except each member's offset is calculated relative to the base address instead of relative to the end of the previous member.

C was designed to be very close to the metal. The programmer was responsible for things like keeping track of array bounds, ensuring pointers were compatible with underlying data, etc., which is the way it was in assembly. C was a step forward in that respect, moving some of that burden from the programmer to the compiler.

Modern languages really do spoil us. We have a tremendous amount of computing resources available both at compile time and at run time. We can treat arrays like first-class objects, and the runtime engine will remember their size for us, throw an exception if we exceed their bounds, etc. It's easy to forget just how limited and primitive computers used to be. Some old C compilers only had access to enough memory to remember the first 8 characters of a variable or function name, everything after that was ignored. The preprocessor, compiler, linker, etc. were all separate programs, and compiling a single .c file would require multiple passes and lots of intermediate files. C was originally implemented on a PDP-7 computer, which came standard with just 9KB of memory. The things we take for granted now were either wildly impractical or not yet invented.

$\endgroup$
3
  • $\begingroup$ Arrays don't decay into pointers when you pass them to a function. Yes, they do. Arrays decay to pointers whenever they're used as rvalues. See stackoverflow.com/questions/1461432/… $\endgroup$
    – Barmar
    Commented 1 hour ago
  • $\begingroup$ @Barmar You seem to have skipped over the next sentence $\endgroup$
    – Bergi
    Commented 2 mins ago
  • $\begingroup$ That sentence is also wrong. Arrays are not pointers. You can assign to pointers, to make them point somewhere else, but you can't assign to arrays. An array of pointers is not the same as a 2-dimensional array. sizeof treats them differently. $\endgroup$
    – Barmar
    Commented 7 secs ago

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .