Understanding of pointers with malloc and free - c

Pointers are a really tricky thing in C. For a lot of people is hard to understand it, so for a good understanding I wrote following code:
#include <stdlib.h>
#include <stdio.h>
int main(int argc, char *argv[])
{
int *p; // pointer -> will be dynamic allocated
int *a; // array -> will be dynamic allocated
// print before allocate memory (1)
printf("&p: %p\tp: %p\t*p: %d\n", &p, p, *p);
printf("&a: %p\ta: %p\t*a: %d\n", &a, a, *a);
printf("\n");
// allocate memory (2)
p = (int *)malloc(sizeof(int));
a = (int *)malloc(sizeof(int) * 10);
// print after allocate, but before give a value to poinetrs (3)
printf("&p: %p\tp: %p\t*p: %d\n", &p, p, *p);
printf("&a: %p\ta: %p\t*a: %d\n", &a, a, *a);
printf("\n");
// give a value to poinetrs (4)
*p = 1;
for (int i = 0; i < 10; i++) { a[i] = i; }
// print after we gave a value to pointers (5)
printf("&p: %p\tp: %p\t*p: %d\n", &p, p, *p);
printf("&a: %p\ta: %p\t*a: ", &a, a);
// because a is an array we must use a loop for print
for (int i = 0; i < 10; i++) { printf("%d ", a[i]); }
printf("\n");
printf("\n");
// free pointers (6)
free(p);
free(a);
// print pointers after free (7)
printf("&p: %p\tp: %p\t*p: %d\n", &p, p, *p);
printf("&a: %p\ta: %p\t*a: ", &a, a);
// because a is an array we must use a loop for print
for (int i = 0; i < 10; i++) { printf("%d ", a[i]); }
printf("\n");
printf("\n");
// try to change values after free (8)
*p = 12;
for (int i = 0; i < 10; i++) { a[i] = 3; }
// print after (8)
printf("&p: %p\tp: %p\t*p: %d\n", &p, p, *p);
printf("&a: %p\ta: %p\t*a: ", &a, a);
// because a is an array we must use a loop for print
for (int i = 0; i < 10; i++) { printf("%d ", a[i]); }
printf("\n");
printf("\n");
return 0;
}
Output:
&p: 0xbfe5db64 p: 0xbfe5dc24 *p: -1075452506
&a: 0xbfe5db68 a: 0xbfe5dc2c *a: -1075452502
&p: 0xbfe5db64 p: 0x8716008 *p: 0
&a: 0xbfe5db68 a: 0x8716018 *a: 0
&p: 0xbfe5db64 p: 0x8716008 *p: 1
&a: 0xbfe5db68 a: 0x8716018 *a: 0 1 2 3 4 5 6 7 8 9
&p: 0xbfe5db64 p: 0x8716008 *p: 0
&a: 0xbfe5db68 a: 0x8716018 *a: 0 1 2 3 4 5 6 7 8 9
&p: 0xbfe5db64 p: 0x8716008 *p: 12
&a: 0xbfe5db68 a: 0x8716018 *a: 3 3 3 3 3 3 3 3 3 3
Now, questions and observations:
When I print pointers before give memory for it, why pointer have a random value and a random address to point to it and why it isn't NULL?
After we use malloc, we can see the address where pointer points to changed and its value is NULL, so what malloc really does?
After we give a value to it and print it, we free it and print it again, but values and address are same as behind for array, but not for the integer, why? So what free really does?
After we freed space, we can continue to change values of array and integer, why is this possible after free space? We don't need to reuse malloc?

Because the language specification says so. The value of the pointer (i.e. the address it points to) is indeterminate. It can point anywhere, just like an int could hold any value. Reading those values, (as you do with *p and *a in the first printfs) is actually undefined behaviour.
If you mean the data it points to is 0, that is by chance. The memory allocated does not have to be zeroed out. For example, it could be part of a block previously allocated with malloc and then freed (free doesn't zero out the memory, see point 3. below.)
That is also by chance. When you free the memory, it is not zeroed out, and it does not have to be used immediately. It can keep storing old values until it is used for something else (for instance, by another allocation)
That is also undefined behaviour. You are writing to memory you no longer own. Anything can happen. The program could have crashed. By chance, it seems like you can successfully write to the array, probable because the memory is still not used by anything else that would cause a more apparent run time error.

1.When I print pointers before give memory for it, why pointer have a random value and a random address to point to it and why it isn't NULL?
You didn't make the pointer NULL. you are just declaring it. After declaring the pointer, it may have any value.
To make NULL-
int *p = NULL;
int *a = NULL;
2.After we use malloc, we can see the address where pointer points to changed and its value is NULL, so what malloc really does?
Man Page says-
void *malloc(size_t size);
The malloc() function allocates size bytes and returns a pointer to the allocated memory. The memory is not initialized. If size is 0, then malloc() returns either NULL, or a unique pointer value that can later be successfully passed to free().
If your allocated memory have 0 means that is by a chance only! The memory allocated by malloc doesn't freed out. But calloc does!
3.After we give a value to it and print it, we free it and print it again, but values and address are same as behind for array, but not for the integer, why? So what free really does?
free does not mean that it will actually delete the memory! It will inform to the OS that I don't want this memory any more, use it for some other process!
You can certainly continue to use memory a after calling free(...) and nothing will stop you. However the results will be completely undefined and unpredictable. It works by luck only. This is a common programming error called "use after free" which works in many programs for literally years without "problems" -- until it causes a problem.
4.After we freed space, we can continue to change values of array and integer, why is this possible after free space? We don't need to reuse malloc?
This is totally Undefined behavior! After freeing the memory also the pointer still points to the same memory location. It is called Dangling Pointer.
To avoid dangling pointer, Make pointer to null after free!
But after freeing the memory, you need to use memory means use malloc to allocate memory and use it!

What are pointers
Pointer declarations look much like other declarations: but don't be misled. When pointers are declared, the keyword at the beginning declares the type of variable that the pointer will point to. The pointer itself is not of that type, it is of type pointer to that type. A given pointer only points to one particular type, not to all possible types. In practice, all pointers are treated as integers, but the compiler will most likely complain about an assignment between incompatible types.
int *p;
The pointer p hasn't been assigned an address, so it still contains whatever random value was in the memory it occupies (whatever value was there before it was used for p). So, provided that a pointer holds the address of something, the notation *p is equivalent to giving the name of the something directly. What benefit do we get from all this? Well, straight away it gets round the call-by-value restriction of functions. Imagine a function that has to return, say, two integers representing a month and a day within that month.
Summary
Arrays always index from zero—end of story.
There are no multidimensional arrays; you use arrays of arrays instead.
Pointers point to things; pointers to different types are themselves different types.
They have nothing in common with each other or any other types in C; there are no automatic conversions between pointers and other types.
Pointers can be used to simulate ‘call by reference’ to functions, but it takes a little work to do it.
Incrementing or adding something to a pointer can be used to step along arrays.
To facilitate array access by incrementing pointers, the Standard guarantees that in an n element array, although element n does not exist, use of its address is not an error.
Pointer arithmetic
Not only can you add an integral value to a pointer, but you can also compare or subtract two pointers of the same type. They must both point into the same array, or the result is undefined. The difference between two pointers is defined to be the number of array elements separating them; the type of this difference is implementation defined and will be one of short, int, or long. This next example shows how the difference can be calculated and used, but before you read it, you need to know an important point.
In an expression the name of an array is converted to a pointer to the first element of the array. The only places where that is not true are when an array name is used in conjunction with sizeof, when a string is used to initialize an array or when the array name is the subject of the address-of operator (unary &). We haven't seen any of those cases yet, they will be discussed later. Here's the example.
#include <stdio.h>
#include <stdlib.h>
#define ARSZ 10
main(){
float fa[ARSZ], *fp1, *fp2;
fp1 = fp2 = fa; /* address of first element */
while(fp2 != &fa[ARSZ]){
printf("Difference: %d\n", (int)(fp2-fp1));
fp2++;
}
exit(EXIT_SUCCESS);
}
freed Pointers
Calling free() on a pointer doesn't change it, only marks memory as free. Your pointer will still point to the same location which will contain the same value, but that vluae can now get overwritten at any time, so you should never use a pointer after it is freed. To ensure that it is a good idea to always set the pointer to NULL after free'ing it.

Regarding 1), reading an uninitialised value is undefined behaviour. Undefined behaviour means that the C standard does not specify what the compiler should do with your program when you attempt to read an uninitialised value, and therefore the compiler writer is free to pretend that uninitialised values will never be read from - and aren't required to account for that happening in a program.
Within the context of your code, you are presumably using GCC or Clang to compile your program. Your two pointer variables are local variables, and local variables are stored on the stack in GCC and Clang C code. The stack may contain 'random' bytes in it; a pointer on 64-bit computers is generally a 64-bit (8-byte) number that corresponds to an address of memory.
For each of the two pointer variables, these randomly initialised 8 bytes are interpreted as a memory address. You can attempt to read from this random memory address, and if you do this (and your program is allowed to access that particular memory address), you will read some garbage values that will be interpreted as the datatype that your pointer points to. For example, in your case, a regular int.
Regarding 4), not to add to what the accepted answer has already said - I'm curious whether, if you malloc a number of objects and you free one of them and you make no further memory allocations, you are not creating new malloc metadata since you have no new allocations. It would be reasonable that the free'd block does not have its contents mutated, right?
To my knowledge, free does not actually return memory to the operating system. Therefore, one would expect that the answer to the above be yes - the block is not mutated.

Related

Increment a pointer and value store in it

#include <stdio.h>
int main()
{
int N = 4;
int *ptr1;
// Pointer stores
// the address of N
ptr1 = &N;
printf("Value #Pointer ptr1 before Increment: ");
printf("%d \n", *ptr1);
// Incrementing pointer ptr1;
ptr1++;
*ptr1=5;
printf("Value #Pointer ptr1 after Increment: ");
printf("%d \n\n", *ptr1);
return 0;
}
The output I was expecting is that printf prints the value 4 and then the value 5.
But after executing the 1st printf statement the code exited and the code never printed the 2nd printf.
Can anyone please explain what am I doing wrong?
As per my knowledge I am incrementing a pointer then storing a new value into the incremented address.
Have I understood it right?
Welcome to stackoverflow :D
The problem is that you have allocated space for a single integer. But you're trying to access two integers. Which is undefined behavior; meaning that sometimes it will crash, like in your case, and sometimes it might work, and some other times it might work but give unpredictable results.
When you increment the pointer you go to an address in memory that you have not allocated. If you want two integers, maybe allocate an array like this:
int N[2] = {4, 4};
Now when you increment the address of the pointer, you're reaching valid memory that you have allocated.
You're incrementing the pointer.
But it looks like you want to increment the value that the pointer points to.
Try this:
(*ptr1)++;
The parentheses are important: they say that what you're incrementing is the contents of the pointer pointed to --
that is, what the ++ operator is applied to is the subexpression (*ptr1).
See also Question 4.3 in the C FAQ list.
When you try to increment a pointer, what c does is this: "ptr + 1 * sizeof(int)" (assuming that ptr is an int pointer). So, now, it is pointing 4 bytes ahead of the variable "N"(assuming that you are on a 64 bit machine) which probably is occupied by another program. When you dereference it you are taking the value that is stored 4 bytes ahead of "N". With that being said, I recommend this:
(*ptr)++

"pointer being freed was not allocated" while using memcpy

I trying to use memcpy to copy content from one array to another, the code is as follow:
#include <stdio.h>
int main(){
int *a;
a = (int*) malloc(sizeof(int) * 4);
a[0] = 4;
a[1] = 3;
a[2] = 2;
a[3] = 1;
int *b;
b = (int*) malloc(sizeof(int) * 4);
memcpy(&b, &a, sizeof(a));
free(a);
for (int i = 0; i < 4; i++){
printf("b[%d]:%d",i,b[i]);
}
printf("%d\n",sizeof(b));
free(b);
return 0;
}
However, when I try to run it, I encounter the following error:
b[0]:4b[1]:3b[2]:2b[3]:18
mem(6131,0x7fffbb4723c0) malloc: *** error for object 0x7fa5a4c02890: pointer being freed was not allocated
*** set a breakpoint in malloc_error_break to debug
Abort trap: 6
This error disappear if I remove the free(b) piece of code, however, I don't know why since I explicitly allocate resource to it.
Your memcpy is wrong. You're copying the value of the pointer a into b instead of copying the data pointed by a into the buffer pointed by b. You end up doing a double free because a and b point to the same place. Replace your memcpy call with this:
memcpy(b, a, sizeof(int)*4);
memcpy(&b, &a, sizeof(a));
This doesn't copy the 4 ints that a points at to the 4 ints that b points at.
This instead overwrites b with the contents of a. Then you end up doing a double free, since both a and b contain the same address.
I'll repeat my comment on your question: You need to take the time to appreciate the difference between a pointer and a pointee. What holds and address, and what is at that address.
This is the exact problem why we say not to cast the return value of malloc() and family in C..
First things first, you are missing stdlib.h include and because of that, the prototype of malloc() is not known and your compiler assumnes that it returns an int. This creates the mismatch between the implicit declaration and the actual definition, which at a later stage, invokes undefined behavior.
That said, another major issue is when you write memcpy(&b, &a, sizeof(a)); which is very wrong. You want to supply a and b themselves, as you want to copy the contents of the memory location pointed by a to the memory location pointed by b, not the address of the pointers.
In your case, the erroneous call actually messes up the contents of b, i.e., the actual pointer returned by malloc(). Therefore, as we know, passing a pointer which was not exactly returned by memory allocator functions or already free()-d, to free() causes UB, quoting C11, chapter §7.22.3.3,
[...] Otherwise, if
the argument does not match a pointer earlier returned by a memory management
function, or if the space has been deallocated by a call to free or realloc, the
behavior is undefined.
you're getting into trouble, as the current pointer held by b is the same one as a which has been already passed to free(). As mentioned in the other answer(s) [1][2], you need to write
memcpy(b, a, sizeof(int)*4);
Moral of the story: Enable compiler warnings and always check the data types.
Maybe because you free b when you do free(a). Because the memory zone is the same after doing memcpy. Try to do free(b) at the end, without the free(a).

Can we change the base address of an array through a pointer to array using brute force?

Somebody wrote the following C program and asked why gcc allows "to change the base address of an array". He was aware that the code is terrible but still wanted to know. I found the question interesting enough, because the relationship between arrays and pointers in C is subtle (behold the use of the address operator on the array! "Why would anybody do that?"), confusing and consequently often misunderstood. The question was deleted but I thought I ask it again, with proper context and -- as I hope -- a proper answer to go with it. Here is the original prog.
static char* abc = "kj";
void fn(char**s)
{
*s = abc;
}
int main()
{
char str[256];
fn(&str);
}
It compiles with gcc (with warnings), links and runs. What happens here? Can we change the base address of an array by taking its address, casting it to pointer to pointer (after all arrays are almost pointers in C, aren't they) and assigning to it?
It cannot work (even theoretically), because arrays are not pointers:
int arr[10]:
Amount of memory used is sizeof(int)*10 bytes
The values of arr and &arr are necessarily identical
arr points to a valid memory address, but cannot be set to point to another memory address
int* ptr = malloc(sizeof(int)*10):
Amount of memory used is sizeof(int*) + sizeof(int)*10 bytes
The values of ptr and &ptr are not necessarily identical (in fact, they are mostly different)
ptr can be set to point to both valid and invalid memory addresses, as many times as you will
The program doesn't change the "base address" of the array. It's not even trying to.
What you pass to fn is the address of a chunk of 256 characters in memory. It is numerically identical to the pointer which str would decay to in other expressions, only differently typed. Here, the array really stays an array -- applying the address operator to an array is one of the instances where an array does not decay to a pointer. Incrementing &str, for example, would increase it numerically by 256. This is important for multi dimensional arrays which, as we know, actually are one-dimensional arrays of arrays in C. Incrementing the first index of a "2-dimensional" array must advance the address to the start of the next "chunk" or "row".
Now the catch. As far as fn is concerned, the address you pass points to a location which contains another address. That is not true; it points to a sequence of characters. Printing that byte sequence interpreted as a pointer reveals the 'A' byte values, 65 or 0x41.
fn, however, thinking that the memory pointed to contains an address, overwrites it with the address where "kj" is residing in memory. Since there is enough memory allocated in str to hold an address, the assignment succeeds and results in a usable address at that location.
It should be noted that this is, of course, not guaranteed to work. The most common cause for failure should be alignment issues -- str is, I think, not required to be aligned properly for a pointer value. The standard mandates that arguments to functions must be assignment-compatible with the parameter declarations. Arbitrary pointer types cannot be assigned to each other (one needs to go through void pointers for that, or cast).
Edit: david.pfx pointed out that (even with a proper cast) the code invokes undefined behaviour. The standard requires access to objects through compatible lvalues (including referenced pointers) in section 6.5/7 of the latest public draft. When casting properly and compiling with gcc -fstrict-aliasing -Wstrict-aliasing=2 ... gcc warns about the "type punning". The rationale is that the compiler should be free to assume that incompatible pointers do not modify the same memory region; here it is not required to assume that fn changes the contents of str. This enables the compiler to optimize away reloads (e.g. from memory to register) which would otherwise be necessary. This will play a role with optimization; a likely example where a debugging session would fail to reproduce the error (namely if the program being debugged would be compiled without optimization for debugging purposes). That being said, I'd be surprised if a non-optimizing compiler would produce unexpected results here, so I let the rest of the answer stand as is.--
I have inserted a number of debug printfs to illustrate what's going on. A live example can be seen here: http://ideone.com/aL407L.
#include<stdio.h>
#include<string.h>
static char* abc = "kj";
// Helper function to print the first bytes a char pointer points to
void printBytes(const char *const caption, const char *const ptr)
{
int i=0;
printf("%s: {", caption);
for( i=0; i<sizeof(char *)-1; ++i)
{
printf("0x%x,", ptr[i]);
}
printf( "0x%x ...}\n", ptr[sizeof(char *)-1] );
}
// What exactly does this function do?
void fn(char**s) {
printf("Inside fn: Argument value is %p\n", s);
printBytes("Inside fn: Bytes at address above are", (char *)s);
// This throws. *s is not a valid address.
// printf("contents: ->%s<-\n", *s);
*s = abc;
printf("Inside fn: Bytes at address above after assignment\n");
printBytes(" (should be address of \"kj\")", (char *)s);
// Now *s holds a valid address (that of "kj").
printf("Inside fn: Printing *s as string (should be kj): ->%s<-\n", *s);
}
int main() {
char str[256];
printf("size of ptr: %zu\n", sizeof(void *));
strcpy(str, "AAAAAAAA"); // 9 defined bytes
printf("addr of \"kj\": %p\n", abc);
printf("str addr: %p (%p)\n", &str, str);
printBytes("str contents before fn", str);
printf("------------------------------\n");
// Paramter type does not match! Illegal code
// (6.5.16.1 of the latest public draft; incompatible
// types for assignment).
fn(&str);
printf("------------------------------\n");
printBytes("str contents after fn (i.e. abc -- note byte order!): ", str);
printf("str addr after fn -- still the same! --: %p (%p)\n", &str, str);
return 0;
}
What you have here is simply Undefined Behaviour.
The parameter to the function is declared as pointer-to-pointer-to-char. The argument passed to it is pointer-to-array-of-256-char. The standard permits conversions between one pointer and another but since the object that s points to is not a pointer-to-char, dereferencing the pointer is Undefined Behaviour.
n1570 S6.5.3.2/4:
If an invalid value has been assigned to the pointer, the behavior of the unary * operator is
undefined.
It is futile to speculate how Undefined Behaviour will play out on different implementations. It's just plain wrong.
Just to be clear, the UB is in this line:
*s=abc;
The pointer s does not point to an object of the correct type (char*), so the use of * is UB.

The following C program regarding pointers is not working

#include<stdio.h>
int main(void)
{
int *pc;
*pc=100;
printf("\n Address of Pointer : %d",pc);
printf("\n Contents of Pointer : %d",*pc);
}
When I run the code in eclipse, it is saying "Pointers.exe has stopped working". What is the error?
Assigning a value to *pc is particularly dangerous. If pc contains the valid memory address , then the assignment
*pc = 100;
will attempt to modify the data stored at that address.
If the location modified by this assignment belongs to the program, it may behave erratically; if it belongs to operating system, the program will most likely crash.
Your compiler should raise a warning that pc is uninitialized.
Change it to:
int a;
int *pc;
pc = &a;
*pc = 100;
Your pc pointer was not initialized in your program.
Moreover the way your are printing a pointer value is incorrect, use this:
printf("\n Address of Pointer : %p", (void *) pc);
You forgot to point pc to somewhere useful. When you write
int *pc;
The compiler creates a pointer for you. The pointer points somewhere in memory, and you have no way of knowing where. Chances are, it points to a bad place in memory. When you overwrite it with *pc=... you overwrite a place in memory that shouldn't be overwritten.
So you need to have pc point somewhere.
int a, *pc=&a;
will do the trick. Now the compiler prepares an int for you then and you point pc to it. That way, when you write through the pointer, nothing bad will happen - you're writing on an area of memory that you know is safe.
Another way to initialize pc is like so:
int *pc = (int*)malloc(sizeof(int)); // Allocate an integer on the heap
Now *pc=100; will also work. Just don't forget to free(pc) when you're done.
You have:
int *pc;
*pc=100;
When you define the pointer to integer pc above, this pointer is not initialized, so it points to some memory that you have not allocated and you have not the right to access.
So your executable is crashing.
Just use a valid pointer, e.g. you can allocate some heap memory using malloc(sizeof(int)) (and later release it using free()), or just use stack-based memory for local automatic variables, e.g.:
/* An integer stack-allocated variable */
int n = 64;
/* Pointer to that integer */
int * pn = &n;
/* Change its value */
*pn = 100;
Moreover, to print an address, considering using the %p specifier for printf().
e.g.:
#include <stdio.h>
int main(void)
{
int n = 64;
int *pn = &n;
printf("n (before): %d\n", n);
*pn = 100;
printf("n (after): %d\n", n);
printf("Address of n: %p", (void *)pn);
return 0;
}
Output:
n (before): 64
n (after): 100
Address of n: 003AF7CC
Pointers need to POINT! A pointer is a variable that contains the adress of another variable.
int *pc;
This pointer contains some garbage value. Who knows what was in that memory before. The adress that it's pointing to could be an adress of memory that is occupied by another program. You need to assign a valid adress to a pointer if you want to change the value in that adress.
If you want to calculate sum of all the members of an array you would have to initialize the variable sum to 0 because it could contain other values.
The same is with pointers. They have to contain a valid adress of a variable if you want to modify the value.
Your program would work if you comment line *pc = 100;. You can display the adress of an uninitialized pointer but the program will crash if you try to change the value in that adress.

Pointer Assignment, malloc() and free() in C

I'm very new in C programming and I was playing around with malloc(), free() and Pointer Assignment in order to get a better grasp of it.
Here is my code:
#include <stdio.h>
#include <stdlib.h>
#define SIZE 10
void
array_fill(int * const arr, size_t n)
{
size_t i;
for (i = 0; i < n; i++)
arr[i] = i;
}
void
array_print(const int * const arr, size_t n)
{
size_t i;
for (i = 0; i < n; i++)
printf("%d ", arr[i]);
printf("\n");
}
int
main(int argc, char ** argv)
{
int * p1, * p2;
p1 = (int *) malloc(SIZE * sizeof(int));
p2 = p1;
array_fill(p1, SIZE);
array_print(p1, SIZE);
array_print(p2, SIZE);
printf("\nFREE(p1)\n");
free(p1);
array_print(p2, SIZE);
return (0);
}
Compiling it with gcc test.c -o test and running it with ./test:
0 1 2 3 4 5 6 7 8 9
0 1 2 3 4 5 6 7 8 9
FREE(p1)
0 0 2 3 4 5 6 7 8 9
p2 = p1, does it mean that p2 points to the same value as p1?
After freeing p1 why I can still print p2 (Value of index 1 is different)? Am I causing any memory leak or pointer dangling?
Is this normal to be able to print p2 even p1 is freed?
p2 = p1, does it mean that p2 points to the same value as p1?
Yes, after the assignment both pointers point to the same region of memory.
After freeing p1 why I can still print p2 (Value of index 1 is different)? Am I causing any memory leak or pointer dangling?
Yes, once you free p1, the p2 pointer becomes dangling. Accessing anything through it is undefined behavior.
Is this normal to be able to print p2 even p1 is freed?
No, it is undefined behavior.
Don't let the fact that you see numbers that look like ones that you have previously confuse you: any resemblance with the numbers that were there before you called free is a complete coincidence. Unfortunately, coincidences like that make problems with dangling pointers extremely hard to find. To aid with this problem, memory profiler programs take over the free-d region, and deliberately write some garbage values into it. This makes detection faster, but it is not bulletproof.
1) Yes
2) You are accessing freed memory which is a bad thing.
3) The behaviour is undefined. It could work, it could crash, it could print garbage, it could suck us all into a black hole (though unlikely)
Yes, p2 points to same area as p1.
Apparently, memory is freed, but it wasn't reused yet (though, one value was already overwritten). After freeing memory, you're not supposed to access it via another pointer.
It could lead to undefined behavior. In your case, it printed corrupted array. It also could've crashed with segmentation fault (if memory page no longer belonged to your application). Behavior could change depending on OS, compiler and other stuff, so it's better to avoid such practices.
1) The values stored in pointers are memory addresses. Which means, two pointers with the same value points to the same address, which means the same memory region.
2) Freeing the pointer p1 only sets the value of p1 to NULL and says that the memory pointed to by p1 is free to use and it's no longer reserved. But it doesn't erase the memory. It still holds it's value. But accessing it by another pointer that still has the address is an undefined behavior as it can be reserved for another thing.
3) Yes it's normal as it's already explained in (2); the memory region is not erased or set to 0s and p2 still points to the address which means it still prints the same value
Note that if the memory region is reserved by later by malloc, printing p2 may print another value if the memory region is modified.

Resources