In C, a pointer of type T indicates where some data of type T stored. To save specific things, I will talk about T = int below.
The simplest use of a pointer can be to point to a single value:
int a = 42; int *pa = &a;
Now *pa and a same and equal to 42 . In addition, *pa and pa[0] equivalent: for example, you can do:
*pa += 1; pa[0] += 1; a += 1;
In fact, the C compiler automatically translates pa[0] to *(pa+0) .
A pointer may indicate a location within a data sequence:
int arr[] = { 1, 2, 3 }; int *parr = arr;
Now our memory looks like this:
+---+---+---+ arr: | 1 | 2 | 3 | +---+---+---+ +------+ | | parr | ----+ +------+
parr is a pointer that points to the first arr element in the figure above. By the way, parr also has a field around it, because we need to store the parr object somewhere in memory. The parr value is the address of the first arr element.
Now we can use parr to access the arr elements:
arr[0] == parr[0]; /* true */ parr[1]++; /* make arr[1] equal to 3 */
Thus, a pointer can be used to indicate: "I am pointing to the first of n elements in the continuous storage of some objects." Of course, you need to know how many objects for this circuit work: but as soon as we remember this, this is an extremely convenient way to access memory in C.
A pointer can also point to dynamically allocated memory:
#include <stdlib.h> size_t n; /* now, obtain a value in n at runtime */ int *p = malloc(n * sizeof *p);
If the call to malloc() succeeds higher, p now points to the first of the adjacent regions allocated for 10 int s. We can use p[0] through p[n-1] in our program.
You probably knew most or all of the above :-), but nevertheless, the above helps to understand what I will say next.
Remember, we said that a pointer can point to a continuous sequence of objects of the same type? The "same type" may be another type of pointer.
#include <stdlib.h> int **pp; pp = malloc(3 * sizeof *pp);
Now pp points to int * . Returning to the previous picture:
+------+------+------+ | | | | +------+------+------+ +------+ | | pp | ----+ +------+
And each of the three fields is int * , which can point to the first element of the adjacent sequence int s:
for (i=0; i < 3; ++i) pp[i] = malloc((i + 1) * sizeof *ppi[i]);
Here we have allocated space for one int in pp[0] , 2 in pp[1] and 3 in pp[3] :
+------+ +---+ pp -------->| |-------->| | +------+ +---+---+ | |-------->| | | +------+ +---+---+---+ | |-------->| | | | +------+ +---+---+---+
So, pp[0] is a pointer to a single int and that int is the only int in the dynamically allocated int s block. In other words, pp[0][0] is int , and points to the top of width-3 above. Similarly, pp[1][0] and pp[1][1] are valid and are two fields below the field pp[0][0] .
The most commonly used pointer to pointer is the creation of a two-dimensional "array" at runtime:
int **data; size_t i; data = malloc(n * sizeof *data); for (i=0; i < n; ++i) data[i] = malloc(m * sizeof *data[i]);
Now, assuming all malloc() succeed, data[0] ... data[n-1] are valid int * values, each of which points to a separate length m adjacent int objects.
But, as I showed above, a pointer to a pointer should not have the same "number of elements" in each of its "lines". The most obvious example is argv in main() .
Now, as you can guess, a 3-level deep pointer, such as int ***p; is ok and can be useful in C.