Let's not be too abstract here.
A pointer is a 32-bit or 64-bit (depending on your target architecture) variable which contains a memory address. It's as simple as that.
Now the C language adds abstractions to make it easier to program with variables containing addresses.
Imagine an array of integers, which represents memory (each number is a cell):
Address: 0 1 2 3 4 5 6 7 8 9 ...
Value: 1 8 9 3 4 2 8 1 5 0 ...
Let's assign the name var
to the second cell (which contains the value 8). Whenever I read var
, I read the content of cell #1 (counting from 0).
What if I want to call a function which needs to modify the second cell? I can't pass var
, since this would pass the value 8, and it doesn't help. I need to pass the location of the cell, which is #1. Therefore I pass &var
, which means exactly this: cell #1. Now the function has its own parameter variable, let's call it param
, which contains the value 1 (address of var
). If the function sets param
to 3, then it's just replacing the address 1 with 3. The memory array above remains unchanged. By setting *param
, though, we're setting the value at address param
(1) to 3, so our first 8 above becomes 3.
What would be **param
, then? It is the equivalent of *(*param)
, which is: the value at address (the value at address param
(1)). The value at address 1 is 8, and the value at address 8 is 9.
A common use case of a double pointer is for a function to return a C string by parameter. A C string variable (char *
) is the address of the first character of a string in memory. If you want to return a C string by parameter, this parameter needs to have the type char **
, that is, the address of the address of the first character of a C string.