giovedì 21 Novembre 2024
Home / Corsi / Corso C / Lezione 5 – I puntatori (prima parte)
Corso C

Lezione 5 – I puntatori (prima parte)

Ebbene si!  Siamo arrivati al mostro del linguaggio!!!

Questo è forse l’argomento più caratterizzante del linguaggio, lo scoglio per tutti i principianti (ma non solo).

Cercheremo di procedere con calma e con molti esempi, per evidenziare tutte le proprietà di questi elementi.

Molti linguaggi di programmazione, come ad esempio Java o Python non li comprendono, hanno introdotto la così detta Garbage Collection (che in italiano potremmo chiamare spazzatura) in cui finiscono tutti i blocchi di memoria riservati dal programmatore che quando non servono più sono automaticamente liberati.

In C questo non c’è.

Tramite i puntatori il programmatore si riserva blocchi di memoria che poi deve liberare.

Ma basta chiacchiere ed andiamo a cominciare!

Sommario

  1. Soluzione esercizio proposto in Lezione 4
  2. Struttura della memoria di un calcolatore
  3. malloc
  4. Le stringhe
  5. Le funzioni
  6. Riassunto
  7. Esercizi

1 Soluzione esercizio proposto in Lezione 4

2 Struttura della memoria di un calcolatore

Per addentrarci in questo argomento, per prima bisogna aver chiaro in mente come è strutturata la memoria del nostro calcolatore.

Fig1 – Memoria di un Calcolatore

Come illustrato nella Fig1, si tratta di una tabella con due colonne dove la prima è quella degli indirizzi (ADD) la seconda quella delle Celle Di Memoria (CDM).

L’indirizzo, con la sempre crescente necessità di memoria, è passato da 16 a 32 ed ultimamente a 64bit.

Il fattore determinante però è il sistema operativo. Ad esempio un Linux a 32 bit può girare tranquillamente su di una macchina a 64 bit, (l’opposto ovviamente no).

Notiamo poi, che la cella di memoria è di 8 bit, ed è la più piccola quantità di memoria con cui si possa lavorare (ricordate le variabili char ?).

Consideriamo adesso la seguente dichiarazione di un generico codice C:

nella fase iniziale dell’esecuzione, nella memoria verranno riservate locazioni atte a contenere ognuna delle variabili precedentemente introdotte.

A titolo di esempio, facendo riferimento alla variabile x, chiamando con Addr_x il suo indirizzo, in memoria si avrà:

Fig2 – Rappresentazione in memoria di x

In questo caso (x di tipo char lunga 1 byte), l’indirizzo Addr_x+1 sarà libero ed eventualmente utilizzabile per altre variabili; nel caso invece di y, se Addr_y è il suo indirizzo in memoria, sicuramente le locazioni comprese tra Addr_y+1 e Addr_y+7 non saranno disponibili in quanto riservate per contenere una variabile double appunto lunga 8 byte.

A livello di programmazione, l’indirizzo di x lo possiamo conoscere tramite l’operatore di referenziazione &

Fig3 – Operatore di referenziazione

In generale questo operatore può anche essere applicato ad una qualsiasi variabile.

Facendo sempre riferimento alla dichiarazione riportata precedentemente, a è invece un puntatore, ossia una grandezza che immagazzina un indirizzo (ADD). 

Applicando a questo l’operatore di dereferenziazione *, si può conoscere il contenuto della cella di memoria.

Fig4 – Puntatore non inizializzato

Di fatti la Fig4 è una pseudo-rappresentazione della situazione immediatamente dopo la dichiarazione riportata.

Perché pseudo? Perché a, di fatti, dovrebbe contenere l’indirizzo di una cella di memoria, che in una macchina a 32bit, sarebbero in effetti 4 Celle di memoria.

Una rappresentazione più corretta, ma sicuramente più pesante, sarebbe quella di Fig5.

Fig5 – Memoria occupata da un puntatore

D’ora in avanti, per snellire la notazione e per evidenziare solo i concetti, utilizzeremo la notazione di Fig6, dove le celle rappresentate le si devono immaginare sufficientemente capienti per contenere i dati a loro destinati.

Fig6 – Rappresentazione alternativa di un puntatore

Quindi un puntatore inizializzato è:

Fig7 – Puntatore inizializzato

Proviamo adesso ad usare queste grandezze.

3 malloc

Uno degli esempi tipici dei puntatori è quello di definire gli array o vettori che possono avere più dimensioni.

Queste grandezze possono anche essere definite staticamente, ossia di dimensione fissa, vediamo un esempio. Si noti che in C ogni vettore, statico o definito tramite i puntatori, ha come primo elemento l’indice 0.

Osserviamo alcune caratteristiche:

  • Usando lo script di Lezione 0, verrà segnalato un Warning (formato intero per indirizzo di puntatore). E’ stata fatta una forzatura (vedi oltre). Usando invece il semplice comando gcc esempio5_1.c -o esempio5_1 non ci sarà nessun Warning
  • Nella dichiarazione, la dimensione di un vettore statico va espresso tra parentesi quadre (int vettore[4];)
  • Come anticipato, gli elementi saranno vettore[0],…vettore[3]
  • Riportati indirizzi dei vettori, in forma decimale e esadecimale
  • Dalla versione decimale (Warning) è facile osservare che la differenza tra un elemento e l’altro è 4, infatti un intero si rappresenta su 4 byte o 32 bit
  • Dalla seconda rappresentazione invece, ricordando la nota relazione che fa corrispondere ad una cifra esadecimale 4 bit, verifichiamo che la nostra macchina è a 32 bit
  • Infine, se si fa girare il codice più volte, si otterranno locazioni di memoria differenti, legate alla disponibilità di memoria del calcolatore in quel preciso istante.

Nell’esempio seguente vedremo un metodo più standard per verificare la dimensione degli indirizzi e poi l’allocazione dinamica di vettori.

  • l’istruzione sizeof riporta l’occupazione in byte della grandezza testata
  • I 4 tipi di puntatori hanno tutti fornito come risultato 4 byte
  • malloc è la funzione per allocare blocchi di memoria (stdlib.h)
  • potendola applicare a qualsiasi tipo di dati (anche a quelli che impareremo a costruire), restituisce un puntatore tipo generico void a cui è necessario applicare un cast alla grandezza desiderata
  • la quantità di memoria necessaria poi è definita sempre attraverso l’aiuto di sizeof
  • buona norma sempre verificare che l’operazione sia andata a buon fine valutando il valore del puntatore
  • in caso di errore segnaliamo il problema ed usciamo magari con un codice di errore codificato
  • infine, come ultima operazione, se la memoria non serve più, liberiamola con free

I puntatori possono anche essere utilizzati per rappresentare strutture dati a due, tre o più dimensioni (esempio5_3.c)

  • si noti l’associazione vettore puntatore singolo, matrice doppio (ed analogamente con più dimensioni)
  • le operazioni di casting sui tipi appropriati
  • il controllo sul risultato della malloc
  • la liberazione della memoria procedendo a ritroso
  • i test condizionali per abbellire l’uscita

4 Le stringhe

Sono degli array di caratteri e non esistono come tipo nel C.

Appare quindi evidente perché siano state introdotte nella lezione dei puntatori.

Come i vettori possono essere statiche, ma le più usate sono quelle dinamiche.

Una stringa, analizzata carattere per carattere, alla fine ha il così detto terminatore di stringa ossia ‘\0’.

Una stringa si alloca come fatto precedentemente per gli altri tipi.

Il C fornisce tutta una serie di funzioni accessibili includendo string.h

Per le stringhe non vale l’operatore di assegnazione (=) ma si possono valorizzare almeno in quattro modi differenti (esempio5_5.c).

  • il formato di uscita per stampare una stringa è %s
  • l’header string.h presenta molte funzioni che si applicano alle stringhe dalla lunghezza, confronto, ricerca di sottostringhe o caratteri
  • valutando la dimensione minima di una stringa è necessario tenere in conto il terminatore di stringa ‘\0’
  • se allocate come tutti i blocchi di memoria, quando non servono più vanno liberate.

5 Le funzioni

Osservando i codici scritti, si noti quanto appesantisce la notazione, il controllo delle uscite della malloc: sono operazioni necessarie ma rendono il codice estremamente pesante da leggere.

Questo accade in generale quando ci sono blocchi di codice che sono ripetuti una o più volte.

Questa non è una buona programmazione.

Bisogna infatti riutilizzare quanto più possibile sorgenti già scritti, testati, alleggerendo il codice stesso ed evitando di cadere in errori che sono figli del copy&paste (copia ed incolla), come il propagare di errori e compicazione del sorgente.

A tale scopo esistono le funzioni. (esempio5_4.c)

  •  in cima al codice è riportata la lista dei prototipi (ossia la dichiarazione delle funzioni)
  • questi avvisano il compilatore sull’introduzione di altri elementi e consentono anche il controllo sintattico dell’uso  (sia nei tipi sia nelle quantità)
  • in coda al main è riportata la funzione che presenta 2 parametri di ingresso ed un’uscita.
  • ce ne sono con e senza ingressi (rand()) con e senza uscite
  • una descrizione è sempre utile (cosa sono ingressi, uscite e funzionamento)
  • alla riga 20 è usata

L’esempio riportato è assolutamente banale ma scriviamo una funzione per alleggerire esempio5_2 con malloc.

Prima è necessari introdurre 2 macro ossia particolari costrutti che il linguaggio riconosce ed interpreta (macro.c)

Dal listato numerato è evidente il funzionamento delle due macro.

La funzione malloc_chk.c

  • size_t è una ridefinizione standard di unsigned int indipendente dalla piattaforma (quindi capace di contenere anche grosse moli di dati).
  • in caso di errore, questa volta si è usata la funzione exit(codice) che ammette un valore di ritorno (utilizzabile come il valore di return).

Si noti come il main sia più leggibile e chiaro, pur avendo comunque verificato un possibile errore della malloc

6 Riassunto

  • Introduzione alla struttura della memoria di un calcolatore con indirizzi e celle di memoria
  • I puntatori contengono l’indirizzo di una particolare cella di memoria
  • L’operatore unario & consente di conoscere l’indirizzo di una cella di memoria e * la cella di memoria di un puntatore
  • La funzione sizeof consente di conoscere l’occupazione, in termini di byte, di una qualsiasi grandezza C
  • La funzione malloc consente di allocare blocchi di memoria (con l’aiuto di sizeof )che poi devono essere liberati
  • Tale funzione restituisce un puntatore void che poi deve essere “castato” secondo le necessità
  • E’ bene sempre controllare se l’allocazione di memoria sia riuscita o meno
  • Le stringhe sono vettori di caratteri e possono essere statici e dinamici
  • I secondi si creano sempre con la malloc
  • Per queste strutture dati non vale l’operatore di assegnazione (=)
  • Esistono comunque metodi per valorizzare le stringhe
  • Le funzioni sono blocchi di codice che possono essere richiamati quando serve, semplificando il sorgente che le richiama
  • Consentono di riutilizzare programmi e procedure già testate e verificate, senza dover ogni volta ricominciare tutto da capo.

7 Esercizi

Anche questa settimana vediamo un esercizio di cui la soluzione verrà pubblicata nella prossima lezione.

Fig8 – Cubo dati da riservare in memoria

In Fig8 è rappresentato un cubo con dei dati.

A partire dalla traccia riportata:

Allocare nel puntatore triplo cubo una struttura 3x3x3.

Inserire i dati come riportati in figura.

Stampare poi le matrici 3×3 a partire dalla base ed andando su, poi dalla faccia frontale e procedendo in profondità ed infine dalla parete laterale a sinistra procedendo verso destra.

Dopo aver stampato i dati, liberare la memoria.

Con questo terminiamo la prima lezione sui puntatori in cui sono stati anche inseriti elementi che li utilizzano.

Continueremo seguendo la stessa filosofia anche nella prossima lezione: arrivederci!!

Qui il forum di supporto al corso.

Se vuoi restare aggiornato, seguici anche sui nostri social: Facebook, Twitter, Youtube

Se vuoi anche trovare prodotti e accessori Raspberry Pi in offerta, seguici anche su Telegram !!

 

A proposito di arkkimede

Vedi Anche

MagPi in Italiano! Gioca ai Classici Horror Retrò su Raspberry Pi 5

Estratto, tradotto in italiano, di The MagPi N°146 la rivista ufficiale della Fondazione Raspberry Pi.

Powered by themekiller.com