An analogy:
The stack is a literal stack of small boxes on your desk. Each box represents a function, with the box on the bottom being main(). Each box contains some pieces of paper that represent the local variables of the function. The values of those variables are written on the papers. When main() calls a function the box for that function gets placed (stacked) on top of main() and its variables are placed in it. When that function exits its box is removed and its variables go along with it, so that the box on top of the stack is always the currently executing function. This is effectively how the stack works.
Now the heap is a filing cabinet in the desk drawer. There are no boxes here. Instead there is a long, ordered stack of re-usable papers. Each one is numbered in order. You cannot remove these papers, but you can look them up by number and read or write any of them. In order to avoid accidentally writing over something that you're storing in there, you have a set of paper clips. When you want to use a paper in the drawer, you put a paper clip on it and then write down its number on a note and put that note in the top box. This is the allocation process. In fact, you have a secretary called 'new' that does this for you. You tell the secretary how many pages you need and the secretary finds an appropriate set of pages, paperclips them and tells you the number of the first one. The paper that you're writing the number on is a pointer. The number itself is an address. When you're done using heap memory you tell the number you wrote down to the secretary (using 'delete') and they remove the clip from that allocation so that it can be used again later.
The important thing to remember is that the pointer is a paper in the box and the address is the number that refers to a page in the drawer.
Where it gets a little confusing is that memory isn't really separated this way. The stack is actually just a data structure that's extending into memory. Pointers are just a special numeric type. It's perfectly legal to have a pointer to a stack variable. It's perfectly legal to have a pointer with any value you want. It's just a piece of paper in the box with a number written on it.
Consider the following:
int* allocation = new int[3]; //reserve enough memory for 3 ints and store the address in 'allocation'
int* ptr = allocation; //copy the address in 'allocation' into 'ptr'
*ptr = 1; //set the value of the memory at 'ptr' to 1
ptr++; //increase the vale of ptr - note that this is making ptr point to a different address, not changing the '1' that we just wrote
*ptr = 2; //set the value of the memory at 'ptr' to 2
ptr++; //increase the vale of ptr
*ptr = 3; //set the value of the memory at 'ptr' to 3
delete allocation; //release the allocation pointed to by 'allocation'
//note that if we tried to delete ptr there would be an error because it no longer points to the base of the allocation
If you freeze the program at this point then the memory there should still contain 1,2,3 in order (because nothing has written over it yet).
'allocation' will still point to the '1' and 'ptr' will still point to the '3'.
The memory is no longer reserved. If the program allocates more memory later there's a decent chance that it will re-use this area of memory and change the values there. Since the reservation is gone the memory in question is 'fair game' to be reallocated.
Since the memory is no longer reserved, calling delete on 'allocation' again should cause an error because there's no reservation recorded for that address. However, it's possible to get some 'fun' if something else asks for memory and a new allocation starts at that same address. In that case an extra delete will destroy the new allocation, so don't rely on getting explicit errors (you almost always will in practice, though).
It is legal to delete zero, or nullptr, even though they will never be the base of an allocation. Relying on this for general use is a bad plan. There are some cases where it's useful, but avoid the bad habit of always setting pointers to null after releasing their allocations. If you can't resist setting the pointer then set them to 0x0BAD0BAD or something similar that will provide a recognizable error if you try to release them again. Error messages are good. They tell you something is wrong, and that's something that you want to know about, so in general try to avoid behaviors that obfuscate errors.