Sono lieto di vedere che hai apprezzato le mie osservazioni.
Il punto da te evidenziato e' della massima importanza:
che differenza c'e' fra funzione(plist pp) e funzione(plist *pp)?
(dove plist e' un tipo puntatore a una struct nodo,
in particolare al primo nodo di una lista).
Cominciamo con un esempio banale; facciamo finta che in un nostro
programma ci serve una funzione che raddoppi un valore contenuto
in una variabile numero intero; potremmo pensare di scrivere
una funzione cosi':
void raddoppia(int num)
{
num=num*2;
}
per poi usarla nel programma principale:
....
int a;
....
raddoppia(a);
....
la cosa non funzionerebbe, dopo la chiamata della funzione
la variabile a continuerebbe ad avere il valore precedente.
Il motivo e' che abbiamo passato il parametro di input per valore
(non per indirizzo). Dopo la chiamata della funzione,
la variabile num viene collocata nello stack
(con il valore di a, insieme alle eventuali variabili locali)
e viene cancellato quando si esce dalla funzione.
Tutte le operazioni su num producono un effetto confinato
all'interno della funzione, ma la variabile esterna a non viene toccata.
Se vogliamo che il valore di a sia modificato dopo la chiamata
della funzione, dobbiamo passare in input il suo indirizzo,
non il suo valore:
void raddoppia(int *p_num)
{
(*p_num)=(*p_num) * 2;
}
....
raddoppia(&a);
....
cosi' funziona, perche' l'indirizzo (anch'esso inserito nello stack)
ha un corrispondente in un puntatore esterno alla funzione, &a.
Un discorso analogo si puo' fare per le funzioni che manipolano
liste concatenate, specialmente per quelle che devono introdurre
delle variazioni nel puntatore di testa, che serve ad accedere
al primo nodo: dobbiamo passare alla funzione non il puntatore,
ma il suo indirizzo: la funzione deve ricevere in input
un puntatore a un altro puntatore.
In questo modo, i cambiamenti operati sul puntatore all'interno
della funzione si estendono anche al corrispondente esterno.
Vediamo la sintassi (per generalizzare non uso plist,
ma il tipo di dato struct nodo, cosi' pp punta a un puntatore
che punta a una struct nodo):
int cancella(nodo **pp, int num) // num e' il numero da cancellare
{
nodo *p_altro_nodo;
....
// visualizzare il campo info
printf("%d", (*pp)->info);
// si puo' anche cosi'
printf("%d", (**pp).info);
....
// deallocare la memoria
free(*pp);
....
// assegnare al puntatore di testa
// il puntatore a un altro nodo
*pp=p_altro_nodo;
....
}
main()
....
nodo *p_lista; // p_lista punta al primo nodo
....
// chiamata della funzione
cancella(&p_lista, num_da_cancellare);
....
Per semplificare, evitando di dover usare la notazione
per i doppi puntatori, si puo' creare un puntatore locale
dentro la funzione:
int cancella(nodo **pp, int num)
{
nodo *p_primo_nodo;
nodo *p_altro_nodo;
....
p_primo_nodo=*pp;
....
// visualizzare il campo info
printf("%d", p_primo_nodo->info);
// si puo' anche cosi'
printf("%d", (*p_primo_nodo).info);
....
// deallocare la memoria
free(p_primo_nodo);
....
// assegnare al puntatore di testa
// il puntatore a un altro nodo
p_primo_nodo=p_altro_nodo;
....
// prima dell'uscita dalla funzione
// bisogna fare l'assegnamento seguente,
// altrimenti il puntatore esterno non cambia
*pp=p_primo_nodo
....
}
Le tue funzioni passate di tipo f(plist a), dove a e' il puntatore di testa,
potevano anche funzionare regolarmente, ma questo capitava solo
nei casi in cui non veniva modificato il puntatore iniziale;
ad es., inserimento alla fine di lista non vuota
o cancellazione di nodo diverso dal primo.
Ma visto che ci sono casi in cui bisogna variare il puntatore iniziale
(inserimento/estrazione da pila, cancellazione primo nodo, inserimento
in lista vuota) bisogna usare le funzioni f(plist *a) tutte le volte
in cui bisogna introdurre modifiche nella lista.
Quando si scrivono funzioni che gestiscono liste, bisogna pensare a tutti
i casi particolari possibili e costruire la funzioni in modo che li sappiano
riconoscere e trattare ognuno nel modo dovuto; molti errori nascono
proprio dalla mancata considerazione di tutti i casi possibili.