Alright. Let's talk about some basic issues first:
- On many systems, - intwill 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 - %pformat 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().