Alright. Let's talk about some basic issues first:
On many systems, int will take up less room than void *. So you're possibly truncating your pointers when you print them. It's quite easy to fix that, so let's do it.
Additionally, it's unnecessary to cast the value of malloc(), so let's get rid of that as well to clean up the code a bit.
Finally, as chris notes, to use the %p format specifier, we need to cast the int ** and int * variables to void *.
Fixed foo.c
#include <stdio.h>
#include <stdlib.h>
int main() {
int **p = malloc(sizeof(int *) * 2);
int i, j, c = 1;
for (i = 0; i < 2; i++)
p[i] = malloc(sizeof(int) * 2);
for (i = 0; i < 2; i++)
for (j = 0; j < 2; j++)
p[i][j] = c++;
printf("%p %p %p %d\n", (void *) &p[0][0], (void *) p, (void *) *p, **p);
}
Which now outputs:
0x7fd193c03930 0x7fd193c03920 0x7fd193c03930 1
So now, your actual question:
First, let's talk about what address &p[0][0] points to. That's a fairly complicated expression, but we can reduce it using a fairly simple process such that we end up with *p or p[0].
&p[0][0] == &*(p[0] + 0) == p[0] + 0 == p[0] == *(p + 0) == *p
Looking at that, it should be pretty clear why &p[0][0] (the first argument) and *p (the third argument) print the same value.
Now, I don't see any good reason why p[0] should point to the same address as p. p and p[0] have been assigned values from separate calls to malloc().