Puntatori.
I puntatori sono uno degli argomenti più difficili della programmazione, cercherò di spiegarlo in modo più semplice e pratico. Iniziamo con la rappresentazione dei dati nella memoria del microcontrollore: nel precedente argomento sulle operazioni a bit abbiamo visto che il blocco di memoria minimo indirizzabile è un byte, cioè possiamo fare riferimento a qualsiasi byte nella memoria del microcontrollore. Quando lavoriamo con le variabili, non pensiamo agli indirizzi e alla posizione dei dati in memoria, utilizziamo semplicemente i loro nomi per leggere/scrivere, passare i nomi come argomenti alle funzioni ed eseguire altre azioni con i dati. Possedere gli indirizzi dei blocchi di dati ti consente di fare molte cose più velocemente e con maggiore efficienza in termini di memoria. Alcuni esempi delle possibilità fornite dai puntatori:
- Usando i puntatori, puoi dividere qualsiasi dato (variabili di tutti i tipi, strutture) in bit per la successiva manipolazione con essi (trasferimento/scrittura/lettura).
- È possibile passare gli indirizzi dei blocchi di dati come argomenti alle funzioni, quindi quando la funzione viene chiamata, le copie delle variabili non vengono create e il codice viene eseguito più velocemente. In altre parole, consentire alla funzione di modificare l'argomento passato.
- Lavorare con la memoria dinamica "direttamente", allocare memoria per variabili e oggetti.
Che cos'è un puntatore? Si tratta di una variabile che contiene l'indirizzo dell'area dati (variabile/struttura/oggetto/funzione, ecc.) nella memoria del microcontrollore, più precisamente il suo primissimo blocco, il byte. Conoscendo l'indirizzo del primo byte in un blocco di dati, puoi ottenere il controllo sui dati a quell'indirizzo, ma devi conoscere la dimensione di questo blocco. Pertanto, durante la creazione di un puntatore, specifichiamo a quale tipo di dati punta, può essere qualsiasi tipo di dati.
Questo argomento è il più breve e "di riferimento" possibile, consiglio di leggere di più su puntatori, riferimenti e loro caratteristiche nel riferimento C++, o su questo sito.
Operatori.
Non abbiamo molti operatori, ma sono una delle funzionalità più potenti del linguaggio C++:
- & - restituisce l'indirizzo in memoria.
- * - accesso (lettura, scrittura) all'indirizzo specificato
- -> - operatore di accesso indiretto a membri e metodi (per puntatori a strutture e classi). È una notazione abbreviata per una costruzione tramite un puntatore: a->b è equivalente a (*a).b.
Possiamo creare un puntatore al tipo di dati desiderato in questo modo:
tipo_dati* nome_puntatore;
tipo di dati * nome_puntatore;
tipo di dati *nome_puntatore;
Sì, puoi confondere con la moltiplicazione, ma il compilatore non confonderà. Tutte e tre le opzioni di registrazione sono equivalenti, puoi incontrarle nel codice o nell'articolo di qualcun altro.
Puntatore a variabili "ordinarie".
Lavorare con un puntatore permette di leggere/modificare il valore di una variabile attraverso il suo indirizzo. Guarda un esempio con commenti:
byte b; // просто переменная типа.
byte
b = 10; // b ora è 10.
byte* ptr; // ptr - variabile "puntatore a un oggetto di tipo byte".
ptr = &b; // ptr - memorizza l'indirizzo di una variabile.
b
*ptr = 24; // la variabile b ora è uguale a 24 (scriviamo all'indirizzo &b).
byte s; // variabile
s.
s = *ptr; // anche la variabile s è ora uguale a 24 (leggere all'indirizzo &b).
Non sembra essere niente di complicato: abbiamo creato un puntatore ptr a byte( byte * ptr) e scritto l'indirizzo della variabile b: ptr = &b. Ora abbiamo potere sulla variabile b tramite il puntatore ptr, possiamo cambiarne il valore come *ptr = valore; oppure leggi come *ptr;.
Proviamo a passare l'indirizzo alla funzione e cambiamo il valore della variabile al suo indirizzo all'interno della funzione. Abbiamo una funzione che prende l'indirizzo di una variabile int e solleva questa variabile al quadrato:
void square(int* val)
{
*val = *val * *val;
}
Ecco come lo useremo:
int value = 7; // creare una variabile
square(&value); // passare il suo indirizzo alla funzione
// qui il valore = 49
Perché questo approccio? Non abbiamo fatto una copia della variabile, abbiamo semplicemente passato l'indirizzo e modificato direttamente i valori.
Puntatori agli array.
Il nome di un array è un puntatore al primo elemento di questo array e specifichiamo il tipo dell'elemento quando si dichiara l'array. Cioè, myArray[0] == *myArray o myArray == &myArray[0]. Per facilitare la scrittura e la lettura del codice vengono introdotte le parentesi quadre, ma in effetti funziona così: a[b] == *(a + b).
Un array è una regione di memoria piena di "variabili" di un tipo specificato e possiamo accedervi per indirizzo. Un paio di esempi su come lavorare con un array senza usare parentesi quadre:
void setup()
{
Serial.begin(9600);
// lavorando senza [] parentesi
byte myArray[] = {1, 2, 3, 4, 5};
// visualizza 1 2 3 4 5
for (byte i = 0; i < 5; i++)
{
Serial.print(*(myArray + i));
Serial.print(' ');
}
Serial.println();
// lavorando con un puntatore separato
int myArray2[] = {10, 20, 30, 40, 50};
int* ptr2 = myArray2; // puntatore alla array
// visualizza 10 20 30 40 50
for (byte i = 0; i < 5; i++)
{
Serial.print(*ptr2);
ptr2++;
Serial.print(' ');
}
}
Presta attenzione al secondo esempio: nel ciclo, il puntatore viene incrementato di uno, ptr2++, "passando" così all'elemento successivo dell'array. Questa disposizione degli array consente di passarli come argomenti alle funzioni senza alcun problema.
Esempio: una funzione che restituisce la somma di tutti gli elementi in un array:
void setup()
{
Serial.begin(9600);
int myArray[] = {1, 2, 3, 4, 5, 6};
Serial.println(sumArray(myArray));
}
int sumArray(int* arrayPtr)
{
int sum = 0;
// sommare la array
for (byte i = 0; i < 6; i++) sum += arrayPtr[i];
return sum; // restituzione
}
Un punto importante: l'array "non sa" quale sia la sua dimensione, è solo un'area di memoria allocata. Per l'universalità di questo approccio, è necessario conoscere in anticipo la dimensione dell'array o passarla come argomento:
void setup()
{
Serial.begin(9600);
int myArray[] = {1, 2, 3, 4, 5, 6};
// visualizza la somma dell'array
Serial.println( sumArray(myArray, sizeof(myArray)/sizeof(int)) );
// ha passato un array e la sua dimensione nel numero di elementi
}
long sumArray(int* arrayPtr, int arrSize)
{
long sum = 0;
for (int i = 0; i < arrSize; i++) sum += arrayPtr[i];
return sum; // restituzione
}
Un punto importante: la funzione deve sapere quale tipo di array le viene passato (nel nostro caso int).
Puntatore a funzione.
La funzione ha anche un proprio indirizzo in memoria, a cui è possibile accedere. Puoi semplicemente chiamare una funzione tramite un puntatore oppure puoi passarla come argomento a un'altra funzione. Un puntatore a funzione viene dichiarato in questo modo:
tipo_dati_restituito (*nome)(argomenti)
Al puntatore può quindi essere assegnato l'indirizzo di qualsiasi funzione solo come nome (l'operatore di indirizzo, come con gli array, non è necessario). Per chiamare una funzione tramite un puntatore, anche l'operatore * non è necessario:
void setup()
{
Serial.begin(9600);
void (*ptrF)(byte a); // puntatore a funzione (dichiarato di seguito)
ptrF = printByte; // fornisce l'indirizzo della funzione printByte
printByte
ptrF(125); // richiama printByte tramite puntatore (visualizza
125)
int (*ptrFunc)(byte a, byte b); // crea un altro puntatore
ptrFunc = sumFunc; // alla funzione sumFunc
// chiama printByte, a cui passiamo il risultato di sumFunc
// tramite puntatore ptrFunc
ptrF(ptrFunc(10, 30)); // ne stampa 40
}
void printByte(byte b)
{
Serial.println(b);
}
int sumFunc(byte a, byte b)
{
return (a + b);
}
void loop()
{
}
Puntatore a strutture/classi.
Le strutture e le classi sono tipi di dati compositi, il meccanismo per interagire con esse tramite i puntatori è leggermente diverso. Creiamo una struttura, un puntatore ad essa, e facciamo riferimento alla struttura attraverso di essa:
struct myStruct
{
byte myByte;
int myInt;
};
// creare una struttura someStruct
myStruct someStruct;
// puntatore di tipo myStruct* per strutturare someStruct
myStruct* ptr = &someStruct;
// scrivere all'indirizzo in someStruct-myInt
ptr -> myInt = 32;
//(*p).myInt = 32; // o così, vedi inizio.
Pertanto, puoi passare strutture di grandi dimensioni da una funzione all'altra senza crearne copie in memoria: il programma verrà eseguito più velocemente. La classe sarà esattamente la stessa.
Puntatore a void.
In tutti gli esempi precedenti, abbiamo creato un puntatore a un tipo di dati noto. Ma cosa succede se si desidera passare un indirizzo a un tipo di dati sconosciuto? Puoi rendere void* ptr - un puntatore a void, di qualsiasi tipo. In futuro, dovrai convertire il puntatore nel tipo di dati desiderato:
float Fval = 0.254; // variabile float
void* ptrV = &Fval; // puntatore a qualsiasi tipo (in questo caso float)
//creato Fptr - puntatore a float
// e convertito sconosciuto ptrV in float
float* Fptr = (float*)ptrV;
// *Fptr ora è 0,254
Qui abbiamo convertito ptrV che era un void* (puntatore a void) a un puntatore float con aiuto (float *). A volte questo può essere conveniente, ad esempio, quando si trasferiscono dati di formati diversi utilizzando una "funzione universale". Puoi anche vedere la conversione del tipo di puntatore tramite cast:
float* Fptr = static_cast<float*>(ptrV);
Dividi in byte.
A volte è necessario trasmettere alcuni dati e poi riceverli dall'altra parte. Oppure scrivere questi dati su un supporto esterno (EEPROM, scheda di memoria, ecc.) e poi rileggerli. Abbiamo bisogno di uno strumento universale che registrerà qualsiasi data e quindi la leggerà correttamente. Per risolvere questo problema è possibile utilizzare i puntatori, ciò avviene come segue: creare un puntatore al tipo di byte, assegnargli l'indirizzo di un blocco dati di qualsiasi tipo mediante conversione (byte*). Ottieni un puntatore al primo byte di dati. Conoscendo la lunghezza (dimensione in byte) del nostro dato, possiamo leggerlo byte per byte semplicemente aggiungendone uno all'indirizzo.
Considera un semplice esempio di divisione di un numero unsigned long di 4 byte usando i puntatori:
// gran numero unsigned long
uint32_t bigVal = 123456789;
// puntatore ptrB all'indirizzo &bigVal
// trasmettere a (byte*)
byte* ptrB = (byte*)&bigVal;
// diviso in byte
byte bigVal_1 = *ptrB;
byte bigVal_2 = *(ptrB + 1);
byte bigVal_3 = *(ptrB + 2);
byte bigVal_4 = *(ptrB + 3);
// proviamo a raccogliere indietro
// bisogno di una nuova variabile
// lo stesso tipo del primo (uint32_t)
uint32_t newBig;
byte* ptrN = (byte*)&newBig;
// e raccogli indietro 4 byte!
*ptrN = bigVal_1;
*(ptrN + 1) = bigVal_2;
*(ptrN + 2) = bigVal_3;
*(ptrN + 3) = bigVal_4;
// a questo punto newBig è 123456789
Pertanto, puoi "analizzare" e "assemblare" qualsiasi set di dati (array di qualsiasi tipo, struttura), conoscendone le dimensioni. Il problema può essere risolto in modo più bello utilizzando un array
di byte per la lettura e la scrittura. Consideriamo esempi con il lancio del tipo di puntatore tramite (byte*), tramite void* e tramite template:
Esempio tramite (byte*).
Esempio tramite void*.
Esempio tramite template.
Questi esempi differiscono solo nel modo in cui l'argomento-indirizzo viene passato ed elaborato:
- Nel primo caso, lanciamo il puntatore a (byte*) quando passiamo l'argomento alla funzione. Passiamo anche la dimensione del blocco di dati usando sizeof().
- Nel secondo caso, abbiamo void* e non importa quale tipo di dati gli vengono passati. Successivamente, traduciamo il puntatore in uint8_t tramite reinterpret_cast. Passiamo anche la dimensione del blocco di dati usando sizeof().
- La terza opzione è attraverso una funzione templare, a cui non interessa affatto il tipo e la dimensione dei dati che ha: accetta i dati per riferimento. Successivamente, facciamo un puntatore al primo byte e, proprio all'interno della funzione, calcoliamo la dimensione del blocco di dati tramite sizeof(). Questa è l'opzione più potente e versatile.
Nota: tutti e tre gli esempi occupano la stessa quantità di memoria.