Nei titoli e nei testi troverete qualche rimando cinematografico (ebbene si, sono un cinefilo). Se non vi interessano fate finta di non vederli, già che non sono fondamentali per la comprensione dei post...

Di questo blog ho mandato avanti, fino a Settembre 2018, anche una versione in Spagnolo. Potete trovarla su El arte de la programación en C. Buona lettura.

martedì 30 giugno 2026

MQTT
come usare MQTT in C - pt.2

Logan: Dove vado non c'è redenzione. E chi la vuole?

Dove eravamo rimasti? Ah, si, si parlava di MQTT (Message Queuing Telemetry Transport), un bel protocollo di comunicazione da studiare e usare. Nello scorso articolo abbiamo cominciato realizzando una implementazione di un semplice Client MQTT usando la libpaho, che è la libreria più usata e (probabilmente) più performante, ma non è l'unica: e quindi, come promesso, in questo secondo articolo mostrerò un altro semplice esempio di Client, questa volta usando la libreria MQTT-C. Ah, il film della frase qui sopra è lo stesso dell'altra volta, lo splendido Logan del bravo James Mangold. E, direi, sono d'accordo con il buon Wolverine/H.Jackman: anche noi programmatori C non abbiamo bisogno di redenzione... o forse si? Beh, chi usa Linux sicuramente no, mentre gli altri... ah ah ah.

...non ho bisogno di redenzione, ho bisogno di un MQTT funzionante!...

Come già anticipato nello scorso articolo, MQTT-C è una libreria semplice e leggerissima, compatibile solo con lo standard MQTT 3.1.1. È ideale per applicazioni embedded (con OS o anche di tipo "bare-metal") ma usabile anche su Linux o alcuni RTOS. La libreria consiste in solo due file sorgente (mqtt.c e mqtt_pal.c) da includere nel progetto e non dipende da librerie di sistema specifiche, rendendola ideale per ambienti con risorse limitate. L'interfaccia di programmazione è relativamente semplice: si può inizializzare un Client, connettersi a un broker, pubblicare messaggi e sottoscriversi ai "topic" con poche chiamate dirette.

Per l'uso, è necessario includere l'header mqtt.h (e, indiretamente, l'altro header mqtt_pal.h) e istanziare un oggetto mqtt_client. La ben più complessa Eclipse Paho MQTT C Client Library offre API sincrone e asincrone con supporto nativo per SSL/TLS e threading, mentre la semplice MQTT-C delega la gestione della rete e dell'I/O alle risorse del sistema ospite (Linux, un RTOS embedded o addirittura un "bare-metal"), mantenendo lo stack MQTT dedicato e leggero.

Il Client di esempio che ho scritto oggi è, più o meno, come quello dello scorso articolo: implementa un client che funziona, allo stesso tempo, come Publisher e come Subscriber, usando opportunamente la linea di comando: in questo modo si possono, ad esempio, lanciare due client che scrivono/leggono sui rispettivi topic "incrociati" (e cioè: il client1  pubblica sul topic2  e legge dal topic1  e il client2  pubblica sul topic1  e legge dal topic2)... spero di essere stato chiaro. Comunque, vediamo il codice, compiliamo ed eseguiamo come appena descritto, e sarà tutto più chiaro. Vai col codice!

// mqttclient.c - un semplice client mqtt (con MQTT-C)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include "include/mqtt.h"
#include "templates/posix_sockets.h"

// prototipi locali
void onMessage(void** unused, struct mqtt_response_publish *published);
void* clientDaemon(void* client);
void cleanExit(int status, int sockfd, pthread_t *client_daemon);

// funzione main
int main(int argc, char *argv[])
{
// test del numero di argomenti
if (argc != 4) {
// errore: numero errato di argomenti
printf("%s: numero errato di argomenti\n", argv[0]);
printf("uso: %s clientId topicPub topicSub"
"(e.g.: %s client1 test/topic2 test/topic1)\n", argv[0], argv[0]);
return EXIT_FAILURE;
}

// apro il non-blocking TCP socket collegato al broker
int sockfd;
if ((sockfd = open_nb_socket("localhost", "1883")) == -1) {
printf("non posso aprire il socket\n");
cleanExit(EXIT_FAILURE, sockfd, NULL);
}

// creo un client
struct mqtt_client client; // dimensionare i buffer in base ai messaggi previsti in uso
uint8_t sendbuf[2048];
uint8_t recvbuf[1024];
mqtt_init(&client, sockfd, sendbuf, sizeof(sendbuf),
recvbuf, sizeof(recvbuf), onMessage);

// crea una sessione anonima/vuota e invio una richiesta di connessione al broker
const char* client_id = NULL;
uint8_t connect_flags = MQTT_CONNECT_CLEAN_SESSION;
mqtt_connect(&client, client_id, NULL, NULL, 0, NULL, NULL, connect_flags, 400);

// check errori
if (client.error != MQTT_OK) {
fprintf(stderr, "error: %s\n", mqtt_error_str(client.error));
cleanExit(EXIT_FAILURE, sockfd, NULL);
}

// avvio un thread per aggiornare il client (gestisce il traffico I/O del client)
pthread_t client_daemon;
if(pthread_create(&client_daemon, NULL, clientDaemon, &client)) {
printf("non posso avviare il client daemon\n");
cleanExit(EXIT_FAILURE, sockfd, NULL);
}

// mi sottoscrivo a un topic (topicSub in "uso: %s clientId topicPub topicSub")
printf("%s: mi sottoscrivo al topic %s\n", argv[1], argv[3]);
mqtt_subscribe(&client, argv[3], 0);

// pubblico messaggi sull'altro topic (topicPub in "uso: %s clientId topicPub topicSub")
for (int i = 0; i < 3; i++) {
// attendo 2 secondi prima di pubblicare un messaggio
sleep(2);

// pubblico
char payload[256];
snprintf(payload, sizeof(payload), "messaggio %d dal client %s", i, argv[1]);
mqtt_publish(&client, argv[2], payload, strlen(payload) + 1, MQTT_PUBLISH_QOS_0);

// check errori
if (client.error != MQTT_OK) {
fprintf(stderr, "error: %s\n", mqtt_error_str(client.error));
cleanExit(EXIT_FAILURE, sockfd, &client_daemon);
}

printf("Messaggio %d publicato sul topic %s\n", i, argv[2]);
}

// mi disconnetto
sleep(2);
cleanExit(EXIT_SUCCESS, sockfd, &client_daemon);
}

// onMessage - una callback per i messaggi che arrivano sul topic sorvegliato
void onMessage(void** unused, struct mqtt_response_publish *published)
{
char *topic_name = malloc(published->topic_name_size + 1);
memcpy(topic_name, published->topic_name, published->topic_name_size);
topic_name[published->topic_name_size] = '\0';
printf("messaggio ricevuto sul topic %s: \"%s\"\n",
topic_name, (const char*)published->application_message);
free(topic_name);
}

// clientDaemon - un thread che sincronizza l'I/O mqtt
void* clientDaemon(void* client)
{
// loop infinito del thread
for (;;) {
// sincronizzo
mqtt_sync((struct mqtt_client*) client);

// thread sleep (NOTA: in produzione NON usare usleep(3). Usare nanosleep(2))
usleep(100000U);
}

return NULL;
}

// cleanExit - uscita controllata
void cleanExit(int status, int sockfd, pthread_t *client_daemon)
{
// chiudo il socket
if (sockfd != -1)
close(sockfd);

// cancello il thread (NOTA: in produzione NON usare pthread_cancel(3) ma uno stop-flag)
if (client_daemon != NULL)
pthread_cancel(*client_daemon);

// chiudo l'applicazione
exit(status);
}

Come avrete notato, il codice è abbastanza semplice e super-commentato, quindi con c'è molto più da spiegare. Se fate un confronto con l'analogo Client scritto per la libpaho noterete che il codice è veramente molto simile. Faccio solo notare che anche questo esempio si collega a un broker locale (ossia installato sulla macchina stessa di esecuzione), quindi il collegamento è fatto con l'indirizzo: "tcp://localhost:1883". Ma ci sono anche dei broker pubblici disponibili in rete che funzionano esattamente come quello locale. Nel mio caso (Linux) il broker  locale che uso è Mosquitto (disponibile anche per Windows) e funziona alla grande.

L'esempio che ho scritto è ispirato dagli esempi contenuti nella pagina Github di MQTT-C, ho fatto un po' di cambi ma l'ispirazione proviene da lì. Per cui faccio anche notare, come da apposite NOTE che ho aggiunto al codice, che sono presenti alcune cose da cambiare per una eventuale versione di produzione di questo Client. Le NOTE sono:

  • NOTA: in produzione NON usare usleep(3). Usare nanosleep(2): come già visto in un mio vecchio articolo, usleep(3) è deprecata da tempo, e bisogna usare nanosleep(2), anche se è probabile (ma non sicuro) che la usleep(3) della libc sia implementata internamente usando nanosleep(2).
  • NOTA: in produzione NON usare pthread_cancel(3) ma uno stop-flag): come già visto in un mio vecchio articolo, pthread_cancel(3) è una funzione molto problematica di cui sconsiglio l'uso: meglio passare uno stop-flag al thread per forzarne una uscita pulita.

Ed è giunta l'ora di compilare ed eseguire. Compiliamo:

aldo@Linux $ gcc -o mqttclient mqttclient.c mqtt_pal.c mqtt.c

ed eseguiamo in due terminali diversi (possibilmente in quasi-contemporanea, per non perdersi messaggi: se siete troppo lenti, quando il secondo client parte il primo potrebbe già aver pubblicato tutti i suoi messaggi, ah ah ah. Comunque le "strane" sleep(3) che ho aggiunto sono proprio per rendere leggibili i risultati). Il risultato è questo:

terminale 1:

aldo@Linux $ ./mqttclient Client2 test/topic1 test/topic2
Client2: mi sottoscrivo al topic test/topic2
messaggio ricevuto sul topic test/topic2: "messaggio 0 dal client Client1"
Messaggio 0 publicato sul topic test/topic1
messaggio ricevuto sul topic test/topic2: "messaggio 1 dal client Client1"
Messaggio 1 publicato sul topic test/topic1
messaggio ricevuto sul topic test/topic2: "messaggio 2 dal client Client1"
Messaggio 2 publicato sul topic test/topic1

terminale 2:

aldo@Linux $ ./mqttclient Client1 test/topic2 test/topic1
Client1: mi sottoscrivo al topic test/topic1
Messaggio 0 publicato sul topic test/topic2
messaggio ricevuto sul topic test/topic1: "messaggio 0 dal client Client2"
Messaggio 1 publicato sul topic test/topic2
messaggio ricevuto sul topic test/topic1: "messaggio 1 dal client Client2"
Messaggio 2 publicato sul topic test/topic2
messaggio ricevuto sul topic test/topic1: "messaggio 2 dal client Client2"

Visto? Anche questo funziona perfettamente!

Ok, per oggi può bastare. Nel prossimo articolo proveremo (udite! udite!) a cambiare linguaggio e implementeremo un Client MQTT in Go: non è la prima volta che affrontiamo il gran linguaggio Go su queste pagine eh! Non dovrebbe sorprendervi! E, come già vi ho raccomandato l'altra volta, non trattenete il respiro nell'attesa! (può nuocere gravemente alla salute).

Ciao, e al prossimo post!

domenica 31 maggio 2026

MQTT
come usare MQTT in C - pt.1

Charles Xavier: Due giorni in viaggio, un solo pasto e non abbiamo chiuso occhio. Lei ha 11 anni e io 90, cazzo!
Logan: Sono 101 ragioni per continuare a muoverci.

MQTT, un titolo secco di una sola parola. E qui ci vuole un film con un titolo (e un contenuto) altrettanto secco: Logan del bravo James Mangold fa proprio al caso nostro. Un film "crepuscolare" che celebra la fine della Saga dei Mutanti, con un Wolverine/H.Jackman e un Prof.X/P.Stewart veramente perfetti. Un film super-raccomandato con frasi come quella sopra che ci introducono al tema di oggi: dopo tanta fatica fatta per studiare e assimilare gli n-mila protocolli di rete disponibili, è il caso di imparare a usarne un'altro? E la risposta è si: in questo articolo parleremo di MQTT!

...facciamo in fretta, se no ci perdiamo il prossimo MQTT!...

Ok, veniamo al dunque: cosa è, esattamente, MQTT (Message Queuing Telemetry Transport)? È un protocollo di messaggistica leggero, basato su standard aperti, di tipo "pubblica/sottoscrivi", progettato per garantire una comunicazione efficiente tra dispositivi in reti con larghezza di banda ridotta, elevata latenza o inaffidabili. È un protocollo relativamente recente: fu sviluppato nel 1999 per il monitoring di oleodotti via satellite, e in seguito, visto che funzionava bene, è diventato lo standard de facto per le applicazioni dell'Internet delle cose (IoT) e dell'IoT industriale (IIoT) grazie al suo impatto minimo sulle risorse e alla sua scalabilità.

Il protocollo lavora usando un'architettura basata su un broker centrale (una specie di server) in cui i dispositivi (client publisher) pubblicano messaggi su argomenti specifici anziché inviare dati direttamente ad altri dispositivi. Gli abbonati (client subscriber) registrano il proprio interesse per questi argomenti, consentendo al broker di filtrare e distribuire i messaggi solo ai destinatari pertinenti. Le caratteristiche principali includono tre livelli di Quality of Service (QoS) per garantire la consegna, messaggi conservati per gli aggiornamenti di stato e supporto per la crittografia e l’autenticazione TLS per garantire la sicurezza. La sua natura binaria e le intestazioni dei messaggi di piccole dimensioni (fino a due byte) lo rendono ideale per microcontrollori con risorse limitate e sensori sensibili al consumo energetico.

Si può anche notare che per pubblicare e sottoscrivere si usa una nomenclatura gerarchica molto simile a quella di un file-system POSIX, quindi con alberi di nomi separati dal carattere "/", e questi alberi possono essere molto nidificati, esattamente (come appena detto) come in un file-system: un'idea molto interessante e azzeccata. Notare che nella terminologia MQTT questi pseudo-pathname, usati per pubblicare e sottoscrivere, vengono chiamati topic.

E non possiamo continuare senza mostrare un po' di codice, no? Ok, per scrivere il codice dobbiamo/possiamo appoggiarci a qualche libreria di quelle disponibili: per il nostro amato C abbiamo due scelte principali:

  • Eclipse Paho MQTT C Client library: è una libreria abbastanza sofisticata e completa, compatibile con i due standard disponibili, MQTT 3.1.1 e MQTT 5.0. È ideale per applicazioni "senza compromessi", su Hardware di buon livello (PC e Server).
  • MQTT-C: è una libreria semplice e leggerissima, compatibile solo con lo standard MQTT 3.1.1. È ideale per applicazioni embedded (con OS o anche di tipo "bare-metal") ma usabile anche su Linux o alcuni RTOS.

Per questo articolo ho scritto un semplice esempio che usa la libpaho (versione 3.1.1), ed è un codice che implementa un client che funziona, allo stesso tempo, come Publisher e come Subscriber, usando opportunamente la linea di comando: in questo modo si possono, ad esempio, lanciare due client che scrivono/leggono sui rispettivi topic "incrociati" (e cioè: il client1  pubblica sul topic2  e legge dal topic1  e il client2  pubblica sul topic1  e legge dal topic2)... spero di essere stato chiaro. Comunque, vediamo il codice, compiliamo ed eseguiamo come appena descritto, e sarà tutto più chiaro. Vai col codice!

// mqttclient.c - un semplice client mqtt (con libpaho)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include "MQTTClient.h"

#define QOS 1
#define TIMEOUT 10000L

// prototipi locali
int onMessage(void *context, char *topicName, int topicLen, MQTTClient_message *message);

// funzione main
int main(int argc, char *argv[])
{
// test del numero di argomenti
if (argc != 4) {
// errore: numero errato di argomenti
printf("%s: numero errato di argomenti\n", argv[0]);
printf("uso: %s clientId topicPub topicSub"
"(e.g.: %s client1 test/topic2 test/topic1)\n", argv[0], argv[0]);
return EXIT_FAILURE;
}

// creo un client
MQTTClient client;
MQTTClient_create(&client, "tcp://localhost:1883",
argv[1], MQTTCLIENT_PERSISTENCE_NONE, NULL);

// set delle callback: in questo caso solo la onMessage()
MQTTClient_setCallbacks(client, NULL, NULL, onMessage, NULL);

// set delle opzioni di connessione
MQTTClient_connectOptions conn_opts = MQTTClient_connectOptions_initializer;
conn_opts.keepAliveInterval = 20;
conn_opts.cleansession = 1;

// mi collego
int rc;
if ((rc = MQTTClient_connect(client, &conn_opts)) != MQTTCLIENT_SUCCESS) {
printf("errore di connessione: codice %d\n", rc);
return EXIT_FAILURE;
}

// mi sottoscrivo a un topic (topicSub in "uso: %s clientId topicPub topicSub")
printf("%s: mi sottoscrivo al topic %s\n", argv[1], argv[3]);
MQTTClient_subscribe(client, argv[3], QOS);

// pubblico messaggi sull'altro topic (topicPub in "uso: %s clientId topicPub topicSub")
MQTTClient_message pubmsg = MQTTClient_message_initializer;
for (int i = 0; i < 3; i++) {
// attendo 2 secondi prima di pubblicare un messaggio
sleep(2);

// pubblico
char payload[256];
snprintf(payload, sizeof(payload), "messaggio %d dal client %s", i, argv[1]);
pubmsg.payload = (void*)payload;
pubmsg.payloadlen = strlen(payload);
pubmsg.qos = QOS;
pubmsg.retained = 0;
MQTTClient_deliveryToken token;
MQTTClient_publishMessage(client, argv[2], &pubmsg, &token);
MQTTClient_waitForCompletion(client, token, TIMEOUT);
printf("Messaggio %d publicato sul topic %s\n", i, argv[2]);
}

// mi disconnetto
sleep(2);
MQTTClient_disconnect(client, TIMEOUT);
MQTTClient_destroy(&client);
return EXIT_SUCCESS;
}

// onMessage - una callback per i messaggi che arrivano sul topic sorvegliato
int onMessage(void *context, char *topicName, int topicLen, MQTTClient_message *message)
{
printf("messaggio ricevuto sul topic %s: \"%.*s\"\n",
topicName, message->payloadlen, (char *)message->payload);
MQTTClient_freeMessage(&message);
MQTTClient_free(topicName);
return 1;
}

Come avrete notato, il codice è abbastanza semplice e super-commentato, quindi con c'è molto più da spiegare. Faccio solo notare che questo esempio si collega a un broker  locale (ossia installato sulla macchina stessa di esecuzione), quindi il collegamento è fatto con l'indirizzo: "tcp://localhost:1883". Ma ci sono anche dei broker pubblici disponibili in rete che funzionano esattamente come quello locale. Nel mio caso (Linux) il broker  locale che uso è Mosquitto (disponibile anche per Windows) e funziona alla grande.

E, come detto sopra, compiliamo:

aldo@Linux $ gcc -o mqttclient mqttclient.c -lpaho-mqtt3c

ed eseguiamo in due terminali diversi (possibilmente in quasi-contemporanea, per non perdersi messaggi: se siete troppo lenti, quando il secondo client parte il primo potrebbe già aver pubblicato tutti i suoi messaggi, ah ah ah. Comunque le "strane" sleep(3) che ho aggiunto sono proprio per rendere leggibili i risultati). Il risultato è questo:

terminale 1:

aldo@Linux $ ./mqttclient Client2 test/topic1 test/topic2
Client2: mi sottoscrivo al topic test/topic2
Messaggio 0 publicato sul topic test/topic1
messaggio ricevuto sul topic test/topic2: "messaggio 0 dal client Client1"
Messaggio 1 publicato sul topic test/topic1
messaggio ricevuto sul topic test/topic2: "messaggio 1 dal client Client1"
Messaggio 2 publicato sul topic test/topic1
messaggio ricevuto sul topic test/topic2: "messaggio 2 dal client Client1"

terminale 2:

aldo@Linux $ ./mqttclient Client1 test/topic2 test/topic1
Client1: mi sottoscrivo al topic test/topic1
messaggio ricevuto sul topic test/topic1: "messaggio 0 dal client Client2"
Messaggio 0 publicato sul topic test/topic2
messaggio ricevuto sul topic test/topic1: "messaggio 1 dal client Client2"
Messaggio 1 publicato sul topic test/topic2
messaggio ricevuto sul topic test/topic1: "messaggio 2 dal client Client2"
Messaggio 2 publicato sul topic test/topic2

Visto? Funziona perfettamente!

Ok, per oggi può bastare. Nel prossimo articolo proveremo a usare la leggerissima libreria MQTT-C. E, come spesso vi raccomando, non trattenete il respiro nell'attesa! (può nuocere gravemente alla salute).

Ciao, e al prossimo post!

giovedì 30 aprile 2026

The (Duel)Lists
come usare le Linked List in C - pt.3

Armand d'Hubert: Ma questa tua opera di mezzana non è fuori moda?
Léonie: Quello che è sensato non passa mai di moda.

(...una premessa: questo post (parte 3 di 3) è un remake di un mio vecchio post. Ma, anche se tratta lo stesso argomento, amplia e perfeziona un po' il discorso è mi è sembrato il caso di riproporlo. Leggete e mi direte...)

Nell'ultimo articolo avevo promesso che avrei concluso l'argomento linked list descrivendo una variazione interessante, le doubly linked list. E qui casca a fagiolo la citazione qui sopra:  in The Duellists (I duellanti) di Ridley Scott, la bella Léonie (Meg Wynn Owen) dice che "Quello che è sensato non passa mai di moda", esattamente come non passerà mai di moda la programmazione low-level del nostro amato C: in molti casi si può programmare aiutandosi con librerie già fatte (quelli del lato oscuro della forza ne sanno qualcosa, usano la STL anche per legarsi le scarpe) ma, quando è necessario, bisogna "sporcarsi le mani" con le basi della programmazione, e quindi le basi bisogna conoscerle bene, bisogna conoscerle come le proprie tasche. Avete mai provato a scrivere "scrivere" una libreria invece di "usarla"? Le basi, amici miei, le basi... E, come avrete già intuito, anche in questo articolo parleremo di linked list!

...le linked list non passeranno mai di moda...

Le doubly linked list sono una interessantissima variazione su tema delle linked list classiche. L'unica differenza apparente consiste nella presenza nei nodi, oltre al famoso campo next, anche di un campo prev (che sta per previous). E quindi, ogni node contiene un pointer anche al node precedente, e questa piccola aggiunta permette interessanti sviluppi. Prima di andare avanti è il caso di fare un breve riepilogo dei pro e contro delle liste doppie rispetto a quelle singole:

Vantaggi delle doubly linked list

  • È possibile attraversare la lista in due direzioni (avanti e indietro). In alcune applicazioni pratiche questa e`una notevole semplificazione (e.g.: in un browser la navigazione avanti/indietro o la gestione della cronologia).
  • L'eliminazione di un nodo è più semplice e rapida rispetto alle linked list, poiché non è necessario, per modificare i collegamenti, mantenere un pointer al nodo precedente mentre si scorre la lista.
  • Inoltre, l'inserimento e la cancellazione agli estremi della lista avvengono in tempo costante O(1) se si dispone dei pointer a head e alla coda.
  • Questa struttura è ideale per implementare funzionalità di undo/redo, deque (code con due estremità) e cache MRU/LRU.

Svantaggi delle doubly linked list

  • C'è un maggiore consumo di memoria, poiché ogni ha due pointer (ai node precedente e successivo) oltre ai dati, aumentando l'overhead rispetto alle linked list.
  • Le operazioni di inserimento e cancellazione generano un overhead maggiore nella gestione dei pointer, rendendo l'implementazione più complessa e error-prone se i pointer non vengono aggiornati correttamente.
  • Infine, le operazioni di ricerca e accesso rimangono in tempo lineare, quindi non offrono vantaggi di velocità di accesso rispetto ad altre strutture come gli array, specialmente per liste di grandi dimensioni.

(...ecco, su quest'ultimo svantaggio apro una parentesi: in realtà non sarebbe il caso di parlare di array: stiamo parlando di liste. Però, già che ci sono, faccio notare che in alcune operazioni gli array sono superiori (ricerca) e in altre sono inferiori (sostituzione). Comunque i punti precedenti (vantaggi e svantaggi) si riferiscono solo a "doubly linked list vs linked list", quindi dimentichiamoci, almeno per il momento, degli array. Chiudo la parentesi...)

E qui ci starebbe bene un piccolo riepilogo:

"In sintesi, le doubly linked lists sono ottime quando le operazioni di inserimento e cancellazione sono frequenti e necessitano di manipolazione bidirezionale, ma non sono ideali se la memoria è limitata o se è necessario un accesso casuale rapido agli elementi (ma quest'ultimo punto è, di nuovo, rispetto agli array, come ho appena descritto nella parentesi qui sopra)."

Ok, bando alle ciance: ho rivisto il codice dell'ultimo articolo per usare le doubly linked list; noterete che il risultato è molto simile, ma, grazie al doppio pointer, ho potuto fare alcune ottimizzazioni, specialmente nella cancellazione dei nodi (che, guarda caso, era proprio uno dei vantaggi descritti sopra). Vai col codice!

// linklist.c - un programma di esempio sulle doubly linked list
#include <stdlib.h>
#include <stdio.h>

// node di una doubly linked list con campo dati
typedef struct snode {
int data;
struct snode *prev;
struct snode *next;
} node_t;

// prototipi locali
node_t *createNode(int data);
void addNode(node_t **head, int data);
void appendNode(node_t **head, int data);
node_t *findNode(node_t **head, int data);
void delNode(node_t **head, int data);
void cleanList(node_t **head);
void printList(node_t **head);

// funzione main
int main()
{
// init lista vuota e inserisco con appendNode() 5 nodi con data = indice del loop
node_t *head = NULL;
for (int i = 0; i < 5; i++)
appendNode(&head, i);

// mostro la lista
printList(&head);

// cerco un node: quello con data == 3
findNode(&head, 3);

// cancello un node: quello con data == 3
delNode(&head, 3);

// cerco il node cancellato (quello con data == 3)
findNode(&head, 3);

// mostro di nuovo la lista: deve mancare il node con data == 3
printList(&head);

// cancello l'intera lista
cleanList(&head);

// mostro di nuovo la lista: deve essere vuota
printList(&head);

return 0;
}

// createNode - crea un node vuoto
node_t *createNode(int data)
{
// alloco un nuovo node e lo ritorno
node_t *node = malloc(sizeof(node_t));
node->data = data;
node->prev = NULL;
node->next = NULL;
return node;
}

// addNode - alloca in testa alla lista un node con dati e pointer agli elementi next/previous
void addNode(node_t **head, int data)
{
// creo un nuovo node
node_t *node = createNode(data);

// aggiungo il nuovo node
node_t *current = *head;
if (current == NULL) {
// caso speciale per lista vuota: assegna head lista al nuovo node
*head = node;
}
else {
// assegno all'ultimo node il nuovo node
current->prev = node;
node->next = *head;

// assegno la head lista al nuovo node
*head = node;
}
}

// appendNode - alloca in fondo alla lista un node con dati e pointer agli elementi next/previous
void appendNode(node_t **head, int data)
{
// creo un nuovo node
node_t *node = createNode(data);

// appende il nuovo node
node_t *current = *head;
if (current == NULL) {
// caso speciale per lista vuota: assegna head lista al nuovo node
*head = node;
}
else {
// scorro la lista per trovare l'ultimo node
while (current->next != NULL)
current = current->next;

// assegno all'ultimo node il nuovo node
current->next = node;
node->prev = current;
}
}

// findNode - trova un node nella lista
node_t *findNode(node_t **head, int data)
{
// scorro la lista
node_t *current = *head;
while (current) {
// cerco il node con il <data> corrispondente
if (current->data == data) {
// node trovato!
printf("%s: node trovato: data=%d (myhead=%p prev=%p next=%p)\n\n", __func__,
current->data, (void *)current, (void *)current->prev, (void *)current->next);
return current;
}

// passo al prossimo node
current = current->next;
}

// node non trovato!
printf("%s: node con data=%d non trovato!\n\n", __func__, data);
return NULL;
}

// delNode - rimuove un node dalla lista
void delNode(node_t **head, int data)
{
// cerco il node con il <data> corrispondente
node_t *current = findNode(head, data);

// modifico il riferimento <next> per eliminare dall'elenco il node trovato
if (current->prev)
current->prev->next = current->next;
else
*head = current->next;

// cancello il node e ritorno
printf("%s: cancello data=%d (current=%p prev=%p next=%p)\n", __func__,
current->data, (void *)current, (void *)current->prev, (void *)current->next);
free(current);
return;
}

// cleanList - reset completo della lista
void cleanList(node_t **head)
{
printf("%s: cancello elementi:\n", __func__);

// scorro la lista
node_t *current = *head;
node_t *next;
while (current) {
// salvo il node successivo prima di liberare quello corrente
next = current->next;

// cancello il node
printf("cancello data=%d (current=%p prev=%p next=%p)\n",
current->data, (void *)current, (void *)current->prev, (void *)current->next);
free(current);

// mi sposto sul node successivo
current = next;
}

printf("\n");

// imposto la head lista a NULL per indicare che la lista è vuota
*head = NULL;
}

// printList - mostra la lista
void printList(node_t **head)
{
// scorro la lista e stampo i valori
node_t *current = *head;
printf("%s: lista in %p - elementi contenuti:\n", __func__, (void *)current);
int find = 0;
while (current) {
printf("data=%d (current=%p prev=%p next=%p)\n",
current->data, (void *)current, (void *)current->prev, (void *)current->next);
current = current->next;
find++;
}

if (!find)
printf("%s: nessun elemento trovato!\n", __func__);
else
printf("\n");
}

Interessante no? Avrete anche notato (spero) che ho aggiunto una nuova funzione createNode() che evita di ripetere le stesse linee di creazione in varie funzioni dell'applicazione (serve solo a rendere più leggibile e fluido il codice). E poi, dato che c'ero, ho deciso di variare un po' e ho usato appendNode() invece di addNode() per creare la lista: il risultato è una lista con un ordine crescente in memoria invece che decrescente (come era, invece, l'esempio nell'ultimo articolo) il che la rende un po più user-frendly come interpretazione.

Ah, un’ultima cosa, e scusatemi se mi ripeto: quello sopra è un esempio molto semplificato e, ovviamente, se si vorrà usare come codice di produzione bisognerà aggiungere gli opportuni controlli (specialmente sull'esito delle malloc(3)) e gli opportuni trattamenti degli errori delle funzioni (che dovranno ritornare, quando necessario, valori significativi invece di void). Non dimenticatevene mai!

E anche questa volta: cosa manca per finire l'articolo? Ah, si, compilare ed eseguire. Vediamo un po:

aldo@Linux $ gcc -Wall -Wextra -pedantic linklist.c -o linklist
aldo@Linux $ ./linklist
printList: lista in 0x612757d4b2a0 - elementi contenuti:
data=0 (current=0x612757d4b2a0 prev=(nil) next=0x612757d4b2c0)
data=1 (current=0x612757d4b2c0 prev=0x612757d4b2a0 next=0x612757d4b2e0)
data=2 (current=0x612757d4b2e0 prev=0x612757d4b2c0 next=0x612757d4b300)
data=3 (current=0x612757d4b300 prev=0x612757d4b2e0 next=0x612757d4b320)
data=4 (current=0x612757d4b320 prev=0x612757d4b300 next=(nil))

findNode: node trovato: data=3 (myhead=0x612757d4b300 prev=0x612757d4b2e0 next=0x612757d4b320)

findNode: node trovato: data=3 (myhead=0x612757d4b300 prev=0x612757d4b2e0 next=0x612757d4b320)

delNode: cancello data=3 (current=0x612757d4b300 prev=0x612757d4b2e0 next=0x612757d4b320)
findNode: node con data=3 non trovato!

printList: lista in 0x612757d4b2a0 - elementi contenuti:
data=0 (current=0x612757d4b2a0 prev=(nil) next=0x612757d4b2c0)
data=1 (current=0x612757d4b2c0 prev=0x612757d4b2a0 next=0x612757d4b2e0)
data=2 (current=0x612757d4b2e0 prev=0x612757d4b2c0 next=0x612757d4b320)
data=4 (current=0x612757d4b320 prev=0x612757d4b300 next=(nil))

cleanList: cancello elementi:
cancello data=0 (current=0x612757d4b2a0 prev=(nil) next=0x612757d4b2c0)
cancello data=1 (current=0x612757d4b2c0 prev=0x612757d4b2a0 next=0x612757d4b2e0)
cancello data=2 (current=0x612757d4b2e0 prev=0x612757d4b2c0 next=0x612757d4b320)
cancello data=4 (current=0x612757d4b320 prev=0x612757d4b300 next=(nil))

printList: lista in (nil) - elementi contenuti:
printList: nessun elemento trovato!

Beh, anche questa volta i risultati parlano chiaro, no? E corrispondono nuovamente ai passi indicati nel codice (che non sto a ripetere qui, piuttosto leggete i commenti super-dettagliati!).

Ok, credo che per un po' potremo accantonare questo interessante (spero) argomento delle linked list. Cosa ci riserverà il futuro? Boh, in questo momento non lo so, ma sicuramente sarà interessantissimo!

Ciao, e al prossimo post!