.... with undefined behavior; why this matters (and when it does not)
in a moment:
#include<stdio.h>
main()
{
int arr[3]={1,0,2};
int **dp;
[this is a typo -- you mean "int *dp", otherwise you must get
compiler diagnostics from any conforming C compiler]
int (*pa)[3];
int i = 0;
pa = &arr;
dp = arr;
printf("%u \t %u \n",arr,&arr); /* stmt A */
printf("%u \t %u \n",pa,*pa); /* stmt B */
printf("%u \t %u \n",dp,*dp); /* stmt C */
I am going to address just these for the moment.
[output so far]
2062498376 2062498376
2062498376 2062498376
2062498376 1
Whats bugging me here is
a) why both the values in stmt A are equal.
b) why both the values in stmt B are equal
c) why the values in stmt C are not equal
Here is the first (and major) problem: the "values" in statements
A and B are *not* equal. The output *numbers* are equal, but this
is the way your implementation happens to handle the undefined
behavior. The values in statement C are indeed not equal, and
the output of "1" is as expected (disregarding the earlier undefined
behavior anyway).
C has a very odd "feature" in dealing with arrays. This feature
is the source of a great deal of confusion. To understand it, you
must start by understanding a fundamental division that C shares
with many other languages, so you might well already know it, but
it is good to get some verbal handles on it.
C separates most of the "C world" into "objects" and "values".
Objects are simply regions of storage that hold values. (Ordinary
variables are objects.) Values are more or less what you expect:
numbers like 42 and 3.14159, letters like 'a', and so on. All
values have a type: 42 has type "int", 3.14159 has type "double",
and 'a' has type "int" (perhaps surprisingly -- it seems like it
should be "char" but it is actually "int"; note that C++ differs
here). Values also include (unless you are Joe Wright
) "pointer
values", which are just values that have some particular pointer
type.
Because C does different things with differently-typed values,
whenever we want to describe a value, we must also attach its type.
For instance, 3 / 4 is 0, but 3.0 / 4.0 is 0.75 -- so if you want
to talk about the int value 3 and the int value 4, I recommend
writing them as pairs for the ultimate in explicitness: <int, 3>
and <int, 4>. That way you can write <double, 3> to mean 3.0, and
you can also write <char, 'a'> to mean the value you get by converting
the "int" value 'a' to "char" (as you would have to do to store it
in a "char" object). Often you can "cheat" and leave out the type
part, inferring it from context: 3 must be an int, while 3.0 must
be a double. But if you write it down -- <double, 3.0> -- it
is clearer; and it lets you talk about <float, 3.0> without using
the funky F suffix (3.0F). It also lets you talk about pointer
values, which need types too.
Like many other languages, C will automatically let you write an
object where a value is required:
int i, j;
...
i = j;
Here i and j both name objects of type int, yet somehow this
assignment statement finds the *value* of j, and copies that
into the *object* i. If you write:
i = i + j;
the statement finds the *values* of i and j, computes the sum of
those two values, and stores the result into the *object* i. How
does the compiler manage this trick? (It is a neat trick, if you
think about it for a bit.)
The answer lies in what I call "object versus value context".
The left hand side of an assignment operator like "=" has "object
context", while the right hand side has "value context". So in
"i = j", the compiler knows to use i as an object -- to store a
new value -- and yet to pull a value out of j. In "i + j", both
i and j are operands to the "+" operator, which takes two values,
so the compiler knows that you want it to find the values stored
in those objects.
With all this in mind, we can finally get to the peculiar thing C
does with arrays. C has a special rule for array objects. This
rule is crucial to understanding arrays and pointers in C. It is
so important, so central, that I call it not "a rule" but "The
Rule", with capital letters. There is no fundamental reason for
The Rule about arrays and pointers in C; it is just something Dennis
Ritchie came up with, that you have to memorize. It goes like
this:
In a value context, an object of type "array N of T" becomes
a value of type "pointer to T", pointing to the first element
of that array, i.e., the one with subscript 0.
Armed with The Rule, let us now take a look at just this part:
int arr[3] = { 1, 0, 2 };
int *dp;
dp = arr;
Now, the ordinary assignment in the last line has object context
on the left and value context on the right -- and "arr" is the
name of an object with type "array 3 of int". So, we must
apply The Rule: "an object of type array N of T" (yep, N=3 and
T=int) "becomes a value of type pointer to T" (pointer to int)
"pointing to the first element of that array" (&arr[0]). So
dp is an object, and its value becomes the pair:
<pointer to int, &arr[0]>
But what about "pa", in:
int (*pa)[3];
pa = &arr;
? Here &arr applies the unary "&" operator to the array object.
The unary "&" operator does *not* take a value; it takes an object.
Here The Rule does *not* apply -- unary "&" has "object context"
instead of "value context". Given a valid object O of some type
T, &O produces a value of type "pointer to T", pointing to that
object. Since "arr" is an object of type "array 3 of int", &arr
yields the address of arr, as a value of type "pointer to array 3
of int". And, since "pa" has type "pointer to array 3 of int",
it can certainly hold that value.
So, now consider:
printf("%u \t %u \n",arr,&arr); /* stmt A -- undefined behavior */
Here "arr" and "&arr" are both passed by value to printf(). This
puts each one in value context. Since "arr" names an array object,
The Rule applies to the middle part, and the value delivered to
printf() is:
<pointer to int, &arr[0]>
On the other hand, &arr produces a value, and no special action
is required; The Rule does not kick in. Here the value delivered
to printf() is:
<pointer to array 3 of int, &arr>
But the format argument to printf has two "%u" conversions, and
when you give printf a "%u", you tell it to pluck an "unsigned
int" value out of the variable parameters. In other words,
printf() is grabbing some <int, ___> (fill in the blank) value
from somewhere. Your actual output was:
2062498376
but what the heck is that? If you were using an old Motorola 68000
based system, it might be whatever was lying around in register
D0, even though the pointer value you gave printf() was sent down
in register A1.
As it happens, my crystal ball tells me you were probably using an
Intel x86 CPU in 32-bit mode -- and nothing like gcc's -mregparm
trick -- so "2062498376" was whatever was at (%esp+8). That happens
to be the numeric value corresponding to a typeless pointer
corresponding in turn to &arr[0]. But all of this is quite
machine-specific: your machine happens to discard the type information
from pointer values at runtime, *and* has pointer values the same
size as ordinary int values, *and* passes them in the same location
to printf(). (All three of these are actually quite common today,
but were not once, and are soon not to be again. In particular,
32-bit-int 64-bit-pointer is no longer terribly rare.)
The second "%u" conversion also printed 2062498376, but again, it
was just by luck (whether good or bad is a matter of interpretation),
or perhaps by "knowing too much" about your machine. It is not
particularly surprising, but if your machine had different kinds
of pointer values in hardware, and used them for C code; or if it
passed pointers in A registers and int values in D registers; or
if "int" and (some) pointers were different sizes -- if any of
these had been true, you would perhaps not have gotten two identical
numeric values.
Lastly, before I move on to the next part, consider the two
pointer values:
<pointer to int, &arr[0]>
<pointer to array 3 of int, &arr>
C has a funny little rule about every object -- including array
objects -- being made up of bytes, so we can convert either of
these two pointer values to a "byte pointer", which C spells
"char *" or "unsigned char *" or -- something added in 1989 purely
to allow us to avoid casts -- "void *". Suppose we convert both
of those two pointer values to "void *":
void *vp1, *vp2;
vp1 = arr; /* by The Rule, "arr" becomes <int *, &arr[0]> */
vp2 = &arr; /* "&arr" is just <int (*)[3], &arr> */
Now we have, in vp1 and vp2, two pointer values of type "void *".
Each of these points to the first byte in the object to which
the original pointers (&arr[0] and &arr) pointed. Those two
"first bytes" are necessarily the *same* byte, so if we were
to use a C-standard-defined printf():
printf("vp1=%p\nvp2=%p\n", vp1, vp2);
we might expect the two values printed to be equal. (But on
the other hand, %p's output format is up to the implementation,
and if the implementation keeps extra information -- such as
for debugging -- about the way those "void *" pointers were
derived, it might print out the debug info and show us values
that are slightly different, e.g.:
vp1=0x7aef3a48 (from <int *>0x7aef3a48)
vp2=0x7aef3a48 (from <int (*)[3]>0x7aef3a48)
Systems that track this much detail are rare, presumably because
no one ever needs to debug C code.
)
We can fix "stmt C" to have well-defined output by making sure
that the printf formats and arguments have matching types:
printf("dp=%p *dp=%d\n", (void *)dp, *dp);
Here the cast to "void *" converts the value in dp to the type we
told printf to expect when we wrote "%p", and the type of *dp --
assuming "dp" is "int *dp" -- is in fact int, which is what %d says
for printf to print.
Now, still with:
int arr[3] = { 1, 0, 2 };
int *dp = arr;
int (*pa)[3] = &arr;
and with "int i" initially zero:
for(;i<3;i++)
printf("arr %d\t pa %d\t dp %d\n",*(arr+i),*(*pa+i),*(dp+i)); /*stmt D*/
[produces]
arr 1 pa 1 dp 1
arr 0 pa 0 dp 0
arr 2 pa 2 dp 2
d) why shouldn't I use *(pa+i) instead of *(*pa+i) in stmt D
e) how come *(dp+i) works fine in stmt D
Now we come to the definitions of the "+" and unary "*" operators
when applied to pointers. (Unary * can ONLY be applied to pointers,
but + works on other things too.)
First, it is worth mentioning that C's subscript operation a
is defined in terms of unary "*" and pointer arithmetic, so that
a "means" *((a) + (b)). We can work this in reverse, too,
rewriting *(arr + i) as arr, and so on. It is also worth
noting that unary * binds more tightly than +, so that *pa+i
"means" (*pa) + i.
Second, the definition of *(ptr) is "follow the pointer value,
producing the object to which it points". The definition of
"ptr_value + int_value" is "move the pointer forward by the given
number of objects (or backward is the number is negative)". For
instance, p+2 is "move forward by 2 of whatever it is p points to".
The "whatever it is" part is determined by the type of the pointer
-- if the pointer has type "pointer to T", we will move forward
two "T"s. As usual in C, the type of the value is critical:
without it we cannot find the answer.
Last, as always, remember The Rule.
Let us take *(arr + i) first. Here we ask the "+" operator to add
the values of "arr" and "i". What is the value of arr? Well,
"arr" names an array object. Voila! An array object in a value
context; The Rule applies. So we apply it, and find that the
"value" of "arr" is &arr[0], which has type "pointer to int". Now
we add i -- 0, 1, or 2 -- so we move forward 0, 1, or 2 "int"s in
the array. Then we apply the unary *, following this pointer value
to its object, which is an object of type "int". That object
happens to be arr -- which is of course what *(arr + i) means,
while *(arr + i) is what arr means. This is how The Rule does
its magic.
Now consider *((*pa) + i). This has TWO occurrences of unary *.
The one inside the parentheses is *pa, which means "find the value
of pa, then follow it to its object". The value is the value we
stored in the object named "pa" earlier:
<pointer to array 3 of int, &arr>
When we follow this to its object, we get an object of type "array
3 of int" -- this being what is left after removing the initial
"pointer to" part. Note that we have an object, and it has an
array type. Now we want to compute that plus the value in i --
and what do you know, it is time once again for The Rule. We have
an array object and we want its value. The object is "arr" itself,
and its "value", by The Rule, is a pointer value of type "pointer
to int", pointing to the first element of "arr". The rest of
the sequence happens just as before.
Note that, as always, we can rewrite *(a + b) as a. In this
case we can change *((*pa) + i) into (*pa). The "*" on "*pa"
*must* be enclosed in parentheses this time, though, because *pa
would "mean" *(pa), which is something quite different. (If we
wrote that, it would be the same as *(*(pa + i)) -- adding the
value stored in i to the value stored in pa, using the type that
"pa" points to, and hence moving forward by 0, 1, or 2 "array 3 of
int"s, because pa has type "pointer to array 3 of int".)
The last expression, *(dp + i), works in the same fashion. This
time there are no array objects at all: dp is an object of type
"pointer to int", to which we add 0, 1, or 2 and move forward by
that many "int"s; then we follow the resulting pointer and get an
"int". And of course, this can be rewritten as dp, so the
last printf() line is the same as:
printf("arr %d\tpa %d\tdp %d\n", arr, (*pa), dp);
One final note: if a "means" *((a) + (b)), consider a[0].
Here we have *((a) + 0). Adding 0 to a pointer means "do not
move the pointer at all", so we can get rid of the +0 part
and just write *(a). This in turn means that *(p) and (p)[0]
alwayws mean exactly the same thing (I used the parentheses
in case there is an expression involved). So (*pa) is just
pa[0], and the above printf() can also be written:
printf("arr %d\tpa %d\tdp %d\n", arr, pa[0], dp);
This last line may give readers an "aha!" moment: a "pointer
to array N of T", like pa is a "pointer to array 3 of int",
has to be subscripted *twice*. We write pa[0] to access
the i'th element of the 0'th -- and in this case only -- array
to which "pa" points. What if we wrote pa[j]? Would this
be the i'th element of the j'th array? How many arrays does
"pa" access in this way? How does this compare to the number
of "int"s we can access with dp?