Es confuso al principio pero debes visualizarlo de esta forma:
Las funciones, al recibir parámetros, reciben copias de los datos. Cuándo reciben un puntero, lo que reciben es una copia del puntero. Jugar con ésto permite al programador poder modificar los valores de una variable externa, como bien sabes, al pasar la referencia de esa variable.
Si miras el código se necesita variar el puntero en sí:
void push(pila *pil, int v) {
pnodo nuevo
= malloc(sizeof(tiponodo
)); nuevo->valor = v;
nuevo->siguiente = *pil;
*pil = nuevo;
}
Como puedes ver en la linea 5 el valor de pil, que en realidad es una dirección de memoria, recibe el valor del puntero nuevo. Si no se hubiera usado un puntero a puntero en la llamada a la función, ésta trabajaría con una copia local de la dirección del puntero pil y, al salir del ámbito de la función push, no habría habido cambios en main, con lo que el programa no haría lo que debería hacer.
La forma de trabajar es la misma que si fuera un dato normal:
Por ejemplo una función declarada así
void cambiar_int(int* i);
ya puedes imaginar que se cambiará el contenido del entero i que se le pase con
cambiar_int(un_numero);
Pues de la misma forma puedes esperarte que a la siguiente función
void cambiar_direccion(int** p);
servirá para que puedas cambiar la dirección a la que apunta un puntero que le pases así:
cambiar_direccion(&un_puntero_a_int);
Aunque cuidado, porqué también se puede referir a que se va a pasar una tabla, pero eso ya es otro tema.