giovedì 2 Maggio 2024
Home / Corsi / Corso C / Lezione 8 – Le librerie e le strutture
Corso C

Lezione 8 – Le librerie e le strutture

Benvenuti a questa ottava lezione del corso di programmazione C.

Arrivati a questo momento, sappiamo fare molte cose e potremmo già risolvere svariati problemi.

Oggi iniziamo a vedere alcuni argomenti che non sono direttamente connessi con la programmazione, ma possono sicuramente essere utili: le librerie.

In seguito vedremo le strutture dati, argomento che non conoscevamo la lezione scorsa e per questo non abbiamo messo la data nel file di output dell’esercizio relativo al ponte radio.

Sommario

  1. Le librerie
  2. Le struct
  3. Riassunto

1 Le librerie

Nelle ultime lezioni, abbiamo spesso usato due funzioni che ci aiutavano nel riservare memoria ed aprire i file.

Per ogni listato nuovo, è stato necessario, di volta in volta riportare per esteso queste funzioni. In generale però sebbene l’argomento di interesse fosse altro, è stato necessario comunque accludere questi file.

Se siamo sicuri che queste routine funzionino correttamente, una possibile soluzione potrebbe essere quella di metterle in particolari archivi chiamati librerie.

Ricordate quando abbiamo usato le funzioni matematiche? Qualcuno nel tempo le ha sviluppate e testate ed accluse al linguaggio C. Noi abbiamo solo dovuto includere un header e compilare con una particolare opzione, ma non abbiamo dovuto riportare il codice che implementava le funzioni che abbiamo usato. Adesso cercheremo di fare qualcosa di analogo: creeremo la nostra libreria di utilities.

Fig1 – Fasi per la generazione di un eseguibile a partire dai sorgenti C

Prima di tutto, in Fig1 sono state riportate le varie fasi necessarie per passare da un sorgente C ad un eseguibile. Fino ad ora non ce ne siamo mai resi conto perché il gcc faceva tutto nascondendo tutti questi passi. Del preprocessore ne avevamo parlato, adesso possiamo vedere anche il resto.

Della figura, la parte da mettere in evidenza è lo Step4 in cui si aggiungono le librerie che non fanno parte del codice sorgente. Sebbene siano stati indicati i nomi dei vari comandi che consentono di passare da una fase all’altra, in pratica sarà sempre il gcc che si occuperà di svolgere queste fasi invocando per noi cpp, as ed ld.

Di librerie in C ve ne sono di due tipi: le statiche (static) e le condivide (shared). Di seguito le analizzeremo, vedremo come si costruiscono e come si usano, dopo proveremo a confrontarle per vederne le loro caratteristiche.

1.a Librerie statiche (static library)

Il punto di partenza sono i sorgenti che si vogliono mettere nella libreria. Avevamo detto che si trattava delle due funzioni relative alla malloc ed alla fopen che per comodità riportiamo di seguito:

—————————————

—————————————

  • Abbiamo ripreso la buona norma di commentare il codice che ultimamente avevamo abbandonato
  • E’ presente un header che contiene la dichiarazione delle due funzioni ed i .h necessari alle istruzioni conenute nei due sorgenti
  • In utilities.h è presente un costrutto #ifndef..#define..#endif. Lo scopo dell’utilizzo di queste istruzioni apparirà più chiaro quando vederemo come separare tutto il sorgente in tanti piccoli file. Serve in sostanza ad evitare di includere più volte il medesimo header, causando il problema della ri-dichiarazione delle medesime funzioni o variabili. Questo può accadere, come detto, quando si usano tanti piccoli sorgenti che per esempio devono aprire file o riservare memoria ed ognuno di essi include il medesimo .h, causando appunto la ri-dichiarazione multipla delle medesime funzioni.

Per costruire la libreria prima di tutto è necessario ottenere i file .o

Mandare in esecuzione allora i seguenti comandi:

gcc -c -Wall -O3 -I . fopen_chk.c

gcc -c -Wall -O3 -I . malloc_chk.c

  • -c comunica al compilatore che deve limitarsi a creare il file .o
  • -Wall abilita tutti i messaggi di warning
  • -O3 attua tutti i livelli di ottimizzazione
  • -I . indica di cercare gli eventuali file di header anche nella directory corrente

A questo punto dovreste avere nella cartella in cui siete fopen_chk.o e malloc_chk.o .

L’ultimo passo per ottenere la nostra libreria adesso è:

ar -rcs libutilities.a fopen_chk.o malloc_chk.o

  • ar è il comando che crea la libreria
  • r comunica di inserire tutti i .o nella libreria ed eventualmente sostituire quelli vecchi con i nuovi
  • c dice di costruire la libreria se non esiste già
  • s la indicizza, per consentire una compilazione molto più efficiente.

Se volessimo conoscere i moduli che sono stati inseriti nella libreria, sarebbe sufficiente eseguite il comando

ar -t libutilitis.a

e si otterrebbe la lista delle funzioni contenute.

A questo punto vediamo come usare la nostra libutilities.a. Per questo scopo consideriamo il sorgente esempio5_6.c, dove per la prima volta è stata usata la malloc_chk. Modifichiamo il sorgente come mostrato in seguito:

Quindi abbiamo eliminato la funzione ed incluso utilities.h

Adesso per ottenere l’eseguibili è sufficiente eseguire il comando:

gcc esempio5_6.c -I . -L . -lutilities -o esempio5_6

  • -I . indica di cercare eventuale .h non trovato anche nella cartella corrente
  • -L . simile al precedente indica di cercare un’eventuale libreria in locale
  • -lutilities è la notazione standard per includere una libreria
  • -o indica il nome dell’eseguibile

Tutto ciò però implica che sia la libreria sia utilities.h dobbiamo copiarle sulla nostra macchina ovunque vogliamo usarla. Un’alternativa è quella di usare la filosofia utilizzata dal gcc (o qualsiasi altro compilatore) quando compila un sorgente: esistono infatti delle cartelle standard in cui vengono cercati header e librerie se non trovate. A tal proposito esistono le directory /usr/local/include ed /usr/local/lib. Nella prima si può mettere utilities.h e nella seconda libutilities.a, ricordandosi che essendo cartelle di sistema bisognerà essere root o nel caso del Raspberry Pi usare sudo ossia:

sudo mv utilities.h /usr/local/include

sudo mv libutilities.a /usr/local/lib

A questo punto il comando di compilazione diventa:

gcc esempio6_5.c -lutilites -o esempio6_5

e tutte le volte che vorremo utilizzare quelle funzioni non dovremo che aggiungere al sorgente utilities.h e compilare come mostrato. Come confronto futuro verifichiamo che l’eseguibile (sulla Raspberry Pi) è 8236 byte

1.b Le librerie condivide (shared library)

Si chiamano condivise ma potrebbero anche essere chiamate dinamiche infatti, queste non fanno parte dell’eseguibile come nel caso delle statiche, ma sono caricate a run time. Verificheremo infatti che l’eseguibile compilato con una libreria statica è più grande di quello compilato con una libreria condivisa.

Il punto di partenza è il medesimo e necessita anche qui utilities.h e pur se non presente in locale, il compilatore lo troverà in una delle cartelle canoniche. I file .o questa volta si ottengono in modo leggermente differente ossia:

gcc -c -Wall -O3 -fPIC fopen_chk.c

gcc -c -Wall -O3 -fPIC malloc_chk.c

Rispetto alla versione statica adesso c’è l’opzione -fPIC che è l’acronimo inglese Position Indipendent Code. Questo significa che verrà creato del codice che utilizzerà degli indirizzi relativi, piuttosto che assoluti, in quanto la libreria, potrebbe essere caricata varie volte ed in condizioni differenti, quindi necessita di questa flessibilità.

Fatto ciò, per creare la shared library dobbiamo eseguire il comando

gcc -Wall -O3 -shared -o libsutilities.so fopen_chk.o malloc_chk.o

Per distinguerla da quella precedentemente creata, abbiamo modificato il nome in libsutilities.so

Supponiamo di sposare anche questa in /usr/local/lib e proviamo a compilare esempio5_6.c:

gcc esempio5_6.c -lsutilities -o esempio5_6

(se avessimo mantenuto la libreria in locale, avremmo dovuto anche usare l’opzione -L .).

A questo punto se mandiamo in esecuzione l’eseguibile otteniamo il seguente errore:

./esempio5_6: error while loading shared libraries: libsutilities.so: cannot open shared object file: No such file or directory

questo perché le librerie condivise dipendono dalla variabile di ambiente LD_LIBRARY_PATH.

Per risolvere il problema basta aggiungere, nella home directory  al file .bashrc la seguente istruzione:

export LD_LIBRARY_PATH=/usr/local/lib

A questo punto mandando in esecuzione tale file con:

. .bashrc

(se testiamo il contenuto della variabile d’ambiente

echo $LD_LIBRARY_PATH

si ottiene come risposta

/usr/local/lib)

A questo punto mandando in esecuzione l’eseguibile esempio5_6 questo girerà tranquillamente. Questo eseguibile è grande 8140 byte leggermente più piccolo di quello ottenuto con la libreria statica (le funzioni sono solo 2 il che giustifica la minima differenza; nel caso di un’intera libreria contenente centinaia di funzioni, le differenze possono essere notevoli).

1c Confronto tra Librerie statiche e condivise

Proviamo a riportare per sommi capi le caratteristiche di entrambe

STATICHE

  • Funzioni e variabili sono risolte durante la compilazione e copiate dal compilatore nell’eseguibile.
  • Questo è in grado di girare da solo, non necessita di altro
  • Storicamente le librerie erano solo statiche
  • La congiunzione tra programma e libreria effettuata dal linker
  • Sebbene l’eseguibile risultante sia più veloce (rispetto all’altro caso), bisogna pagare il prezzo di caricare tutto il programma in memoria e questo tempo potrebbe essere considerevole nel caso di grossi progetti
  • Se la libreria è cambiata, per ottenere l’eseguibile nella nuova versione, bisogna ricompilare

CONDIVISE

  • Funzioni e variabili sono risolte a run time aggiungendo al codice solo il loro indirizzo
  • Queste funzioni sono in una particolare parte della memoria e tutti i programmi vi possono accedere senza avere delle duplicazioni
  • In questo caso la congiunzione tra programma e librerie è effettuata dal sistema operativo
  • Le dimensioni dell’eseguibile sono ridotte in quanto non comprendono le librerie
  • La velocità di esecuzione è leggermente inferiore rispetto al caso precedente (non è un unico blocco monolitico come nel caso precedente) ma non c’è la perdita di tempo nel caricare il codice in memoria perché le librerie condivise sono già in memoria
  • Se si cambia la libreria, non sarà necessario ricompilare il codice per ottenerne una versione aggiornata.

1d Piccola digressione linguistica

Causa la predominanza in questo campo del mondo americano, i termini utilizzati in italiano, sono stati un po’ snaturati.

In inglese Library coincide con la nostra biblioteca mentre Book shop è la nostra libreria.

Ciò nonostante continuiamo a chiamare Librerie quelle che invece dovremmo chiamare Biblioteche: pazienza 🙂

2 Le struct

Le struct sono collezioni di variabili raggruppate sotto lo stesso nome. Questa condizione a volte può essere molto utile e semplifica la vita dei programmatori.

Nell’esempio seguente ne vediamo un esempio

  • Prima si dichiara la struct e si associa un nome
  • Tra parentesi graffe si inseriscono le variabili (dopo la graffa chiusa ricordarsi il punto e virgola)
  • Poi si usa struct nomeStruct come un nuovo tipo (come fosse int o float) e si dichiarano delle variabili
  • Per accedere ai vari campi della struct, si usa il nome della variabile più il punto ed il campo desiderato

Volendo, tramite l’istruzione typedef, è possibile proprio definire un nuovo tipo e si potrà evitare di usare la parola chiave struct della dichiarazione. Vediamo un esempio…

  • Sono stati definiti due nuovi tipi: Punto3d e Segmento
  • Due campi di segmento sono costituiti da Punto3d
  • Abbiamo usato la radice quadrata (sqrtf) quindi abbiamo usato l’header math.h e compilato con l’opzione -lm
  • Abbiamo definito una macro SQ per calcolare il quadrato di un numero

Nella lezione dei File avevamo omesso di mettere la data anticipando che era un argomento legato alle strutture: vediamo allora come si può ottenere la data in un programma C.

Osserviamo gli aspetti salienti 

  • Come riportato nei vari campi della struttura, per ottenere l’anno corrente è necessario aggiungere 1900
  • Ai giorni che partono a 0 è stato aggiunto 1
  • L’ora legale è da intendere come un booleano per cui 1 è vero 0 no.

3 Riassunto

  • Sono state riportate le varie fasi della compilazione e generazione di un eseguibile
  • Sono state introdotte le librerie statiche e condivise
  • Abbiamo visto il vantaggio di utilizzare librerie, ossia utilizzare funzioni consolidate senza introdurle ogni volta nel codice
  • Sono state mostrate le procedure e le configurazioni necessarie per costruire sia le librerie statiche sia le condivise
  • Sono stati indicati, a livello di filesystem, dove vanno collocate queste librerie
  • Si è visto come compilare con le librerie esempi presi da lezioni precedenti
  • Sono state introdotte le strutture e mostrate come usarle
  • Infine abbiamo visto la struct tm e come usarla per ottenere la data corrente ed altre informazioni

Qui finisce anche questa ottava lezione.

Se ne avete voglia, riprendete l’esercizio proposto nella lezione precedente ed al  file dei risultati aggiungete la data.

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 !!

Ricordate che la prossima lezione sarà pubblicata il prossimo 8 giugno

 

 

A proposito di arkkimede

Vedi Anche

MagPi139 doppiapagina

MagPi in Italiano! Soluzione dei Problemi: La Guida

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

Powered by themekiller.com