Archivi tag: output parameters

Restituire più valori in Arduino “alla C”: output parameters con puntatori – lezione 2/2

Nella lezione precedente abbiamo visto come ottenere più risultati da una funzione usando variabili globali, riferimenti e struct.
In questa seconda lezione facciamo un passo “più vicino al C puro” e affrontiamo una tecnica molto diffusa nei linguaggi embedded: gli output parameters con puntatori.

Cosa intendiamo quando parliamo di puntatori?
Una funzione in genere può restituire un solo valore con return, ma può anche ricevere gli indirizzi di memoria delle variabili che vogliamo aggiornare, in pratica le diciamo: “scrivi i risultati direttamente qui”. Questo approccio è estremamente comune in librerie e API, è efficiente, ma richiede più attenzione: con i puntatori entrano in gioco concetti come indirizzo (&), dereferenziazione (*) e la necessità di evitare errori (ad esempio puntatori nulli).

Quando usiamo i puntatori come parametri di uscita:

  • i parametri in ingresso possono essere valori normali (int numeratore)
  • i parametri in uscita sono puntatori (int *quoziente) perché indicano l’indirizzo della variabile da modificare
  • dentro la funzione, per modificare la variabile “esterna”, dovete usare *:
    • *quoziente = ...; significa “scrivi nella variabile puntata da quoziente”

La regola mentale utile è la seguente:

  • &variabile -> “dammi l’indirizzo”
  • *puntatore -> “vai a quella variabile e usala”

Esempio 01: scambio di due valori con puntatori (swap)

nello sketch che segue viene chiarito come:

  • usare & nella chiamata
  • usare * nella funzione
  • la funzione modifica le variabili chiamanti
/*
  Prof. Maffucci Michele
  15.02.26
  Scambia due valori usando puntatori (stile C).
*/

void scambiaConPuntatori(int *a, int *b) {
  if (a == nullptr || b == nullptr) {
    return; // sicurezza: evita dereferenziazioni invalide
  }

  int temporaneo = *a;
  *a = *b;
  *b = temporaneo;
}

void setup() {
  Serial.begin(9600);
  randomSeed(analogRead(A0));
}

void loop() {
  int n1 = random(10);
  int n2 = random(10);

  Serial.print("Valori prima dello scambio (n1, n2): ");
  Serial.print(n1);
  Serial.print(", ");
  Serial.println(n2);

  scambiaConPuntatori(&n1, &n2);

  Serial.print("Valori dopo lo scambio (n1, n2):  ");
  Serial.print(n1);
  Serial.print(", ");
  Serial.println(n2);

  Serial.println();
  delay(1000);
}

La funzione non riceve copie dei numeri, ma riceve gli indirizzi di n1 e n2. Quando scrive *a = ..., sta aggiornando proprio n1 (e analogo per n2).

Esempio 02: divisione con resto (quoziente + resto) e gestione errore

Nello sketch che segue viene mostrato:

  • una funzione che produce due output;
  • un valore booleano di ritorno per indicare successo/errore;
  • controllo se viene fatta una divisione per zero;
  • validazione di puntatori (nullptr).
/*
  Prof. Maffucci Michele
  15.02.26
  Calcola quoziente e resto usando puntatori come parametri di uscita.
*/

bool dividiConRestoPtr(int numeratore, int denominatore, int *quoziente, int *resto) {
  if (denominatore == 0 || quoziente == nullptr || resto == nullptr) {
    return false;
  }

  *quoziente = numeratore / denominatore;
  *resto = numeratore % denominatore;
  return true;
}

void setup() {
  Serial.begin(9600);
  randomSeed(analogRead(A0));
}

void loop() {
  int a = random(0, 101);
  int b = random(0, 11); // include lo zero per testare l'errore

  int q = 0;
  int r = 0;

  Serial.print("Operazione: ");
  Serial.print(a);
  Serial.print(" / ");
  Serial.println(b);

  if (dividiConRestoPtr(a, b, &q, &r)) {
    Serial.print("Quoziente: ");
    Serial.println(q);

    Serial.print("Resto: ");
    Serial.println(r);
  } else {
    Serial.println("Errore: divisione per zero o parametri non validi.");
  }

  Serial.println();
  delay(1500);
}

Approfondimento

  • return comunica se l’operazione è riuscita
  • gli output veri e propri vengono scritti nei parametri *quoziente e *resto

È una struttura molto comune anche in librerie C, dove spesso vedete funzioni del tipo:

  • bool readSensor(int *value);
  • int parseData(..., char *outBuffer); (qui l’output è un buffer)

Errori tipici con i puntatori e come evitarli

Errore 01: dimenticare & nella chiamata

scambiaConPuntatori(n1, n2);

state passando interi dove la funzione si aspetta indirizzi -> errore di compilazione (oppure warning serio).

Versione corretta:

scambiaConPuntatori(&n1, &n2);

Errore 02: dereferenziare un puntatore nullo

Dentro una funzione, *p = ... è pericoloso se p è nullptr

per questo motivo negli esempi trovate sempre:

if (p == nullptr) return false;

Errore 03: confondere * e &

Come indicato ad inizio post:

  • &si usa quando chiamate per dare l’indirizzo
  • * si usa dentro la funzione per leggere/scrivere la variabile puntata

Regola generale: quando scegliere i puntatori e quando no

Mi ripeterò però ritengo utile scriverlo.

I puntatori servono quando volete dire a una funzione:

“Non ti sto passando un valore. Ti sto passando dove si trova quel valore in memoria, così puoi modificarlo direttamente.”

dovete fare attenzione perché state lavorando con indirizzi di memoria.

Quando usare i puntatori

01. Quando lavorate in “stile C” o con librerie in C

Molte librerie e molti esempi “embedded” storici sono scritti così:

    • la funzione ritorna true/false (o un codice) per dire se tutto ok
    • i risultati veri e propri vengono scritti nei parametri puntatore

Esempio:

bool leggiSensore(int *valore);

02. Quando dovete far riempire un “contenitore” (buffer/array)

Se dovete far scrivere a una funzione dentro un array o una stringa, spesso userete puntatori.

Esempio:

    • leggere dati e salvarli in un array di byte
    • costruire una stringa di output

In quel caso, “passare l’indirizzo” è naturale perché l’array è già, di fatto, un’area di memoria.

Quando NON usare i puntatori

01. Quando potete usare i riferimenti (&) in Arduino (C++)

Se state scrivendo codice Arduino “normale”, i riferimenti fanno lo stesso lavoro dei puntatori (modificano variabili esterne), ma sono più leggibili e meno soggetti a errori.

Con riferimento:

void scambia(int &a, int &b);

Con puntatori:

void scambia(int *a, int *b);

Per chi inizia con la programmazione, il riferimento comunica meglio:

“questa variabile può essere modificata dalla funzione”

02. Quando il vostro obiettivo è imparare le l’uso delle funzioni senza complicarvi la vita

I puntatori aggiungono concetti nuovi:

    • & (indirizzo)
    • * (dereferenziazione)
    • rischio di puntatori nulli / sbagliati

Se state iniziando con la programmazione e volete capire “come passare dati alle funzioni”, come spiegato nella lezione precedente, è spesso meglio partire da:

    • per valore (copia)
    • per riferimento (modifica)
    • struct (pacchetto di dati)

Buon Coding a tutti 🙂