Guidac
Guidac
C
Guida pratica alla programmazione
Autore: BlackLight < blacklight@[Link] > rilasciato sotto licenza GNU GPL 3, copyleft 2005-2008
Indice
Cenni di programmazione..........................................................................................................................8 ..........................................................................................................................................................8 Il programma ........................................................................................................................................8 Breve storia della programmazione ......................................................................................................8 I linguaggi a basso livello.................................................................................................................8 I linguaggi a medio/alto livello.........................................................................................................8 Il C....................................................................................................................................................9 L'evoluzione ad oggetti del C - il C++.............................................................................................9 La programmazione oggi................................................................................................................10 Cosa serve per programmare in C............................................................................................................11 ........................................................................................................................................................11 Struttura di un programma in C e cenni su linguaggi compilati e interpretati.........................................12 ........................................................................................................................................................12 Linguaggi compilati e interpretati ......................................................................................................12 Note.....................................................................................................................................................13 Il primo programma.................................................................................................................................14 ........................................................................................................................................................14 Uso delle variabili....................................................................................................................................16 ........................................................................................................................................................16 Tipi di variabili ...................................................................................................................................16 Operazioni elementari sulle variabili..................................................................................................17 Stampa dei valori delle variabili..........................................................................................................18 Variabili locali e globali......................................................................................................................19 Variabili static e auto...........................................................................................................................20 Costanti: l'istruzione #define e la keyword const................................................................................20 Variabili register e volatile..................................................................................................................21 Funzioni e procedure................................................................................................................................22 ........................................................................................................................................................22 Definizione intuitiva di funzione.........................................................................................................22 Esempi d'uso di funzioni e standard di utilizzo...................................................................................22 Procedure.............................................................................................................................................24 Funzioni statiche..................................................................................................................................25 Funzioni Globali\Locali .....................................................................................................................26 Input da tastiera........................................................................................................................................27 ........................................................................................................................................................27 Controllare il flusso di un programma.....................................................................................................29 ........................................................................................................................................................29 Cicli if-else..........................................................................................................................................29 Operatori di confronto.........................................................................................................................30 Operatori logici....................................................................................................................................31 Strutture switch-case...........................................................................................................................33 Cicli iterativi - Istruzione for...............................................................................................................35 Cicli iterativi - Istruzione while...........................................................................................................37 Cicli iterativi - Istruzione do-while.....................................................................................................38 Istruzione goto.....................................................................................................................................38 Istruzione break e continue..................................................................................................................39
Gli array...................................................................................................................................................40 ........................................................................................................................................................40 Array monodimensionali.....................................................................................................................40 Matrici e array pluridimensionali........................................................................................................42 I puntatori.................................................................................................................................................43 ........................................................................................................................................................43 Strutture dinamiche.............................................................................................................................43 Liste monolanciate..............................................................................................................................43 Liste circolari.......................................................................................................................................44 Alberi e Grafi.......................................................................................................................................44 Puntatori in C.......................................................................................................................................45 Passaggio di puntatori alle funzioni....................................................................................................46 Puntatori e array..................................................................................................................................47 Passaggio di array a funzioni...............................................................................................................48 Allocazione dinamica della memoria..................................................................................................48 Puntatori a funzioni.............................................................................................................................49 Funzioni di callback........................................................................................................................50 Stringhe....................................................................................................................................................51 ........................................................................................................................................................51 Dichiarazione di una stringa................................................................................................................51 Operare sulle stringhe - La libreria string.h........................................................................................53 strcmp.............................................................................................................................................53 strncmp...........................................................................................................................................54 strcpy..............................................................................................................................................54 strncpy............................................................................................................................................55 strcat................................................................................................................................................55 strncat..............................................................................................................................................56 strstr................................................................................................................................................56 Altre funzioni sulle stringhe................................................................................................................57 sprintf..............................................................................................................................................57 snprintf............................................................................................................................................57 sscanf..............................................................................................................................................57 gets..................................................................................................................................................58 atoi..................................................................................................................................................59 Argomenti passati al main...................................................................................................................59 Uso delle stringhe e sicurezza del programma....................................................................................60 Algoritmi di ordinamento.........................................................................................................................63 ........................................................................................................................................................63 Naive sort............................................................................................................................................63 Bubble sort..........................................................................................................................................64 Insert sort.............................................................................................................................................66 Quick sort............................................................................................................................................66 Tipi di dato derivati, enumerazioni e strutture.........................................................................................69 ........................................................................................................................................................69 Definire propri tipi - L'operatore typedef............................................................................................69 Enumerazioni.......................................................................................................................................70 Dati strutturati......................................................................................................................................70 Direttive per il preprocessore...................................................................................................................74 ........................................................................................................................................................74
La direttiva #include ...............................................................................................................................74 La direttiva #define .................................................................................................................................74 Controllo del flusso .................................................................................................................................75 Macro con parametri ...............................................................................................................................77 Macro predefinite ....................................................................................................................................77 Operatori # e ## .......................................................................................................................................77 Direttive #error e #warning .....................................................................................................................78 Funzione ricorsive....................................................................................................................................79 ........................................................................................................................................................79 Esempio informale di ricorsione.........................................................................................................79 Esempio pratico di ricorsione..............................................................................................................79 Ricorsione tail e non-tail.....................................................................................................................80 Liste..........................................................................................................................................................82 ........................................................................................................................................................82 Liste come tipi di dato astratto............................................................................................................82 Rappresentazione statica.....................................................................................................................83 Rappresentazione dinamica.................................................................................................................85 Gestione dei file ad alto livello................................................................................................................88 ........................................................................................................................................................88 Apertura dei file in C...........................................................................................................................88 Scrittura su file testuali - fprintf e fputs..............................................................................................89 Lettura di file testuali - fscanf e fgets..................................................................................................91 Scrittura di dati in formato binario - fwrite.........................................................................................94 Lettura di dati in formato binario - fread.............................................................................................95 Posizionamento all'intero di un file - fseek e ftell...............................................................................96 Prendere parametri da riga di comando...................................................................................................98 ........................................................................................................................................................98 Libreria math.h.........................................................................................................................................99 ........................................................................................................................................................99 Funzioni trigonometriche....................................................................................................................99 Funzioni iperboliche............................................................................................................................99 Funzioni esponenziali e logaritmiche..................................................................................................99 Potenze e radici...................................................................................................................................99 Arrotondamento e valore assoluto.......................................................................................................99 Costanti................................................................................................................................................99 Generazione di numeri pseudocasuali...............................................................................................100 Libreria time.h........................................................................................................................................101 ......................................................................................................................................................101 time_t ................................................................................................................................................101 struct tm ............................................................................................................................................101 Esempio ............................................................................................................................................102 Gestione dei file - primitive a basso livello...........................................................................................104 ......................................................................................................................................................104 File pointer e file descriptor..............................................................................................................104 open...................................................................................................................................................104 Modalit di apertura......................................................................................................................105 Permessi........................................................................................................................................105 close...................................................................................................................................................106 read e write........................................................................................................................................106
Esempio pratico............................................................................................................................107 lseek...................................................................................................................................................107 Redirezione........................................................................................................................................108 Gestione del filesystem a basso livello..............................................................................................109 Gestione delle directory.....................................................................................................................109 Socket e connessioni di rete in C...........................................................................................................112 ......................................................................................................................................................112 Protocolli TCP e UDP.......................................................................................................................112 Indirizzi IP e endianness....................................................................................................................112 Porte...................................................................................................................................................113 Inizializzazione dell'indirizzo............................................................................................................114 Creazione del socket e connessione...................................................................................................115 Lettura e scrittura di informazioni sul socket....................................................................................116 Lato server.........................................................................................................................................116 Esempio pratico.................................................................................................................................117 Multiprogrammazione - programmazione multiprocesso e multithread................................................122 ......................................................................................................................................................122 Introduzione ai sistemi multiprogrammati........................................................................................122 Algoritmi di scheduling.....................................................................................................................122 Programmazione multiprocesso........................................................................................................123 Comunicazione tra processi. Concetto di pipe..................................................................................126 Interruzione di un processo. Concetto di segnale..............................................................................129 Programmazione multithread............................................................................................................130 Programmazione della porta parallela in C............................................................................................133 ......................................................................................................................................................133 Disclaimer.........................................................................................................................................133 Struttura della porta...........................................................................................................................135 Individuazione dell'indirizzo della porta parallela............................................................................135 Primitive di sistema per la programmazione del dispositivo............................................................135 ioperm...........................................................................................................................................135 inb o outb......................................................................................................................................136 Esempio pratico............................................................................................................................136 Interfacciamento tra C e MySQL...........................................................................................................138 ......................................................................................................................................................138 Applicazione pratica..........................................................................................................................138 CGI in C.................................................................................................................................................143 ......................................................................................................................................................143 Pagine statiche e pagine dinamiche...................................................................................................143 Richieste GET e POST......................................................................................................................145 GET..............................................................................................................................................145 POST............................................................................................................................................148 Link esterni........................................................................................................................................149 Catturare pacchetti con le librerie PCAP...............................................................................................150 ......................................................................................................................................................150 Compilare e linkare programmi con le librerie PCAP......................................................................150 Trovare un'interfaccia di rete.............................................................................................................150 Sniffing..............................................................................................................................................152 Packet injection.................................................................................................................................155 Introduzione ai sistemi fuzzy e alle reti neurali.....................................................................................156
......................................................................................................................................................156 Prerequisiti matematici .....................................................................................................................156 Sistemi fuzzy ....................................................................................................................................156 Introduzione alle reti neurali ............................................................................................................156 Struttura di una rete neurale .............................................................................................................157 Tecniche di apprendimento ...............................................................................................................160 Sviluppo di una rete neurale .............................................................................................................160 Riferimenti bibliografici ...................................................................................................................168 Raw socket.............................................................................................................................................169 ......................................................................................................................................................169
Cenni di programmazione
.
Il programma
Si definisce "programma" qualsiasi sequenza di istruzioni scritte in linguaggio macchina (l'unico linguaggio comprensibile ad un calcolatore, le famose sequenze di 0 e 1) atta ad essere elaborata da un calcolatore o comunque da una struttura informatica. Ogni volta che accendiamo il PC facciamo uso di programmi. Word e Outlook sono programmi. Winamp un programma. Windows stesso non altro che un programma (un "programma di base" o "software di base"). Anche i virus sono dei programmi eseguibili. Si pone qui il problema di come scrivere un programma. Per questa esigenza si fa ricorso ai linguaggi di programmazione.
progettazione di codice di sistema, il BASIC, nonostante la sua incredibile facilit di apprendimento, non potente, e non ha una struttura vera e propria. Inoltre, questi tre linguaggi si basano tutti sull'istruzione GOTO ("vai a"), ripresa dall'istruzione JMP dell'Assembly, quindi i programmi scritti con questi linguaggi tendono al cosiddetto "codice spaghetti", un codice spezzettato, pieno di diramazioni e salti condizionati che rendono spesso il programma illeggibile, un vero dramma quando si tratta di fare manutenzione. Nasce quindi il PASCAL, un linguaggio ad alto livello dotato di una struttura e di istruzioni vere e proprie per il controllo del flusso del programma, ma non progettato per un vasto campo di azione, quindi poco efficiente per la scrittura di codice di sistema. Al giorno d'oggi il PASCAL usato solo per scopi didattici, grazie alla sua semplicit di apprendimento e alla sua sintassi "pulita".
Il C
Arriviamo all'inizio degli anni 70, l'hardware diventa sempre pi potente e la richiesta di software cresce giorno dopo giorno, ma non esiste ancora un linguaggio ad alto livello che soddisfi qualsiasi richiesta di software. Fino al 1972, "l'ora zero" del linguaggio C: in un laboratorio della AT&T Bell Dennis Ritchie fa girare un primo prototipo del C su un DEC PDP-11 con sistema operativo UNIX. Il C fu il risultato dello sviluppo di due linguaggi di programmazione pi vecchi: il B (sviluppato da Ken Thompson) e il BCPL (sviluppato da Martin Richards). Per anni il linguaggio C fu strettamente legato al sistema operativo UNIX (infatti, dopo la progettazione del C, tutte le successive versioni di UNIX furono scritte in questo linguaggio, e ancora oggi i kernel UNIX sono scritti in C). Nel 1989, alla luce dei vari "stili" del C formatisi, l'ANSI (American National Standards Institute) mise a punto l'ANSI-C, una versione standard del C priva di ambiguit, che in uso tuttoggi. La novit del C, ed anche il motivo di tutto il suo successo, che il C un linguaggio di programmazione sviluppato dai programmatori stessi, e non da un'istituzione governativa o da un'universit, per i programmatori stessi: questo rende il C il linguaggio dei programmatori. Unita a questa caratteristica, c' la versalit del C: un linguaggio usato tanto per semplici programmini didattici, tanto per programmare sistemi operativi: un linguaggio che si presta ad un'infinit di usi, grazie anche ad una libreria davvero vastissima. Il C infatti, a differenza degli altri linguaggi di programmazione, ha davvero pochissime keyword (parole riservate), ma una vastissima gamma di funzioni che spaziano dalle funzioni per l'I/O standard alle funzioni matematiche, dalla manipolazione dei file alla gestione della memoria, dagli strumenti per la creazione di interfacce grafiche (GUI) alla manipolazione delle regex: queste funzioni sono ormai parte integrante del linguaggio. Inoltre, chiunque pu aggiungere nuove funzioni a quelle che corredano il C, e questo un altro elemento che rende il C tanto flessibile e potente. E' inoltre uno dei pochi linguaggi ad alto livello che mette a disposizione delle funzioni per la gestione della memoria e dei processi, o anche per la gestione di porte e periferiche di I/O (operazioni notoriamente di basso livello): se voglio creare un programma che gestisca queste componenti, devo fare assolutamente ricorso ad un linguaggio di basso livello come l'Assembly.
programmatore di gestire le singoli componenti del programma come oggetti, ossia come componenti intrinseche del linguaggio stesso; gli oggetti sono disposti in classi, che sono la loro rappresentazione astratta, ed ogni classe pu ereditare oggetti da altre classi o cambiare la loro visibilit all'interno del programma (esistono oggetti privati, protetti e pubblici). La potenza della OOP permette al programmatore di fare cose davvero interessanti, come ridefinire gli operatori, fare l'overloading di funzioni, creare tipi di dati "su misura" (con i template), gestire le eccezioni in modo potente (con i blocchi try e catch). Ovviamente, occorre procedere per passi: non si possono apprezzare appieno le novit introdotte dal C++ se non si ha familiarit col C.
La programmazione oggi
Nel 1991 fu concepito il Java che, pur essendo un linguaggio a s stante, considerato da molti come un'evoluzione del C++. Infatti, la sintassi di questo linguaggio deve molto al C/C++, i costrutti alla base sono molto simili. Ciononostante, il Java si pone come obiettivi quello di essere un linguaggio di programmazione orientato al Web (ed diventato con gli anni il linguaggio di programmazione del Web, grazie ai suoi meccanismi estremamente versatili di applet e servlet) e quello di essere un linguaggio multipiattaforma (write once, run everywhere il motto del Java), e queste caratteristiche hanno decretato il suo successo negli ultimi anni. Lo stesso programma che scrivo in Java lo posso eseguire teoricamente senza problemi su Windows, su Linux, un un Mac e persino su un telefonino (i famosi "giochi Java"), a patto che esista una JVM (Java Virtual Machine) per quel sistema. Passando all'ultimo decennio, abbiamo la nascita dei linguaggi di quarta generazione, linguaggi di script (non di programmazione) estremamente intuitivi e facili da usare: il Perl, il Python (entrambi sviluppati grazie al C). Microsoft ha messo a punto un suo ambiente di sviluppo basato sul C++ (Visual C++) ed il C#, un linguaggio ad oggetti che deve molto sia alla sintassi del C/C++, sia a Java.
o, in alternativa,
Come editor di testo vanno bene anche l'EDIT del DOS o il Notepad su Windows, oppure, nel caso si desideri un editor pi avanzato, si pu ricorrere a EditPlus o simili. Su Linux o un sistema UNIX-like, le scelte sono molte: dagli editor storici, EMACS e VI, ad editor pi user-friendly (KWrite, KEdit, Kate, Gedit...). Di compilatori possibile trovarne molti in rete, anche freeware (il compito del compilatore quello di tradurre il vostro programma scritto in C in linguaggio macchina, creando quindi un file eseguibile). Sui sistemi Unix lo standard il GCC, il compilatore C della GNU che trovate pre-installato in molte installazioni standard. Su Windows potete scaricare un porting gratuito di GCC per sistemi MS come DJGPP, oppure Dev-C++ (sempre GCC-based), o BCC (della Borland) oppure Visual C++. In alternativa, potete far ricorso ad un ambiente di programmazione integrato (ossia un programma che ha gi incorporato editor e compilatore); su Windows c' Visual C++, oppure potete scaricare (gratuitamente) LCC o Rhide, un IDE basato su GCC, o lo stesso Dev-C++. Su Linux c' l'ottimo KDevelop (per ambienti KDE), o Anjuta (per ambienti Gnome o Gtk-oriented).
[1] Ecco il compilatore cosa fa: Per prima cosa esegue le direttive al preprocessore (quelle che iniziano con #, come #include #define #if #endif #ifdef... alcune le vedremo nel corso di questo tutorial). Se non ci sono errori nei sorgenti, traduce il codice C contenuto nei files sorgenti in linguaggio macchina (in quanto questo l'unico linguaggio davvero comprensibile al compilatore. In genere questo processo genera un file oggetto, con estensione .o o .obj, dove viente piazzato il codice in LM), quindi esegue l'operazione di linking, ossia crea il file eseguibile vero e proprio. Quasi tutti i linguaggi ad alto livello (Pascal, FORTRAN, COBOL...) sono linguaggi compilati. Il BASIC, il Perl e il Python sono invece linguaggi interpretati: ci vuol dire che non possibile creare un file eseguibile vero e propio con questi linguaggi, ma, ogni volta che voglio eseguire un tale algoritmo, devo ricorre ad un interprete, ossia un programma che traduce istantaneamente il codice ad alto livello in linguaggio macchina. La via di mezzo il Java: una volta scritto un programma in Java, ho bisogno di compilarlo (ad esempio, con il comando javac): da questo processo ho un file con estensione .class, scritto in un linguaggio simile al linguaggio macchina, ma che non linguaggio macchina. A questo punto posso eseguire il mio programma con l'interprete Java, che esegue il codice contenuto nel file class. E' uno dei punti di forza del Java, che lo ha reso portabile verso ogni piattaforma. Ovviamente, i linguaggi compilati e interpretati hanno i loro pregi e difetti. Con un linguaggio compilato posso creare un file eseguibile vero e proprio, totalmente indipendente dal linguaggio, ma la procedura di precompilazione-compilazione-linkaggio spesso molto lenta (soprattutto quando si tratta
di compilare programmi dotati di GUI, di interfaccia grafica). Inoltre, il file eseguibile che ho ottenuto dalla compilazione ottimizzato per la macchina dove l'ho compilato, non per un'altra. In poche parole, se compilo un file C su Linux, lo stesso file eseguibile non funzioner su Windows. Un linguaggio interpretato, invece, permette di vedere in real-time se il programma che si sta scrivendo contiene o no errori, senza a avviare la procedura di compilazione. Inoltre, un listato scritto, ad esempio, in Perl su un sistema Linux funzioner anche se lo porto su un sistema Windows, a patto che vi sia installato l'interprete Perl. Per questi linguaggi hanno lo svantaggio di non creare un file eseguibile, ossia di non creare un vero e proprio programma da eseguire facendo un doppio click sopra.
Note
1. Aggiungendo -OX (con X variabile da 1 a 3) si otterranno diversi livelli di ottimizzazione del codice; aggiungendo -s verranno eliminate alcune parti non necessarie e l'eseguibile occuper meno spazio; aggiungendo -Wall verranno elencati alcuni punti del programma che "potrebbero" detrminare errori logici in run-time.
Il primo programma
.
Il primo programmino in C sar un programma abbastanza semplice, che stampa sullo schermo della console "Hello world!" ed esce. Vediamo il codice:
/* hello.c */ #include <stdio.h> int main(void) { printf ("Hello world!\n"); return 0; }
Una volta scritto questo codice con il nostro editor preferito, salviamolo come hello.c e compiliamolo con il nostro compilatore. Se usiamo GCC:
gcc -o hello hello.c
Quando lo eseguiamo (ovviamente in modalit console) apparir la scritta "Hello world!". Ma vediamo cosa fa nel dettaglio... Innanzitutto, la prima riga un commento. I commenti in C iniziano con /* e finiscono con */, ma la maggior parte dei compilatori riconoscono anche i commenti in stile C++ (che iniziano con // e finiscono con la fine della riga). Esempio:
codice codice codice codice /* Questo un commento in stile C */ /* Anche questo un commento in stile C */ // Questo un commento in stile C++
All'interno di un commento possibile scrivere informazioni sul programma, o commenti su un passaggio di codice eventualmente poco chiaro. La prima vera e propria linea di codice #include <stdio.h>: come ho accennato nel paragrafo precedente, questa una direttiva al preprocessore, ovvero un'istruzione che dice al calcolatore che nel programma che segue si useranno le funzioni definite nel file stdio.h (i file header dovreste trovarli nella cartella include del vostro compilatore). Il file stdio.h contiene le funzioni principali per lo STanDard Input/Output, ossia le funzioni che permettono, ad esempio, di scrivere messaggi in modalit testo, di leggere valori dalla tastiera, di manipolare files e buffer... se non includessimo questa istruzione non potremmo usare la funzione printf() pi avanti. Il file stdio.h il file che useremo maggiormente nel corso di questo tutorial. A questo punto inizia il programma vero e proprio: viene eseguito tutto ci che si trova all'interno della funzione main() (la funzione principale di ogni programma), che inizia con { e finisce con }. Nel paragrafo dedicato alle funzioni vedremo meglio come funziona il main (scusate per il gioco di parole). Intanto anticipo che quell'int situato prima del main() dice al chiamante (in questo caso il sistema
operativo stesso) che la funzione main() ritorna un numero intero (e questo spiega la riga return 0). A questo punto chiamiamo la funzione printf(), definita nel file stdio.h. Questa funzione stampa un messaggio sullo standard output (la console sul monitor). Ovviamente, il messaggio racchiuso fra parentesi tonde e i doppi apici "". La sequenza \n una escape sequence, che dice al compilatore di andare a capo dopo aver scritto ci che contenuto nella printf() (\n sta per "new-line"). Ecco le principali sequenze di escape usate nel C:
\n Va a capo (new line) \t Va avanti di una tabulazione (tasto TAB) \b Va indietro di un carattere (tasto BACKSPACE) \a Fa emettere un BEEP allo speaker interno (ALARM) \" Stampa i doppi apici "" \' Stampa un apice singolo
Piccola nota: tutte le istruzioni del C finiscono con un punto e virgola ; (in molti linguaggi di programmazione ad alto livello cos, Java, Perl, Pascal...). L'istruzione return 0, come ho gi detto prima, dice al programma di ritornare il valore 0 (intero) al sistema operativo e uscire. Vedremo meglio il suo funzionamento pi avanti.
Tipi di variabili
La dichiarazione di una variabile in C (ricordando che in ANSI-C indispensabile dichiarare una variabile prima di poterla utilizzare) qualcosa del tipo
tipo nome_variabile;
Il tipo di variabile caratterizza la variabile stessa. Ecco i principali tipi ammessi dal C: Dimensione (in bit) 8 16 16 32 32 32 32 64
Tipo char short int unsigned short int int unsigned int long int float double Esempio:
int a; int b = 3; char c = 'q'; float d = 3.5; a = 2; int e = a+b; // // // // // //
Descrizione Caratteri di testo ASCII Numeri interi piccoli (da -32768 a 32768) Numeri positivi interi piccoli (da 0 a 65536) Numeri interi (da -2147483648 a 2147483648) Numeri interi positivi (da 0 a 4294967296) Numeri interi grandi Numeri a virgola mobile (precisione singola) Numeri a virgola mobile (doppia precisione, notazione scientifica)
Dichiaro una variabile Dichiaro una variabile Dichiaro una variabile Dichiaro una variabile Adesso a vale 2 e vale la somma di a e
intera chiamata a senza inizializzarla intera b che vale 3 char che contiene il carattere q float d che vale 3.5 b, ossia 5
Quello che ho riportato sopra un errore molto frequente anche fra i programmatori pi esperti, se provo a compilare un codice del genere otterr un warning (un "avvertimento del compilatore, che per non pregiudica la compilazione vera e propria) o, pi spesso, un errore vero e proprio (la compilazione viene arrestata). Non posso effettuare operazioni fra due tipi di variabili diversi fra loro (int questo caso, int e float): per farlo devo ricorrere alla conversione di cast, ovvero specificare esplicitamente al compilatore in che tipo di variabile voglio salvare il risultato. Esempio corretto:
int a = 2; float b = 3.5; int c = (int) a+b; // Converto il risultato in int. c vale 5 in questo caso
Oppure:
int a = 2; float b = 3.5; float c = (float) a+b; caso // Converto il risultato in float. c vale 5.5 in questo
La scrittura a += 2 sta per a = a+2 (sono concesse scritture come += -= *= /= %=). La scrittura a++ invece un incremento della variabile a, ed equivale a a=a+1 (cos come la scrittura a-- equivale a a=a-1). Meglio soffermarci un attimo su quest'aspetto. In C sono concesse sia scritture come a++ sia come ++a, ed entrambe incrementano la variabile a di un'unit. Qual la differenza tra le due scritture? Una scrittura del tipo a++ viene chiamata post-incremento. Ci vuol dire che, sulla maggior parte dei compilatori, viene prima eseguita l'istruzione in cui si trova quest'operazione, quindi, alla fine dell'istruzione, la variabile viene incrementata. Una scrittura del tipo ++a viene invece chiamata preincremento. Quando il compilatore incontra un'operazione di pre-incremento in genere incrementa prima il valore della variabile, quindi esegue l'istruzione all'interno della quale collocata. Se devo semplicemente incrementare il valore di a, indifferente usare l'una o l'altra scrittura. Ma si osservi questo esempio di codice...
int a=3; int b=4; int c;
un conto scrivere
c = (a++)+b; // c vale 3+4=7 // a viene incrementata dopo l'istruzione // ora a vale 4
L'output sar:
x vale 3
La stringa di formato %d dice al compilatore di stampare la variabile intera posta fuori i doppi apici "". In questo caso stampa il valore di x, che proprio 3. Se invece si desidera stampare una variabile di tipo float:
float x = 3.14; printf ("x vale %f",x);
dove la scrittura %f dice al compilatore di stampare una variabile di tipo float. Ecco i formati di stringa principali usati per stampare i principali tipi di variabili: Esempio:
/* variabili.c */ #include < stdio.h > int main() { int a,b,c; a = 3; b = 4; c = a+b; // Dichiaro 3 variabili int
// c vale 7
printf ("c vale %d\n",c); a += 3; b++; c = a-b; // Ora a vale 6 // Ora b vale 5 // Ora c vale -1
Il fatto interessante che possiamo eseguire opeazioni anche sulle variabili char. Le variabili char, infatti, vengono considerate dal programma come codici ASCII, ovvero ogni variabile ha il suo codice numerico (da 0 a 255) che viene convertito in runtime in un carattere (attenzione che in linguaggi come Java queste operazioni non sono concesse, in quanto i caratteri non sono in formato ASCII ma Unicode). Ecco un esempio:
char c = 65; // Equivale a scrivere char c = 'A', infatti // 65 il codice per la lettera A char c += 4; // Ora a vale E printf ("a = %c\n",c);
In C++ non si ha questo errore, in quanto la dichiarazione di una variabile considerata un'istruzione vera e propria e pu essere messa ovunque, ma in C c' l'errore. Le variabili dichiarate all'inizio del programma (prima del main e di ogni funzione) vengono dette globali e possono essere usate da ogni funzione del programma (lo vedremo meglio quando parleremo delle funzioni), mentre le variabili locali possono essere viste solo dalla funzione che le dichiara (in C+ + anche possibile far vedere le variabili ad un solo blocco di codice). Esempio:
#include <stdio.h> int var_globale = 3; // Variabile globale
int main() { int var_locale = 2; // Variabile locale ....... var_globale += var_locale; // possibile perch var_globale una variabile globale ....... }
Nel paragrafo sulle funzioni capiremo meglio il meccanismo di visibilit delle variabili globali. In genere, per questioni di modularit del codice e visibilit, consigliabile usare le variabili globali solo quando strettamente indispensabile. Questo perch, proprio in virt delle sue propriet, una variabile globale modificabile da ogni funzione, e questo potrebbe portare a malfunzionamenti nel programma, nel caso in cui una funzione (che possiamo vedere come un 'pezzo' del programma) si trovi a lavorare su una variabile gi modificata da un'altra funzione.
Una variabile statica non pu essere esportata in sorgenti esterni a quello in cui dichiarata. Esempio, ho una variabile definita in un file header, e questa variabile viene importata in un file sorgente in cui incluso l'header. Non posso usare la variabile all'infuori del file "legittimo proprietario" se dichiarata come statica.
L'istruzione #define , come la #include, un'istruzione al preprocessore. In poche parole, quando il compilatore incappa in una #define, legge il valore assegnato alla costante (anche se non propriamente una costante, in quanto non viene allocata in memoria), cerca quella costante all'interno del programma e gli sostituisce il valore specificato in real-time. Ad ogni occorrenza di PI, quindi, il preprocessore sostituisce automaticamente 3.14, senza andare a cercare il corrispondente valore della variabile in memoria centrale.
Con la const, invece, creo una vera e propria variabile a sola lettura in modo pulito e veloce, e per dichiarre una costante di gran lunga preferito quest'ultimo metodo. Ovviamente una scrittura come questa dar un errore (o un warning):
const float pi = 3.14; pi += 1;
Ovviamente, in genere sconsigliato allocare variabili nei registri, in quanto per i registri ci passa una marea di informazioni importanti per la sessione, per il programma ecc. e la variabile pu facilmente andar persa. Dichiarando invece una variabile come volatile, questa variabile pu venir modificata da alti processi o da altre parti del programma in qualsiasi momento:
volatile int vol_var;
Stringa di formato %c %d, %i %x %X %o %l, %ld %u %f %lf %p %s %n Variabili char Valori in formato decimale Valori in formato esadecimale Valori in formato ottale Variabili long int Variabili unsigned Variabili float Variabili double Indirizzo esadecimale di una variabile
Uso
Stringhe di testo (le vedremo pi avanti...) Scrive i byte scritti finora sullo stack dalla funzione printf() (usata principalmente per format string overflow)
Funzioni e procedure
.
Ogni linguaggio di programmazione ad alto livello mette a disposizione del programmatore gli strumenti delle funzioni e delle procedure, tanto pi il C, linguaggio procedurale per eccellenza. Abbiamo gi incontrato nel corso di tutorial un esempio di funzione: il main(). Il main() altro non che una funzione speciale che viene eseguita all'inizio del programma. Ma ovviamente possibile definire anche altre funzioni (avevo gi accennato che tutto ci che si fa in C si fa tramite le funzioni. Anche la printf() che abbiamo usato nei paragrafi precedenti non altro che una funzione definita in stdio.h).
Questa funzione calcola il quadrato di un numero intero x. La variabile int x il parametro che passo alla funzione. Ho stabilito all'inizio, dichiarando la funzione come int, che il valore ritornato dalla funzione (la "variabile dipendente") deve essere di tipo int. Attraverso la direttiva return stabilisco quale valore deve ritornare la funzione (in questo caso il quadrato del numero x, ossia x*x). In matematica, una funzione del genere la potrei scrivere come f(x)=x.
Questa funzione la posso richiamare all'interno del main() o di qualsiasi altra funzione del programma. Esempio:
int y; y = square(2); // Dichiaro una variabile int // Passo alla funzione square il valore 2, // in modo che calcoli il quadrato di 2 printf ("Quadrato di 2: %d\n",y);
Ovviamente, posso dichiarare un'infinit di funzioni in questo modo. Ecco ad esempio una funzione che calcola la somma di due numeri:
int somma(int a, int b) return a+b; } {
Invocazione:
int c; c = somma(2,3); // c vale 5
La maggior parte delle funzioni matematiche sono dichiarate nel file math.h (ci sono ad esempio funzioni per calcolare il seno, il coseno o la tangente di un numero reale, il logaritmo, la radice quadrata, la potenza n-esima...), quindi se vi interessa fare un programma di impostazione matematica date un'occhiata a questo file per capire quale funzione usare. Ovviamente, anche possibile creare funzioni senza alcun parametro in input. Esempio (stupido):
int ritorna_zero() return 0; } {
Vediamo ora come inserire una funzione nel nostro programma. Le funzioni in C possono andare in qualsiasi parte del codice, ma l'ANSI-C, per evitare confusione, ha imposto che all'inizio del programma ci vadano i prototipi delle funzioni usate dal programma stesso. Un prototipo non altro che la funzione vera e propria (tipo ritornato, nome e parametri) senza per la sua implementazione, ossia senza il codice fra le parentesi graffe {}. Esempio:
int square(int x);
Ecco qui un programmino d'esempio che calcola il quadrato di un numero intero stabilito attraverso la funzione square():
/* square.c */ #include <stdio.h> int square(int x); // Prototipo della funzione
int main() { int y; // Variabile intera y = square(3); // Ora y vale 9 printf ("Quadrato di 3: %d\n",y); // Pi brevemente, potremo scrivere: // printf ("Quadrato di 3: %d\n",square(3)); // senza neanche "scomodare" la variabile y return 0; }
Nei programmi di grandi dimensioni, in genere si mette il prototipo della funzione in un file header (con estensione .h), l'implementazione in un file .c e poi il programma vero e proprio nel file main.c. Esempio:
/* Questo il file square.h */ int square(int x); /* Questo il file square.c */ int square(int x) { return x*x; } /* Questo il file main.c */ #include <stdio.h> #include "square.h" // Ovviamente includo il file square.h int main() { printf ("Quadrato di 4: %d\n",square(4)); return 0; }
Quando vado a compilare questo programma devo fare una cosa del genere:
gcc -o square main.c square.c
Ma per i nostri piccoli programmini non il caso di fare una cosa del genere! Ci va tutto in un file.
Procedure
Un discorso simile a quello delle funzioni vale anche per le procedure; le procedure non solo altro che funzioni "speciali", funzioni che non hanno un valore ritornato: eseguono un pezzo di codice ed escono. Per concludere una procedura non necessario il return (in quanto non ritorna alcun valore): al massimo ci possiamo mettere un return;. Per dichiarare una procedura user la keyword void:
void hello() { printf ("Hello world!\n"); return; } // Questa riga opzionale
Quando voglio chiamare questa procedura all'interno di una qualsiasi funzione, baster fare cos:
hello();
Esempio:
#include <stdio.h> void hello(); // Prototipo della procedura
Invocazione:
stampa_var(3); // L'output : "Valore della variabile passata: 3
il compilatore non sa che funzione chiamare e va nel pallone. Proprio per evitare ambiguit del genere, la maggior parte dei compilatori danno un errore (o almeno un warning) quando nel programma compaiono scritture del genere (tuttavia, nel C++ cose del genere sono possibili, con l'overloading delle funzioni, ossia con la dichiarazione di pi funzioni con lo stesso nome MA con la lista dei parametri differente. In ogni caso, una scrittura come quella di sopra dar problemi anche in C++, in quanto entrambe le funzioni hanno un solo parametro e il compilatore, nel momento dell'invocazione, non sa quale funzione chiamare).
Funzioni statiche
Le funzioni statiche hanno propriet molto simili alle variabili statiche. Tali funzioni, al pari delle corrispettive variabili, sono
Istanziate in memoria quando il programma viene creato, e distrutte quando il processo corrispondente terminato Visibili e utilizzabili solo all'interno del file che le ha dichiarate
La seconda propriet impone delle limitazioni d'uso delle funzioni statiche, in modo da rendere pi modulare il programma, pi protetto ed evitare che qualsiasi file del programma possa richiamare qualsiasi funzione del programma. Esempio:
/* file: foo.c */
#include <stdio.h> static void foo1() { printf ("Sono una funzione statica\n"); } void foo2() { printf ("Richiamo una funzione statica\n"); foo1(); // Chiamata valida. La funzione foo1() contenuta // nello stesso file della funzione foo2() } /* file: main.c */ #include <stdio.h> #include "foo.c" main() { foo2(); } foo1(); // Chiamata valida. La funzione foo2() visibile // al main e non una funzione statica // ERRORE! foo1() statica
Il meccanismo della visibilit delle funzioni e delle variabili ancora un po' primitivo nel C, e si basa tutto sul concetto di staticit, mentre verr decisamente approfondito in linguaggi a oggetti come C++, Java e Smalltalk.
Funzioni Globali\Locali
C', infine, un'altro metodo, per creare una funzione, sconsigliato per il fatto che rende meno modulare il codice. Questo metodo consiste nel creare una funzione locale ad un'altra funzione. Ovvero una funzione visibile e richiamabile solo all'interno della funzione in cui stata dichiarata. Un esempio della sua creazione :
#include <stdio.h> int main(void) { void hello_local_function(void) { printf("Local Function is Ready!\n"); } printf("Richiamo la funzione interna...\n"); hello_local_function(); printf("Esco.\n\n"); return 0;
Input da tastiera
.
Finora abbiamo preso in esame programmi che eseguono delle istruzioni ed escono. Ma un programma non ha molto senso se non pu interagire con l'utente che lo usa. Il C mette a disposizione molte funzioni per l'I/O (Input/Output) da tastiera (quelle che useremo sono definite perlopi in in stdio.h). Abbiamo gi incontrato la printf() per l'output sul monitor, ora facciamo conoscenza con la scanf(), per la lettura di valori dalla tastiera. Ecco la forma della scanf():
scanf ("tipo_da_leggere",&variabile);
Ecco nel frammento di programma di sopra cosa succede: Attraverso la scanf() dico al programma di leggere un valore intero dalla tastiera (gi abbiamo visto che la sequenza %d dice al programma che quella che si sta per leggere o scrivere una variabile intera) e di salvare questo valore all'indirizzo della variabile a (capiremo meglio questo concetto quando parleremo dei puntatori), ossia copio questo valore nella variabile intera a. Ecco un programmino facile facile che somma fra loro due numeri reali:
/* somma.c */ #include <stdio.h> double somma(double a, double b); somma() int main() { double a,b; // Prototipo della funzione
printf ("Inserire il primo numero: "); scanf ("%lf",&a); // Leggo il primo valore double e lo salvo all'indirizzo di a printf ("Inserire il secondo numero: "); scanf ("%lf",&b); // Leggo il secondo valore double e lo salvo all'indirizzo di b printf ("Somma fra %lf e %lf = %lf\n",a,b,somma(a,b)); fra a e b return 0; } double somma(double a, double b) return a+b; { // Stampo la somma
Ecco invece un programmino che stampa l'area e la lunghezza di una circonferenza dato il raggio:
/* circ.c */ #include <stdio.h> #include <math.h> // Includo il file math.h per poter usare la costante M_PI (pi greco) double area(double raggio); double circ(double raggio); int main() { double r; // Raggio
printf ("Inserire il valore del raggio: "); scanf ("%lf",&r); // Leggo il valore del raggio printf ("Area: %lf\n",area(r)); printf ("Circonferenza: %lf\n",circ(r)); return 0; } double area(double raggio) { return M_PI*raggio*raggio; } double circ(double raggio) return 2*M_PI*raggio; } { // pi*r
// 2pi*r
Ho incluso il file math.h perch in questo file gi definita la costante M_PI (pi greco) con 20 cifre di precisione dopo la virgola.
Cicli if-else
I cicli if-else (in inglese "se-altrimenti") sono la struttura per il controllo del programma pi semplice messa a disposizione dai linguaggi di programmazione: questa struttura definisce il codice da eseguire se una data condizione si verifica e quello da eseguire se questa condizione non si verifica. La sua sintassi la seguente:
if (condizione) codice codice } else { codice codice } {
Esempio: prendiamo un frammento di codice che stabilisce se un numero intero n positivo o negativo facendo uso del costrutto if-else:
int n; ........ if (n>0) { printf ("n positivo\n"); } else { printf ("n negativo\n"); } // Se n maggiore di zero, allora positivo // Altrimenti, negativo // Dichiaro n
Se un'istruzione if o else (o qualsiasi altro costrutto che vedremo in questo paragrafo) contiene una sola istruzione (come nel caso di sopra) si possono omettere le parentesi graffe {}
int n; ........ if (n>0) printf ("n positivo\n"); else printf ("n negativo\n");
Dopo un'istruzione if non sempre necessaria un'istruzione else: ecco un modo abbastanza interessante per scrivere il frammento di codice riportato sopra:
int n; ......... if (n>0) { printf ("n positivo\n"); return 0; } printf ("n negativo\n"); soltanto se ricade nel costrutto
// Esco dalla funzione // Questa istruzione verr eseguita se e // n negativo, perch se positivo // if di sopra, che esce dalla funzione
Se qualcuno di voi ha programmato in Pascal, in BASIC, in Bash o in linguaggi simili avr notato che il costrutto if del C (e dei linguaggi da esso derivati, C++, Java, Perl) manca della keyword then ("allora") usata in questi linguaggi, in quanto ridondante e inutile (bastano le parentesi graffe per stabilire dove il costrutto inizia e dove finisce).
Operatori di confronto
Abbiamo incontrato, negli esempi sopra, il simbolo di maggiore > , usato per stabilire se un valore maggiore di un altro. Ovviamente, abbiamo anche il simbolo di minore < usato per il caso contrario. Ecco i principali operatori di confronto usati nel C: Operatore Significato > Maggiore < Minore >= Maggiore o uguale <= Minore o uguale != Diverso == Uguale (Attenzione: diverso da = ) Il simbolo == sta per "uguale" come confronto. Se ad esempio vogliamo sapere se una variabile vale 3, scriveremo:
if (a==3) // NON a=3!!!
attenzione: la scrittura di sopra fa semplicemente l'assegnamento di un valore alla variabile a. Sappiamo che il ciclo if verificato se la condizione al suo interno vera, viene ignorato quando la condizione falsa. Il C prende come convenzione vero qualsiasi valore diverso da zero, falso qualsiasi valore uguale a zero. Il codice di sopra non fa altro che assegnare un valore alla variabile a ed entrare nel ciclo se il valore di a diverso da zero (come in quest'esempio), ignorarlo in caso contrario. Il che leggermente diverso dal fare un confronto, come volevamo noi...
In definitiva, l'uguale singolo = viene usato per gli assegnamenti (ad esempio "a=2") mentre quello doppio == per i confronti (nel Pascal invece si usa = per i confronti e := per le assegnazioni).
Operatori logici
Vediamo ora i principali operatori logici usati dal C. Facciamo prima un ripasso di logica: date due o pi proposizioni logiche possibile fare 4 operazioni fondamentali fra loro: la congiunzione (AND), la disgiunzione (OR), la disgiunzione esclusiva (XOR) e la negazione (NOT). Quando parliamo di proposizioni logiche parliamo di una qualsiasi affermazione che pu essere vera o falsa. La congiunzione (AND) di due proposizioni vera se e soltanto se entrambe le proposizioni sono vere. Ad esempio, in logica posso dire "fuori piove E Marco uscito" solo se fuori piove E Marco uscito, ossia solo se entrambi gli eventi sono veri. Con la disgiunzione (OR) basta invece che solo uno dei due eventi sia vero per rendere l'operazione vera. La disgiunzione esclusiva (XOR) invece richiede che un evento sia vero e l'altro sia falso per essere vera. La negazione (NOT) , lo dice il nome stesso, la negazione di una proposizione. Se la proposizione vera, la proposizione negata falsa. Se "fuori piove" una proposizione vera, "fuori non piove" una proposizione falsa. Per maggiori delucidazione, ecco le tabelle di verit (le tabelle delle 4 operazioni logiche fondamentali), dove 0 sta per falso e 1 per vero (cos come la vede la macchina. a e b sono le due proposizioni logiche su cui voglio operare): a 1 1 0 0 a 1 1 0 0 a 1 1 0 0 a 1 0 b 1 0 1 0 b 1 0 1 0 b 1 0 1 0 a AND b 1 0 0 0 a OR b 1 1 1 0 a XOR b 0 1 1 0 NOT a 0 1
A cosa ci possono servire questi rudimenti di logica per la programmazione in C? presto detto. Sappiamo che un computer ragiona con una logica binaria; nel processore tutte le istruzioni che noi mettiamo in una programma diventano, a livello logico-elettronico, delle semplici operazioni logiche, AND, OR, XOR e NOT. In particolare, in C useremo perlopi tali operatori per descrivere meglio le condizioni all'interno di certi confronti, oppure per manipolare variabili a livello di bit nel caso di applicazioni di basso livello. Ecco come si scrivono in C le operazioni logiche: Operazione Scrittura in C AND OR XOR NOT && || ^ !
Vediamo qualche applicazione pratica: un frammento di codice che stabilisce se un numero compreso fra 0 e 10. Senza operatori logici lo scriveremo cos:
if (n>0) { if (n<10) printf ("n compreso fra 0 e 10\n"); else printf ("n maggiore di 10\n"); } else printf ("n minore di 0\n");
Ossia: se n maggiore di 0 E contemporaneamente n minore di 10, allora n compreso fra 0 e 10. Facciamo ora un esempio con l'OR: un programma che stabilisce se un numero minore di 0 OPPURE maggiore di 10 (il contrario dell'intervallo che abbiamo visto sopra):
if ((n<0) || (n>10)) printf ("n minore di 0, oppure n maggiore di 10\n");
Ossia: controlla se n minore di 0 OPPURE maggiore di 10. Ragionamento simile anche per lo XOR. Lo XOR un'operazione logica molto usata in Assembly, in quanto fare lo XOR di un registro con se stesso equivale a svuotare il registro. Il NOT viene invece usato per sostituire scritture ridondanti come n==0 o n!=0: infatti una variabile negata sempre 0:
if (n) // Equivale a scrivere if (n!=0) printf ("n diverso da 0\n"); if (!n) // Se "NOT n". Equivale a scrivere if (n==0) printf ("n uguale a 0\n");
Gli operatori logici possono anche essere usati fra variabili, consentendo quindi di effettuare operazioni logiche fra numeri a livello di bit:
int a=0xa0a1a2a3; int b = a && 0x0000ff00; // Fa un AND che azzera tutti i byte tranne il penultimo
-> b = 0x0000a200 // O anche, esempio pi immediato: char a=3; char b=5; char c = a && b; // O ancora: char a=3; char b=5; char c = a || b; // a = 00000011 // b = 00000101 // c = 00000111 = 7 // a = 00000011 // b = 00000101 // c = 00000001 = 1
C' poi l'operatore di complemento logico. Se infatti per il complemento logico usassimo !, noteremmo semplicemente che (!a) == 0 se a!=0, e (!a) != 0 se a==0. Se vogliamo invece calcolare il complemento logico a 1 di un numero ricorreremo all'operatore binario ~.
char a=7; char b=~a; // a = 00000111 // b = 11111000
Un'altra operazione logica messa a disposizione dal C lo SHIFT. Immaginiamo di avere una variabile int i = 4; scritta in binario (facciamo per comodit a 4 bit) sappiamo che equivale a 0100. Fare uno shift a sinistra di 1 bit (la scrittura in questo caso <<) equivale a spostare tutti i bit di un posto a sinistra: la nostra variabile binaria da 0100 diventa quindi 1000, quindi i da 4 diventa per magia 8! Una cosa degenere in C si scrive cos:
int i = 4; i = i << 1; // Faccio lo shift a sinistra di 1 bit
C' anche lo shift a destra, il simbolo >>. Ad esempio, se facciamo uno shift a destra di 1 bit di i, questa variabile da 0100 diventa 0010, quindi da 4 diventa 2:
int i = 4; i = i >> 1; // Faccio lo shift a destra di 1 bit
Strutture switch-case
Le strutture switch-case sono un modo pi elegante per gestire un numero piuttosto alto di costrutti ifelse. Prendiamo un programmino che riceve in input un carattere e stabilisce se il carattere 'a','b','c','d','e' oppure diverso da questi cinque. Con l'if-else scriveremmo una roba del genere:
char ch; // Carattere printf ("Inserisci un carattere: "); scanf ("%c",&ch); if (ch=='a') printf ("Hai digitato a\n"); else { if (ch=='b') printf ("Hai digitato b\n"); else { if (ch=='c') printf ("Hai digitato c\n");
} }
else { if (ch=='d') printf ("Hai digitato d\n"); else { if (ch=='e') printf ("Hai digitato e\n"); else printf ("Non hai digitato un carattere compreso fra a ed e\n"); } }
Tale scrittura non certo il massimo della leggibilit. Vediamo ora lo stesso frammento di programma con una struttura switch-case:
char ch; printf ("Inserisci un carattere: "); scanf ("%c",&ch); switch(ch) { // Ciclo switch per la variabile ch case 'a': // Nel caso ch=='a'... printf ("Hai digitato a\n"); break; // Interrompe questo case case 'b': printf ("Hai digitato b\n"); break; case 'c': printf ("Hai digitato c\n"); break; case 'd': printf ("Hai digitato d\n"); break; case 'e': printf ("Hai digitato e\n"); break; default: // Nel caso il valore di ch non sia uno di quelli sopra elencati... printf ("Non hai digitato un carattere compreso fra a ed e\n"); break; } // Fine della struttura switch-case
........... case val_n: codice break; default: // La clausola di default non obbligatoria codice break; }
Ogni etichetta case va interrotta con la clausola break, che interrompe lo switch-case e ripassa il controllo al programma.
Il che decisamente scomodo. Per evenualit di questo tipo ci viene in aiuto il ciclo for, che ha la seguente sintassi:
for (variabile_1=valore1, ..., variabile_n=valore_n; condizione; step) codice } {
Dove variabile_1,...,variabile_n sono le cosiddette variabile contatori, condizione una condizione booleana che stabilisce il numero di cicli da eseguire (ovvero, finch la condizione vera esegui il ciclo for) e step l'eventuale incremento o decremento da far subire alle variabili contatore ad ogni ciclo. Esempio chiarificatore: ecco il programmino di sopra scritto con un ciclo for:
int main() int i; { // Variabile "contatore"
Dove la variabile contatore i, e viene inizialmente posta, all'interno del ciclo for, uguale a 0. La condizione i<10, ovvero finch la variabile i minore di 10 esegui il ciclo, lo step invece i++, ovvero 'ad ogni ciclo incrementa la variabile i (finch, ovviamente, non varr 10 e il ciclo pu ritenersi concluso). Ecco un altro esempio chiarificatore:
int main() int i; {
In questo caso, i ha come valore iniziale 1 e il ciclo termina quando i esattamente uguale a 10. In questo caso l'output sar:
Valore Valore Valore Valore Valore Valore Valore Valore Valore Valore di di di di di di di di di di i: i: i: i: i: i: i: i: i: i: 1 2 3 4 5 6 7 8 9 10
Altro esempio:
for (i=10; i>0; i--) printf ("Valore di i: %d\n",i);
In questo caso, i ha come valore iniziale 10, viene decrementata di un'unit ad ogni loop e il ciclo termina quando i vale 0. L'output il seguente:
Valore di i: 10 Valore di i: 9
di di di di di di di di
i: i: i: i: i: i: i: i:
8 7 6 5 4 3 2 1
Vedremo pi avanti che i cicli for sono molto utili per manipolare gli array. Piccola nota: possibile usare i cicli for anche per eseguire un blocco di istruzioni all'infinito:
for (;;) printf ("Stampa questo all'infinito\n");
In questo caso, dato che non c' nessuna variabile contatore che limita il ciclo, le istruzioni all'interno del for verrano semplicemente eseguite teoricamente all'infinito. Questo perch, nonostante l'istruzione for preveda 3 campi (variabili contatore con valori iniziali, condizione di break e step), nessuno di questi 3 campi strettamente obbligatorio.
Sotto un punto di vista pratico, questo frammento di codice esattamente uguale a quello esaminato sopra, nel paragrafo sul for. Semplicemente, controlla se la variabile i minore di 10: se lo , allora esegue il blocco di istruzioni all'interno del while (ovviamente, ad ogni loop la variabile i viene incrementata di un'unit). Quando la condizione di partenza non pi vera, allora il ciclo termina. Esempio un po' pi complesso:
int n; while (n!=0) { printf ("Inserisci un numero (0 per finire): "); scanf ("%d",&n); } printf ("Numero inserito: %d\n",n);
In questo caso, il programma mi chieder di inserire un numero intero e stamper il numero che ho appena inserito: se il numero proprio 0, allora il ciclo termina (l'espressione while (n!=0) sta per
L'istruzione printf() contenuta all'interno del while non verr mai eseguita, in quanto la condizione di partenza falsa (il valore di n minore di 0). Se volessimo che il nostro programma esegua prima il codice e poi controlli la verit della condizione dobbiamo usare un ciclo do-while. La sua struttora la seguente:
do { codice codice ...... } while(condizione_booleana);
Esempio:
int n = -1; do // Variabile int { printf ("Questo codice verr eseguito una sola volta\n"); } while(n>0);
In questo caso il programma esegue prima l'istruzione printf(), quindi controlla la condizione specificata. Dato che in questo caso la condizione falsa, il ciclo termina.
Istruzione goto
L'istruzione goto ("vai a") l'istruzione per i cicli pi elementare, e deriva direttamente dall'istruzione JMP (JuMP) dell'Assembly. La sua sintassi la seguente:
etichetta: codice codice ...... goto etichetta;
Esempio: prendiamo il classico programmino che stampa 10 volte "Hello world!". Con ll'istruzione goto verrebbe pi o meno cos:
int main() int i=0; { // Variabile contatore
hello: // Etichetta "hello". Ma posso chiamarla in qualsiasi altro modo printf ("Hello world!\n"); i++; // Incremento la variabile contatore
Tuttavia, l'istruzione goto oggigiorno estremamente sconsigliata, in quanto tende a creare il cosiddetto "codice a spaghetti", ossia un codice spezzettato, pieno di salti e quindi difficile da leggere. In genere i cicli for, while e do-while sono molto pi leggibili di codici scritti con il goto.
In poche parole, la clausola break interrompe il ciclo che altrimenti sarebbe infinito. possibile usare queste clausole (tra l'altro abbiamo gi incontrare il break nello switch-case) in qualsiasi punto di un ciclo per interromperlo o continuarlo, al verificarsi di determinate condizioni.
Gli array
.
Gli array, o vettori, sono le strutture di dati pi elementari in informatica, del tutto simili ai vettori trattati dall'algebra lineare.
Array monodimensionali
Si tratta di un insieme di variabili dello stesso tipo e accomunate dallo stesso nome (il nome del'array). Ci che distingue un elemento dell'array da un altro l'indice, ovvero il suo numero, la sua posizione all'interno dell'array. Possiamo immaginare un array come una cassettiera: per sapere dove mettere le mani per trovare qualcosa ci serve il numero del cassetto dove cercare (prima cassetto, secondo cassetto...). Cos, un array una raccolta di variabili dello stesso tipo sotto lo stesso nome dove ogni variabile un "cassettino" identificato da un numero. Ecco come si dichiara un array in C:
tipo nome_array[quantit];
Esempio:
int mio_array[10];
dichiara un array di 10 variabili int (N.B. da 0 a 9, non da 1 a 10!) chiamato mio_array. Se voglio cambiare un valore qualsiasi di questo array, baster fare cos:
mio_array[0] = 3; mio_arrar[1] = 2; ....... // Il primo valore ora vale 3 // Il secondo valore vale 2
Posso anche leggere tutti i valori e poi stamparli tramite un ciclo for:
main() { int mio_array[10]; int i; for (i=0; i<10; i++) { printf ("Elemento n.%d: ",i); scanf("%d",&mio_array[i]); } for (i=0; i<10; i++) // Per i volte... // Elemento n.i // Leggo un valore int dalla tastiera // e lo memorizzo nell'elemento numero // i dell'array.
Vediamo ora un esempio pi utile: un programma che calcola la media aritmetica di 5 numeri:
main() { float numeri[5]; float med=0; int i; // Array di 5 float // Media aritmetica // Variabile contatore
for (i=0; i<5; i++) { printf ("Valore n.%d: ",i); scanf ("%f",&numeri[i]); med += numeri[i]; // Sommo fra loro tutti i numeri nell'array } med /= 5; } // Divido la somma dei numeri per la loro quantit (5)
Ancora un altro esempio, assimilabile all'algebra lineare vera e propria: un programmino che effettua il prodotto scalare tra due vettori (ricordo che dati due vettori v1 e v2 entrambi di n elementi il loro prodotto scalare un numero uguale a v1[0]*v2[0] + v1[1]*v2[1] + ... + v1[n-1]*v2[n-1]), dove gli elementi di entrambi i vettori sono stabiliti dall'utente via input:
#include <stdio.h> // Dimensione dei due vettori #define N 5 main() { int v1[N],v2[N]; int i; int prod=0; for (i=0; i<N; i++) { printf ("Elemento %d del primo vettore: ",i+1); scanf ("%d",&v1[i]); } for (i=0; i<N; i++) { printf ("Elemento %d del secondo vettore: ",i+1); scanf ("%d",&v2[i]); } for (i=0; i<N; i++) prod += (v1[i]*v2[i]); } printf ("Prodotto scalare dei due vettori: %d\n", prod);
La lettura e la scrittura su questi elementi vengono effettuate in modo molto simile agli array, ma con due indici, in modo da gestire sia il numero di righe che di colonne della matrice:
int matrix[2][2]; int i,j; // Leggo i valori della matrice da input for (i=0; i<2; i++) for (j=0; j<2; j++) { printf ("Elemento [%d][%d]: ",i+1,j+1); scanf ("%d",&matrix[i][j]); } // Stampo i valori della matrice for (i=0; i<2; i++) for (j=0; j<2; j++) printf ("Elemento in posizione [%d][%d]: %d\n",i+1,j+1,matrix[i][j]);
I puntatori
.
La memoria RAM del calcolatore non altro che un insieme di locazioni di memoria; per poter localizzare ciascuna locazione, ognuna di esse identificata da un indirizzo univoco. Questo significa che:
Per scrivere qualcosa in memoria centrale dobbiamo conoscere l'indirizzo del punto esatto in cui scrivere; Se conosciamo l'indirizzo di una data locazione possiamo leggere ci che contenuto al suo interno.
Strutture dinamiche
a differenza delle strutture dati di tipo statico che allocano i dati in "contenitori" prefissati (array, record etc etc), con i puntatori possibile realizzare delle strutture dati DINAMICHE ottimizzando l'utilizzo della memoria e allo stesso tempo rendendo possibile l'implementazione di algoritmi che ottimizzano anche il tempo di esecuzione dell'elaborazioe dei dati. I puntatori sono il trampolino di lancio per la programmazione ad oggetti, infatti l'istanza di una classe non altro che il puntatore ad un'area di memoria disegnata in maniera tale da corrispondere alla struttura della classe.
Liste monolanciate
[^start]-->[(DATA)(^next)]-->[(DATA)(^next)]-->[(DATA)(NULL)] una lista una struttura dati dinamica che a differenza dell'array non ha uan lunghezza prestabilita, i suoi elementi formano una specie di "trenino" ovvero ognuno ha il collegamento alll'elemento successivo e quindi il puntatore all'elemento successivo. se l'elemento successivo non esiste (fine della lista) il collegamento conterr il valore NULL ovvero non punter a nulla. questo tipo di lista utilizzato per gestire le code dinamiche ed possibile implementare diversi algoritmi, sia sequenziali che ricorsivi. nelle strutture a Lista importante mantenere sempre il puntatore(testa) al primo elemento della lista (nell'esempio ^start) se si perde quel riferimento, la lista adr persa. supponiamo di essere un elemento che si vuole inserire nella lista. - se vogliamo inserirci alla fine baster allocarci da qualsiasi parte sulla ram, quindi modificare il puntatore next dell'ultimo elemento della lista in modo tale che punti a noi. - se vogliamo inserirci all'inizio invece dobbiamo FARCI UNA COPIA del puntatore di testa se no
perdiamo il riferimento alla lista, quindi modificare la testa in modo tale che punti a noi, e noi punteremo all'elemento precedentemente puntato dalla testa. la lista monolanciata, quindi possibile scorrerla in un solo senso, supponiamo ad esempio di fermarci al secondo elemento, e di volerci inserire tra il rpimo e il secondo, non possiamo modificare il puntatore del primo elemento, almeno che non scorriamo di nuovo tutta la lista fino ad arrivare al primo elemento. per ottimizzare il tutto baster inserirci dopo il secondo elemento (quindi tra il secondo ed il terzo, che fattibile senza dover ripercorrere la lista) e quindi invertire il nostro contenuto informativo con quello del secondo elemento.
Liste circolari
implementate negli scheduler, le liste circolari non terminano con NULL ma con il puntatore al primo elemento, in qeusto modo possibile percorrerle costantemente. supponaimoa d esmepio che ogni elemento sia un'operazione da compiere il processo E scorrerer in sequenza la lista, eseguendo l'operaizone che trova il processo M modificher invece i collegamenti, ed eventualmente inserir nuove oeprazioni per modificare il percorso del processo E.
Alberi e Grafi
le strutture dati consentono di modellizzare la realt, a seconda della complessit di un fenomeno fisico, esso pu essere rappresentato cons trutture lineari come gli array oppure con strutture dinamiche o ad oggetti. La Vita non pu essere rappresentata con strutture dati lineari come gli array, altrimenti potrebbe essere zippata.. la Vita sinonimo di Infinito, spazia in ogni dimensione e quindi non pu essere modellizzata con una strutture dati e tantomento compressa, poich ognia spetto,a nche il pi piccolo una parte di Infinito. Nell'esempio precedente possiamo immaginare il processo E come un treno che si muove ed il processo M come il gestore del percorso che lo dirige. le strutture ad Albero ed i grafi consentono di rappresentare la realt ottimizzando ulteriormente laq modellizzaizone. un grafo un insieme di nodi da cui dipartono dei grafi quindi una struttura ricorsiva e pu essere immaginata come un albero fatto di nodi e di rami da ogni ramo si collega ad un nodo da cui dipartono altri rami, e cos via. Pensiamo ad esempio ad Internet, Internet un grafo, gestito da degli algoritmi che lavorano suui grafi, come il famoso algoritmo di Dijkstra. gli alberi sono un particolare tipo dig rafo in cui due rami non possono puntare allo stesso nodo, e tra questi si distinguono gli alberi bilanciati nei quali da un nodo dipartono solo due rami ed il numero di nodi e sottonodi presenti in una diramazione devono corrispondere a quelli presenti nell'altra diramazione. Questa struttura dati particolarmente indicata per la ricerca dicotomica.
Puntatori in C
Il C consente di gestire, oltre al contenuto delle variabili stesse, anche i loro indirizzi (ovvero le loro locazioni in memoria) attraverso il meccanismo dei puntatori. Fino ad oggi abbiamo gestito le variabili all'interno dei blocchi di codice in cui tali variabili erano visibili, quindi non c' stata la reale necessit di utilizzare l'indirizzo della locazione di memoria in cui i valori di tali variabili erano stati memorizzati. L'uso per a volte diventa indispensabile all'interno delle funzioni (anche nella scanf, come avevo anticipato, si usava implicitamente un puntatore per stabilire l'area fisica di memoria in cui salvare la variabile letta da input). Un puntatore ha una sintassi simile:
int a=3; // Variabile int *x; // ''Puntatore'' ad una variabile di tipo int x=&a; *x=4; // Il puntatore ''x'' contiene l'indirizzo della variabile ''a'' // In questo modo modifico il contenuto del valore puntato da x, // quindi modifico indirettamente il valore di a
&a identifica l'indirizzo in memoria al quale si trova la variabile a, indirizzo che viene salvato nel puntatore x. Quest'uso dei puntatori dovrebbe farci tornare alla mente la sintassi della scanf:
int a; printf ("Inserisci un valore intero: "); scanf ("%d",&a);
Ora possiamo capire appieno la sintassi della scanf. una funzione che non fa altro che leggere, in questo caso, un valore intero da tastiera, e salvarlo nell'indirizzo fisico di memoria in cui si trova la variabile a (&a). Ovviamente, se voglio salvare un valore letto da tastiera in una variabile a cui gi associato un puntatore, non ho bisogno di ricorrere alla scrittura di sopra:
#include <stdio.h> main() { int a; int *x=&a; printf ("Inserisci un valore intero: "); // Salvo il valore immesso direttamente nell'allocazione // di memoria puntata da ''x'', ovvero nella variabile ''a'' scanf ("%d",x); printf ("Valore salvato all'indirizzo: 0x%x: %d\n",x,a); }
dove 0xbfc16a24 , in questo caso, l'indirizzo fisico di memoria (in formato esadecimale) in cui si trova la variabile intera a (e quindi il valore 4, in questo caso). Ricordiamo che sulle macchine a 32 bit
(quindi tutte le macchine Intel dal 486 in su, escluse quelle a 64 bit come Itanium e simili) un indirizzo di memoria sempre grande 32 bit (come in questo caso), quindi in memoria un puntatore occuper sempre, indipendentemente dalla variabile a cui punta, 32 bit = 4 byte.
Vogliamo ora implementare questo codice in una funzione a parte che viene poi richiamata dal nostro programma. Con le nostre conoscenze attuali scriveremo un codice del genere:
#include <stdio.h> // Funzione per lo scambio void swap(int a, int b) { int tmp; tmp=a; a=b; b=tmp; } main() { int a=4; int b=3; printf ("a=%d, b=%d\n",a,b); swap(a,b); printf ("a=%d, b=%d\n",a,b);
compilandolo avremo una sorpresa inaspettata: i valori sono rimasti immutati. Questo perch alla funzione swap non passiamo le variabili fisicamente, ma passiamo i loro valori. Quando invochiamo una funzione, l'atto della chiamata crea in memoria una nuova area dello stack associata alla funzione appena chiamata. In questo stack vengono copiati i valori degli argomenti passati. La funzione quindi non opera fisicamente sulle variabili passate, ma opera piuttosto su copie di esse. Quando la funzione termina l'area dello stack corrispondente viene distrutta, e con essa anche le copie delle variabili al suo interno, quindi non possibile tener traccia delle modifiche. La soluzione proprio quella di ricorrere ai puntatori, ovvero non passare alla funzione copie delle variabili, ma gli indirizzi fisici in cui esse si trovano, in modo che la funzione agir direttamente su
quegli indirizzi:
#include <stdio.h> // Funzione per lo scambio void swap(int *a, int *b) { int tmp; tmp=*a; *a=*b; *b=tmp; } main() { int a=4; int b=3; printf ("a=%d, b=%d\n",a,b); // Non passo le variabili alla funzione ma i loro indirizzi in memoria swap(&a,&b); printf ("a=%d, b=%d\n",a,b); } // tmp contiene il contenuto della variabile intera puntata da a // a contiene il contenuto della variabile intera puntata da b // b contiene il contenuto della variabile intera puntata da tmp
Puntatori e array
Nel paragrafo precedente abbiamo visto gli array come oggetti a s stanti, diversi da qualsiasi altro tipo di variabile e di dato che abbiamo incontrato. Ai fini del calcolatore per un array viene trattato esattamente alla stregua di un puntatore, un puntatore all'area di memoria dov' contenuto il primo elemento dell'array stesso. Esempio:
#include <stdio.h> main() { int v[] = {4,2,8,5,2}; // Queste due scritture sono equivalenti printf ("Primo elemento dell'array: %d\n",v[0]); printf ("Primo elemento dell'array: %d\n",*v); }
questo vuol dire che possiamo accedere a qualsiasi elemento dell'array specificando o il suo indice tra parentesi quadre o sommandolo al valore del puntatore al primo elemento:
#include <stdio.h> main() { int v[] = {4,2,8,5,2}; // Queste due scritture sono equivalenti printf ("Secondo elemento dell'array: %d\n",v[1]); printf ("Secondo elemento dell'array: %d\n",*(v+1)); }
questo perch quando viene instanziato un array non viene fatto altro che creare un puntatore ad una certa area della memoria centrale, per poi riservare tanto spazio in memoria quanto specificato dalla dimensione dell'array (nell'esempio di sopra lo spazio di 5 variabili int, una variabile int in genere grande 4 byte su una macchina a 32 bit quindi vengono riservati 5*4=20 byte a partire dall'indirizzo del primo elemento).
for (i=0; i<dim; i++) printf ("Elemento [%d]: %d\n",i,v[i]); } main() { int v[] = { 3,5,2,7,4,2,7 }; print_array(v,7); }
scanf ("%d",&n); v = (int*) malloc(n*sizeof(int)); for (i=0; i<n; i++) { printf ("Elemento n.%d: ",i+1); scanf ("%d",&v[i]); } for (i=0; i<n; i++) printf ("Elemento n.%d: %d\n",i+1,v[i]); }
La scrittura sizeof(int) ritorna la dimensione di una variabile int sulla macchina in uso, quindi n*sizeof(int) il numero di byte effettivi da allocare in memoria (ovvero nella malloc diciamo di allocare in memoria n blocchi di dimensione sizeof(int) l'uno che ospiteranno n variabili intere, e salviamo l'indirizzo a cui comincia questa zona di memoria nel puntatore v). Ci possibile anche con array di dimensione superiore a 1 (es. per le matrici):
#include <stdio.h> #include <stdlib.h> main() { int **m; int i,j; int m,n; printf ("Numero di righe della matrice: "); scanf ("%d",&m); printf ("Numero di colonne della matrice: "); scanf ("%d",&n); m = (int**) malloc(m*n*sizeof(int)); // Inizializzo anche tutti i sotto-vettori, ovvero le righe della matrice for (i=0; i<m; i++) m[i] = (int*) malloc(n*sizeof(int)); for (i=0; i<m; i++) for (j=0; j<n; j++) { printf ("Elemento [%d][%d]: ",i+1,j+1); scanf ("%d",&v[i][j]); } for (i=0; i<m; i++) for (j=0; j<n; j++) printf ("Elemento [%d][%d]: %d\n",i+1,j+1,v[i]);
Puntatori a funzioni
Le funzioni a basso livello non sono altro che sequenze di istruzioni binarie piazzate nella memoria centrale, al pari di una qualsiasi variabile. quindi possibile anche costruire puntatori che puntino a
funzioni, in quanto normali aree di memoria. La sintassi la seguente: tipo (*nome_ptr)(argomenti) = funzione Per richiamare la funzione puntata, basta poi un (*nome_ptr)(argomenti) Esempio:
#include <stdio.h> void foo() { printf ("Ciao\n"); } main() { void (*ptr)(void) = foo; printf ("foo si trova all'indirizzo 0x%.8x\n",ptr); (*ptr)();
o ancora
#include <stdio.h> int foo(int a, int b) return a+b; } main() {
{ int a=2,b=3; int (*ptr)(int, int) = foo; printf ("foo si trova all'indirizzo 0x%.8x\n",ptr); printf ("%d+%d=%d\n",a,b,(*ptr)(a,b));
Funzioni di callback
La conseguenza immediata dei puntatori a funzione sono le funzioni di callback. Ovvero, nulla mi impedisce, a questo punto, di passare come parametro di una funzione un puntatore a funzione, e la funzione richiamata pu richiamare la funzione passata come argomento. Esempio:
#include <stdio.h> void print () { printf ("Ciao\n"); } int foo(void (*f)(void)) f(); } main() } { foo(print); {
Stringhe
.
La gestione delle stringhe alla base della programmazione in qualsiasi linguaggio di programmazione. Ogni oggetto che viene stampato sullo schermo una stringa. I messaggi che abbiamo scritto finora su stdout con la printf non sono altro che stringhe, lo stesso vale anche per le stringhe di formato della scanf ecc. In C una stringa non altro che un array di elementi di tipo char. Linguaggi di programmazione pi moderni, come Java, Perl, Python, PHP e lo stesso C++, tramite l'uso della classe 'string', consentono di usare le stringhe in modo pi avanzato, come tipi predefiniti all'interno del linguaggio stesso. La visione del C (ovvero stringhe=array di tipo char) pu essere pi macchinosa e a volte anche pi pericolosa, ma mette in mano al programmatore la gestione di questo tipo di entit al 100%.
La dichiarazione vista sopra non comodissima, ragion per cui il C consente di dichiarare le stringhe direttamente cos:
char my_string[] = "Hello";
Ovviamente possiamo anche dichiarare delle stringhe senza inizializzarle. In questo caso le dichiariamo specificando il nome e la dimensione:
char my_string[20]; // Stringa che pu contenere 20 caratteri
e vale anche lo stesso discorso che abbiamo fatto con gli array per l'inizializzazione dinamica di una stringa:
char *my_string; int n; ....... printf ("Quanti caratteri deve contenere la tua stringa? "); scanf ("%d",&n); my_string = (char*) malloc (n*sizeof(char));
Per leggere una stringa invece possiamo ricorrere alla funzione scanf, passando come stringa di
formato '%s':
char str[20]; ........ printf ("Inserisci una stringa: "); scanf ("%s",str);
Si noti che non ho usato la scrittura '&str' nella scanf, in quanto la stringa gi di suo rappresenta un puntatore (in quanto un array non altro che, a livello del compilatore, un puntatore al suo primo elemento, come abbiamo visto prima). Attenzione: l'uso della scanf per la lettura delle stringhe potenzialmente dannoso per la stabilit e la sicurezza di un programma. In seguito valuteremo metodi per fare letture in tutta sicurezza. Esercizio pratico: un programmino che prende in input una stringa e trasforma tutti i suoi eventuali caratteri alfabetici maiuscoli in caratteri minuscoli:
#include <stdio.h> #include <string.h> // Funzione per la conversione di tutti i caratteri // maiuscoli in caratteri minuscoli void toLower(char *s) { int i; for (i=0; i<strlen(s); i++) // Se il carattere corrispondente della stringa // un carattere maiuscolo, ovvero compreso tra A e Z... if ( (s[i]>='A') && (s[i]<='Z') ) s[i]+=32;
main() { char s[20]; int i; printf ("Inserisci una stringa: "); scanf ("%s",s); toLower(s); printf ("Stringa convertita completamente in caratteri minuscoli: %s\n",s); }
Da notare l'uso della funzione strlen, definita in string.h. Tale funzione ritorna la lunghezza di una stringa, ovvero il numero di caratteri presenti fino al carattere terminatore della stringa. Ogni stringa possiede infatti un carattere terminatore per identificarne la fine (ovvero fin dove il compilatore deve leggere il contenuto della stringa). Tale carattere , per convenzione, il carattere NULL, identificato dalla sequenza di escape '\0' e associato al codice ASCII 0. Ogni stringa quindi, anche se non specificato, ha N+1 caratteri, ovvero gli N caratteri che effettivamente la compongono e il carattere NULL che ne identifica la fine:
char *str = "Hello"; // In realt a livello del compilatore 'str' vista come // 'H','e','l','l','o','\0'
Con le conoscenze che abbiamo in questo momento possiamo anche capire come scritto il codice della funzione strlen:
int strlen(char *s) int len; {
ovvero un ciclo for dove la variabile contatore viene incrementata finch il carattere corrispondente all'indice all'interno della stringa non uguale al carattere NULL (appunto con codice ASCII uguale a 0). Il valore della variabile contatore a questo punto rappresenta il numero effettivo di caratteri fino al NULL, ovvero il numero effettivo di caratteri all'interno della string, valore che viene ritornato dalla funzione. Scrivere un
for (i=0; i<strlen(s); i++)
equivale quindi a dire "cicla finch la stringa s contiene dei caratteri, o finch non viene raggiunta la fine della stringa". Questa scrittura
if ( (s[i]>='A') && (s[i]<='Z') ) s[i]+=32;
equivale a dire "se il carattere attuale maggiore o uguale ad A e minore o uguale a Z, ovvero una lettera maiuscola, somma al suo valore ASCII attuale il valore 32". 32 l'offset che nella tabella dei caratteri ASCII esiste tra i caratteri maiuscoli e quelli minuscoli. Per verificare:
printf ("%d\n",'a'-'A');
strcmp
La funzione strcmp (STRing CoMPare) confronta tra di loro i valori di due stringhe, il suo prototipo qualcosa di simile: int strcmp(const char *s1, const char *s2); dove s1 e s2 sono le due stringhe da confrontare. La funzione ritorna
Un valore > 0 se da un confronto byte a byte s1 ha pi caratteri il cui codice ASCII maggiore del corrispondente codice ASCII di s2 0 se le due stringhe sono uguali Un valore < 0 nei casi rimanenti
questa funzione utilizzatissima per vedere se due stringhe hanno lo stesso contenuto. Sono infatti da evitare come la peste scritture del genere:
questo perch la scrittura sopra non fa altro che confrontare gli indirizzi fisici in memoria delle due stringhe, ed effettuare le operazioni richieste se gli indirizzi coincidono. Ci ovviamente non sar mai verificato, dato che due variabili diverse in memoria hanno anche indirizzi diversi, quindi il codice scritto sopra praticamente inefficiente. Per confrontare due stringhe invece necessario ricorrere alla funzione strcmp, ricordando che la funzione ritorna 0 quando il contenuto di due stringhe lo stesso. Ecco quindi la versione corretta del codice di sopra:
char *s1; char *s2="pippo"; ........ if (!strcmp(s1,s2)) // Equivale a scrivere // if (strcmp(s1,s2)==0) printf ("Ciao pippo\n");
strncmp
La funzione strncmp molto simile a strcmp, con l'eccezione che confronta solo i primi n caratteri sia di s1 che di s2. La sua sintassi qualcosa di simile: int strncmp(const char *s1, const char *s2, size_t n); dove n il numero di caratteri da confrontare.
strcpy
La funzione strcpy copia una stringa in un'altra. La sua sintassi qualcosa di simile: char *strcpy(char *dest, const char *src); dove dest la stringa all'interno della quale viene copiato il nuovo valore e src la stringa da copiare. Il valore di ritorno della funzione un puntatore a dest. Quando si vuole copiare una stringa in un altra infatti sconsigliabile usare una scrittura del genere:
char *s1="pippo"; char *s2; s2=s1; // ATTENZIONE!!
La scrittura di sopra infatti copia il puntatore al primo elemento della stringa s1 nella stringa s2. Ci vuol dire che ogni eventuale modifica di s1 modifica anche s2, dato che entrambe le variabili agiscono sulla stessa zona di memoria, e viceversa, il che decisamente un effetto collaterale. La scrittura corretta qualcosa del tipo
char *s1="pippo";
in quanto la funzione strcpy genera in s2 una copia esatta di s1, che per, essendo residente in una zona di memoria diversa, completamente indipendente da s1. La funzione strcpy ha un codice simile:
char* strcpy (char *s1, char *s2) int i; {
// Finch la stringa s2 ha dei caratteri... for (i=0; i<strlen(s2); i++) // ...copia il carattere in s1 s1[i]=s2[i]; return s1; }
Questa funzione per potenzialmente dannosa per la sicurezza dell'applicazione. Vedi il paragrafo su "Uso delle stringhe e sicurezza del programma".
strncpy
La funzione strncpy ha una sintassi molto simile a strcpy, con la differenza che copia solo i primi n caratteri della stringa sorgente nella stringa di destinazione, per tenere il processo di copia sotto controllo ed evitare problemi di sicurezza nell'operazione, come vedremo in seguito. La sua sintassi char *strncpy(char *dest, const char *src, int n); La sintassi la stessa di strcpy, a parte per n, che identifica appunto il numero di caratteri di src che verranno copiati in dest. L'uso di questa funzione preferibile a quello di strcpy quando possibile, proprio per evitare problemi di sicurezza legati ad una copia non controllata.
strcat
La funzione strcat concatena l'inizio di una stringa alla fine di un'altra. La sua sintassi char *strcat(char *dest, const char *src); dove dest la stringa alla cui fine viene concatenata la stringa src. Esempio di utilizzo:
#include <stdio.h> #include <string.h> main() { char s1[20]; char *s2 = "pippo"; // Copio all'interno di s1 la stringa "Ciao " // copiando esattamente il numero di byte che // mi servono, tramite l'operatore sizeof strncpy (s1,"Ciao ",sizeof("Ciao ")); strcat (s1,s2); // s1 ora contiene "Ciao pippo"
La funzione strcat ritorna un puntatore a char che rappresenta un puntatore alla zona di memoria dove salvato dest. Attenzione: anche la strcat, cos come la strcpy ed altre funzioni che vedremo in seguito, sul libro nero delle funzioni a potenziale rischio di sicurezza per un'applicazione. Il suo uso, quando possibile, va evitato.
strncat
La sua sintassi molto simile a strcat, con la differenza che in strncat vanno specificati anche il numero di caratteri di src da copiare in dest, in modo da tenere la copia sotto controllo. La sua sintassi char *strncat(char *dest, const char *src, int n); dove n rappresenta il numero di caratteri di src da copiare. Il suo uso, quando possibile, preferibile a quello di strcat.
strstr
La funzione strstr serve per verificare se esiste una sottostringa all'interno della stringa di partenza. La sua sintassi char *strstr(const char *haystack, const char *needle); dove haystack (lett. 'pagliaio') la stringa all'interno della quale cercare, needle (lett. 'ago') la stringa da cercare (da notare ancora una volta il sottile umorismo degli sviluppatori del C). La funzione ritorna
Un puntatore intero, che rappresenta la zona di memoria in cui stata trovata la sottostringa, nel caso in cui la sottostringa dovesse essere trovata NULL nel caso in cui la sottostringa non dovesse essere trovata
Esempio:
/* * Questo programmino chiede in input all'utente due stringhe e * verifica se la seconda stringa localizzata all'interno della prima */ #include <stdio.h> #include <string.h> main() { char s1[32]; char s2[32]; printf ("Inserire la stringa all'interno della quale cercare: "); scanf ("%s",s1); printf ("Inserire la stringa da cercare: "); scanf ("%s",s2); if // // // (strstr(s1,s2)) Equivale a scrivere if (strstr(s1,s2) != 0) ovvero se il valore di ritorno della funzione non NULL
printf ("Stringa \"%s\" trovata all'interno di \"%s\", in posizione %d\n", s2,s1,(strstr(s1,s2)-s1)); else printf ("Stringa \"%s\" non trovata all'interno di \"%s\"\n",s2,s1);
Si noti questa scrittura: strstr(s1,s2)-s1 strstr ritorna infatti l'indirizzo dell'area di memoria in cui si trova s2 all'interno di s1. Se a questo indirizzo sottraggo l'indirizzo di s1, ovvero l'indirizzo del primo carattere di s1, ottengo la locazione effettiva della sottostringa all'interno della stringa di partenza. Se ad esempio s1="Ciao pippo" e s2="pippo", strstr(s1,s2)-s1 = 5.
Anche la funzione sprintf sulla lista di quelle da usare con cautela, e solo quando si sicuri che la stringa di destinazione in grado di contenere tutti i byte che si stanno per copiare al suo interno.
snprintf
La funzione snprintf un'alternativa pi sicura alla sprintf, e al suo interno va specificato anche il numero massimo di caratteri da copiare. La sua sintassi quindi int snprintf(char *str, int size, const char *format, ...); dove size rappresenta il numero massimo di byte della stringa di formato da copiare all'interno di str.
sscanf
La funzione sscanf del tutto analoga alla scanf classica, solo che invece di leggere i dati dalla tastiera li legge dall'interno di una stringa. Esempio, ecco un uso classico di sscanf. Abbiamo una stringa che rappresenta una data, in formato 'gg/mm/aaaa'. Vogliamo ottenere, dall'interno di questa stringa, il giorno, il mese e l'anno e salvarli all'interno di 3 variabili intere. Con sscanf la cosa presto fatta:
char *date = "13/08/2007"; int d,m,y; sscanf (date,"%d/%d/%d",&d,&m,&y); // d=13, m=8, y=2007
La funzione ritorna un intero che rappresenta il numero di campi letti all'interno della stringa. Il controllo su questo valore di ritorno pu tornare utile per verificare se l'utente ha inserito la stringa nel formato giusto:
#include <stdio.h> #include <stdlib.h> main() { char date[16]; int d,m,y; printf ("Inserisci una data: "); scanf ("%s",date); // Se non leggo almeno 3 interi nella stringa // inserita separati da '/'... if (sscanf(date,"%d/%d/%d",&d,&m,&y) != 3) { printf ("Errore: data inserita non valida: %s\n",date); exit(1); } printf ("Giorno: %d\n",d); printf ("Mese: %d\n",m); printf ("Anno: %d\n",y);
gets
Un piccolo limite della lettura delle stringhe sta nel fatto che la lettura si interrompe quando incontra uno spazio. Se ad esempio un'applicazione richiede all'utente di inserire una stringa e l'utente inserisce "Ciao mondo", se la lettura avviene tramite scanf molto probabilmente la stringa risultante dopo la lettura sar semplicemente "Ciao". Per evitare questo piccolo inconveniente si ricorre alla funzione gets, nonostante il suo uso sia deprecato dai nuovi standard C. Esempio di utilizzo:
#include <stdio.h> main() { char str[32]; printf ("Inserisci una stringa: "); gets (str); // Se ora inserisco stringhe con degli spazi in mezzo vengono salvate ugualmente nella // stringa finale, in quanto la gets legge tutti i caratteri fino al fine linea printf ("Stringa inserita: %s\n",str); }
Attenzione: anche la gets nella lista delle funzioni a rischio. Presto vedremo subito in che modo operare sulle stringhe con le funzioni giuste e nel modo giusto, e i rischi che si ottengono lavorando in modo non controllato sulle stringhe.
atoi
La funzione atoi (ASCII to int), definita in stdlib.h, usata per convertire una stringa in un valore intero. La sua sintassi molto semplice: int atoi(const char *nptr); e restituisce il valore convertito. Nel caso in cui la stringa non contenga un valore numerico valido, la funzione ritorna zero. Esempio di utilizzo:
#include <stdio.h> #include <stdlib.h> main() { int n; char *s = "3"; n=atoi(s); // Ora n contiene il valore intero 3 }
Della stessa famiglia sono le funzioni atol (ASCII to long) e atof (ASCII to float).
La prima stringa del vettore argv contiene infatti il nome dell'eseguibile (quindi la variabile argc sempre settata almeno a uno). Gli eventuali argomenti successivi passati al programma vengono salvati in argv[1],...,argv[n]. Esempio pratico:
#include <stdio.h> main(int argc, char **argv) int i; {
Se compiliamo questo eseguibile come 'stampa_arg' e lo invochiamo con gli argomenti "Ciao mondo come stai", cos (in ambiente Unix): ./stampa_arg Ciao mondo come stai avremo come output qualcosa del tipo Argomenti passati al programma: Ciao mondo come stai
Eseguendo un codice del genere molto probabilmente l'applicazione andr in crash. Se siamo su un sistema Unix il kernel ci risponder con un bel segmentation fault, su Windows ci comparir una finestra che ci avverte che l'applicazione ha tentato la scrittura su un'area di memoria non valida. Quello che abbiamo fatto tentare di copiare in un buffer pi byte di quelli che il buffer stesso pu contenere, e in modo non controllato (la strcpy non effettua una copia controllata, non si ferma se i limiti di capienza della stringa vengono raggiunti). Il risultato che l'applicazione va in crash, in quanto, attraverso la strcpy, andata a scrivere su una zona di memoria al di fuori di quella della stringa stessa, andando a sovrascrivere l'indirizzo di ritorno della funzione con un indirizzo non valido. L'indirizzo viene letto dalla CPU, che tenta di leggere l'istruzione a quell'indirizzo. Indirizzo che nella maggior parte dei casi non sar un indirizzo di memoria valido, quindi provocher il crash del programma. Ma il crash del programma, nonostante sia un danno non da poco, non nemmeno il minore dei danni. Esempio pratico con un'applicazione:
#include <stdio.h> #include <string.h> main(int argc, char **argv) char str[16]; strcpy (str,argv[1]); } {
Attenzione all'uso di strcpy in questa applicazione. La funzione copia il primo argomento passato al programma nella stringa str, che pu tenere 16 caratteri, senza fare ulteriori controlli sulla lunghezza effettiva della stringa da copiare. Proviamo ad avviare l'applicazione con il nostro debugger preferito (in questo caso user Gdb) per vedere cosa succede in memoria quando passo al programma un argomento molto lungo: (gdb) run `perl -e 'print "A" x32'` Starting program: /home/blacklight/prog/c/5 `perl -e 'print "A" x32'` Program received signal SIGSEGV, Segmentation fault. 0x41414141 in ?? () Il comando `perl -e 'print "A" x32'` non fa altro che richiamare l'interprete Perl (un linguaggio di programmazione), stampando la lettera "A" 32 volte (un modo per evitare di scrivere 32 volte "A", giusto una comodit). Il programma, tentando di copiare un buffer troppo grande in una stringa che non in grado di contenere tanti byte, va in crash. Ma vediamo cosa succede a livello dei registri: (gdb) i r eax 0xbfab7890 -1079281520 ecx 0xfffff098 -3944 edx 0xbfab8818 -1079277544 ebx 0xb7edeffc -1209143300 esp 0xbfab78b0 0xbfab78b0 ebp 0x41414141 0x41414141 esi 0xbfab7934 -1079281356 edi 0xbfab78c0 -1079281472 eip 0x41414141 0x41414141 eflags 0x210282 [ SF IF RF ID ] cs 0x73 115 ss 0x7b 123 ds 0x7b 123 es 0x7b 123 fs 0x0 0 gs 0x33 51 Da notare il registro EIP. Tale registro contiene, nelle architetture Intel-based, l'indirizzo in memoria della prossima istruzione da eseguire. L'indirizzo stato sovrascritto da una sequenza di 0x41. E 0x41, in esadecimale, corrisponde al carattere ASCII "A". In pratica il nostro buffer lungo andato a sovrascrivere il registro EIP, cambiando il valore dell'indirizzo della prossima istruzione da pescare in memoria. In questo caso, la sequenza di 0x41 non rappresenta un indirizzo di memoria valido, o almeno un indirizzo nel quale il programma pu accedere, ragion per cui il programma crasha. Ma, oltre ad una semplice sequenza di "A", possiamo anche inserire un buffer costruito apposta, che inietta nel registro un indirizzo valido che punta ad un codice arbitrario. Siamo quindi nella situazione di un buffer overflow sfruttato in modo da poter eseguire codice arbitrario sul sistema, codice che pu mirare ad aggiungere un nuovo utente con certi privilegi su quel sistema, ad ottenere i privilegi di amministratore in modo indebito o ad aprire una shell remota o locale in modo indebito. In ogni caso, quando un attaccante ha sfruttato un codice vulnerabile iniettando del codice arbitrario al suo interno ha il controllo totale della macchina, anche se indebito. I bollettini di sicurezza in giro per il web pullulano di bug del genere trovati ancora oggi in molte applicazioni, e dovuti proprio all'uso errato di funzioni come quelle che abbiamo visto sopra, bug che in genere sono corretti il pi in fretta possibile dopo la scoperta per evitare che i danni ai sistemi che usano quelle applicazioni diventino maggiori. Non vedremo in questa sede, per evitare di divagare troppo nel discorso, in che modo sfruttare tali
vulnerabilit per acquisire il controllo di un sistema, ma per ora ci basta sapere che usando certe funzioni la cosa possibile e sapere in che modo funziona. In conclusione, le funzioni potenzialmente vulnerabili a buffer overflow e da usare con cautela sono:
Queste funzioni vanno usate solo quando si sicuri al 100% delle dimensioni del buffer di destinazione. In alternativa, pi sicuro usare funzioni come
Soffermiamoci un attimo sulla fgets (ne faremo solo una trattazione sommaria per le stringhe in questa sede, mentre la studieremo in modo pi approfondito nel capitolo sui file). Abbiamo visto prima che, per la lettura di una stringa da input, sia l'uso di scanf che di gets pericoloso. Per leggere stringhe la cosa migliore fare ricorso a questa funzione, che prende come primo argomento la stringa di destinazione, come secondo argomento il numero massimo di caratteri da leggere da input e come terzo argomento il descrittore da cui leggere (nel nostro caso lo standard input, identificato da stdin). Esempio di uso:
char str[16]; printf ("Inserisci una stringa: "); fgets (str,sizeof(str),stdin); str[strlen(str)-1]=0; printf ("Stringa inserita: %s\n",str);
La notazione sizeof(str) dice di leggere da input al massimo tanti caratteri quanti sono quelli supportati dalla dimensione di str (ovvero 16 in questo caso), mentre stdin, costante definita in stdio.h, identifica lo standard input. La scrittura str[strlen(str)-1]=0; serve perch la funzione fgets salva nella stringa anche la pressione del carattere invio. Questa scrittura setta il carattere NULL un byte prima, in modo da rimuovere il carattere invio dalla stringa. Attenzione anche ad evitare scritture del genere:
char *str = "Ciao"; printf (str);
Se non viene specificato esplicitamente il formato della stringa da stampare nella printf l'applicazione pu potenzialmente essere vulnerabile a format string overflow, una vulnerabilit scoperta abbastanza recentemente che consente di scrivere dati arbitrari sullo stack. Seguendo questi passi per evitare buffer overflow e format string overflow si pu essere sicuri almeno a un 70% di scrivere applicazioni relativamente sicure.
Algoritmi di ordinamento
.
Una delle caratteristiche irrinunciabili in un calcolatore la capacit di ordinare dati. cos irrinunciabile che il nome che i francesi danno al computer moderno ordinateur, ordinatore. Gli informatici nel corso degli anni hanno studiato e messo a punto molti algoritmi di ordinamento, ovvero algoritmi in grado di ordinare insiemi di dati (nel nostro caso array). Ci che differenzia un algoritmo dall'altro il suo grado di ottimizzazione, ovvero il numero medio di passi compiuti per giungere allo scopo finale (ovvero avere un vettore ordinato in senso crescente o decrescente), e spesso e volentieri un algoritmo abbastanza immediato per il nostro modo di ragionare non lo per il calcolatore, e viceversa. Ecco che la necessit di risparmiare in fatto di tempo di esecuzione del codice sul calcolatore (necessit che diventa irrinunciabile quando si deve ordinare una grande mole di dati) ha portato col tempo allo sviluppo di algoritmi di ordinamento via via pi complessi per la logica umana, ma estremamente ottimizzati per il calcolatore. In questa sede prenderemo in esame gli algoritmi pi usati, andando in ordine crescente in quanto a complessit (e decrescente in quanto a ottimizzazione):
Naive sort
Si tratta dell'algoritmo di ordinamento pi semplice e anche meno ottimizzato per il calcolatore. Quello che fa trovare in un vettore la posizione dell'elemento pi grande. Se la sua posizione non alla fine del vettore (infatti in un vettore ordinato in modo crescente l'elemento pi grande si trova alla fine) allora scambia tra di loro l'elemento all'ultima posizione e il valore massimo, in modo che l'elemento pi grande si trovi all'ultima posizione. All'iterazione successiva viene considerato il vettore come di dimensione dim-1, dove dim la dimensione di partenza. Vengono effettuate tali iterazioni finch la dimensione del vettore non uguale a 1 (ovvero il vettore ordinato). Esempio pratico dell'algoritmo: v = {1,0,5,4} v = {1,0,4,5} v = {0,1,4,5} Ed ecco come scriverlo in C (esempio applicato a un vettore di interi):
// Procedura per lo scambio dei valori tra due variabili void swap (int *a, int *b) { int *tmp; tmp=a; a=b; b=tmp; } int findPosMax(int *v, int n) { int i,p=0; /* ipotesi: max = v[0] */
// Ciclo su tutti gli elementi dell'array for (i=1; i<n; i++) // Se l'elemento attuale maggiore dell'elemento massimo, // allora il nuovo indice del massimo quello appena trovato if (v[p]<v[i]) p=i; return p; {
// Finch nel vettore ci sono elementi... while (dim>1) { // ...trova la posizione dell'elemento pi grande p = findPosMax(v, dim); // Se la sua posizione non alla fine del vettore, // scambia tra di loro l'elemento massimo e l'ultimo elemento if (p < dim-1) scambia(&v[p],&v[dim-1]); // Decrementa la dimensione del vettore dim--;
} }
Bubble sort
Il bubble sort un algoritmo pi efficiente del naive anche se leggermente meno intuitivo. Il difetto principale del naive sort infatti quello che non si accorge quando il vettore gi ordinato, e in tal caso continua a effettuare iterazioni su di esso. Il bubble sort corregge questo difetto considerando coppie adiacenti di elementi nel vettore, e non il vettore nella sua interezza, e partendo dal presupposto che il vettore sia ordinato. Se due coppie adiacenti qualsiasi sono scambiate tra di loro (prima il valore pi grande e poi quello pi piccolo) effettua uno scambio, e quindi vuol dire che il vettore non era ordinato. Se invece non si verifica alcuno scambio il vettore gi ordinato, e quindi l'algoritmo termina. Esempio applicativo:
Insert sort
L'insert sort un algoritmo che parte da un approccio diverso da quelli visti finora: per ottenere un vettore ordinato basta costruirlo ordinato, inserendo ogni elemento al posto giusto. Ecco un esempio grafico:
Per implementarlo useremo due funzioni. La funzione insertSort prende come parametri il vettore da ordinare e la sua dimensione, e, per i che va da 0 a N-1, inserisce alla posizione corretta all'interno del sottovettore v[0],...,v[i] l'i-esimo elemento del vettore:
void insertSort(int *v, int dim) int i; {
// Ciclo su tutti gli elementi for (i=1; i<dim; i++) // Inserisco al posto giusto l'i-esimo elemento insMinore(v,i);
La funzione insMinore prende come parametri il vettore e la posizione dell'elemento da ordinare. Questa funzione determina la posizione in cui va inserito l'elemento alla posizione specificata, crea lo spazio per l'inserimento spostando gli elementi all'interno del vettore ed effettua l'inserimento:
void insMinore(int *v, int lastpos) int i, x = v[lastpos]; {
for (i = lastpos-1; i>=0 && x<v[i]; i--) v[i+1]= v[i]; /* crea lo spazio */ v[i+1]=x;
Quick sort
Avvicinandoci via via ad algoritmi sempre pi ottimizzati giungiamo al quick sort, algoritmo di default per l'ordinamento usato ancora oggi dal C. Il quick sort si basa su un principio relativamente semplice: ordinare un vettore di piccole dimensioni molto meno costoso dell'ordinare un vettore di grandi dimensioni. L'idea quella di dividere il vettore di principio in due sottovettori, con un elemento intermedio (chiamato pivot). Le celle di memoria prima del pivot conterranno tutti gli elementi minori del pivot, quelle successive gli elementi maggiori del pivot. A questo punto l'algoritmo viene applicato ricorsivamente ai due sottovettori, fino ad arrivare a vettori di dimensione unitaria che, per definizione,
Codice:
void qSort(int *v, int first, int last){ int i,j,pivot; if (first<last) { // Partenza: i parte dal primo elemento del vettore, j dall'ultimo i = first; j = last; // Il pivot l'elemento medio del vettore pivot = v[(first + last)/2]; do { // Finch l'elemento generico i-esimo a sinistra del pivot // minore del pivot, incrementa i while (v[i] < pivot) i++; // Finch l'elemento generico j-esimo a destra del pivot // maggiore del pivot, decrementa j while (v[j] > pivot) j--; // Altrimenti, scambia tra loro l'elemento i-esimo e quello j-esimo if (i <= j) { swap(&v[i], &v[j]); i++, j--; } } while (i <= j); // Cicla finch i e j non si incontrano // Richiama il quick sort sul primo sottovettore qSort(v, first, j);
Un altro utilizzo molto comodo per la definizione di nuovi dati di tipo vettoriale. Ad esempio, so che un codice fiscale sempre composto da 17 caratteri. Posso creare un nuovo tipo di dato dedicato alla memorizzazione dei codici fiscali in questo modo:
typedef char CF[17]; ......... CF c = "AAABBBCCCDDDEEEFF";
Le dichiarazioni dei nuovi tipi in genere vanno messe in modo da essere visibili a tutte le funzioni del programma, quindi o al di fuori del main o in un file header importato dall'applicazione.
Enumerazioni
Le enumerazioni in C si dichiarano attraverso la keyword enum, e hanno l'obiettivo di dichiarare nuovi tipi di dato con un dominio limitato, dove al primo valore dell'enumerazione viene associato il valore 0, al secondo il valore 1 e cos via. Ad esempio, in C non ho di default un tipo di dato per poter operare su tipi booleani. Posso per costruirmi un tipo di dato booleano grazie ad un enumerazione:
typedef enum { false, true } boolean;
In questo caso il primo campo dell'enumerazione false, a cui viene attribuito il valore 0, e il secondo true, a cui quindi viene attribuito il valore 1. Ora, grazie alla specifica typedef, posso usare questo tipo di dato all'interno del mio codice:
boolean trovato=false; ......... if (valore1==valore2) trovato=true;
In questo caso Lunedi=0, Martedi=1, ..., Domenica=6. All'interno del mio codice posso istanziare una variabile di questo tipo e sfruttarla cos:
giorno g1=Lunedi; giorno g2=Martedi; ......
Dati strutturati
Nella realt di tutti i giorni abbiamo a che fare con entit descritte da pi di una caratteristica, e anche con tipi diversi di caratteristiche. Per soddisfare questa esigenza, il C mette a disposizione i tipi strutturati. Esempio classico di tipo strutturato: un'automobile descritta da una targa, dall'anno di immatricolazione, dalla casa produttrice e dal modello. Ecco come implementare queste caratteristiche in C, creando il tipo di dato strutturato 'automobile':
typedef struct { char targa[16]; char marca[16]; char modello[16]; int anno_imm; } automobile;
Usando la keyword typedef, posso usare questo tipo di dato all'interno del mio programma direttamente cos:
automobile a;
In questo caso, posso usare il tipo di dato strutturato all'interno del mio programma ma specificando anche il fatto che faccio uso di un tipo di dato strutturato dichiarato in precedenza:
struct automobile a;
Per comodit e maggiore leggibilit del codice, in questo luogo useremo la prima scrittura (quella con il typedef). Per accedere ai dati contenuti all'interno di una struttura posso sfruttare un'istanza della struttura stessa (ad esempio, nel caso di sopra, una variabile di tipo 'automobile') e specificare il componente a cui voglio accedere separato da un punto '.'. Esempio:
typedef struct { char targa[16]; char marca[16]; char modello[16]; int anno_imm; } automobile; ........ automobile a; printf ("Inserisci la targa: "); scanf ("%s",[Link]); printf ("Inserisci la marca: "); scanf ("%s",[Link]); printf ("Inserisci il modello: "); scanf ("%s",[Link]); printf ("Inserisci l'anno di immatricolazione: "); scanf ("%d",&a.anno_imm); printf printf printf printf ("Targa: %s\n",[Link]); ("Marca: %s\n",[Link]); ("Modello: %s\n",[Link]); ("Anno di immatricolazione: %d\n",a.anno_imm);
automobile a[10]; int i; for (i=0; i<10; i++) { printf ("Automobile n.%d\n\n",i+1); printf ("Inserisci la targa: "); scanf ("%s",[Link]); printf ("Inserisci la marca: "); scanf ("%s",[Link]); printf ("Inserisci il modello: "); scanf ("%s",[Link]); printf ("Inserisci l'anno di immatricolazione: "); scanf ("%d",&a.anno_imm); }
e ovviamente vale lo stesso discorso fatto con gli array di tipi primitivi per quanto riguarda l'inizializzazione dinamica:
// Puntatore alla struttura automobile automobile *a; int i,n; printf ("Inserire i dati di quante automobili? "); scanf ("%d",&n); // Inizializzazione dinamica del vettore di automobili a = (automobile*) malloc (n*sizeof(automobile)); for (i=0; i<n; i++) { printf ("Automobile n.%d\n\n",i+1); printf ("Inserisci la targa: "); scanf ("%s",[Link]); printf ("Inserisci la marca: "); scanf ("%s",[Link]); printf ("Inserisci il modello: "); scanf ("%s",[Link]); printf ("Inserisci l'anno di immatricolazione: "); scanf ("%d",&a.anno_imm); }
Posso anche dichiarare puntatori a strutture e accedere alle strutture stesse tramite questi puntatori. In questo caso, invece del punto '.' per accedere ad un certo elemento della struttura il C propone un operatore apposito, l'operatore '->':
// Puntatore a struttura automobile *a; printf ("Inserisci la targa: "); scanf ("%s",a->targa);
printf ("Inserisci la marca: "); scanf ("%s",a->marca); printf ("Inserisci il modello: "); scanf ("%s",a->modello); printf ("Inserisci l'anno di immatricolazione: "); scanf ("%d",&a->anno_imm); printf printf printf printf ("Targa: %s\n",a->targa); ("Marca: %s\n",a->marca); ("Modello: %s\n",a->modello); ("Anno di immatricolazione: %d\n",a->anno_imm);
La direttiva #include
Solitamente anche nei programmi pi banali si usa la direttiva #include per, appunto, includere nel sorgente file esterni o librerie. Per includere una libreria si usano le parentesti angolari < e >, mentre per includere un file esterno o magari nella stessa cartella del programma si usano i doppi apici ". Un esempio di inclusione di una libreria e un file che si trova nella cartella superiore di dove si trova il sorgente in cui la includiamo:
#include <stdio.h> #include "../file1.h"
In questo caso il preprocessore quando incontrer queste righe le sostituir con il contenuto del file richiamato. In Unix solitamente i file d'intestazione specificati nelle parentesi angolari si trovano nel percorso /usr/ include/. Nei file inclusi possono naturalmente anche esserci altre direttive al preprocessore che verranno poi a loro volta "lavorate".
La direttiva #define
La direttiva #define si usa, appunto, per definire qualcosa ad esempio:
In questo caso la definizione scrivi che va a sostituire la parola printf quindi nel corso del programma al posto di:
printf("Ciao preprocessore!");
Si potr scrivere
scrivi("Ciao preprocessore!");
Comunque il define pu anche definire numeri, simboli o altro. Vari esempi di define:
#define EQ == #define OK printf("OK\n"); #define DEBUG 1
Nell'esempio sopra visualizzato si pu notare il cosidetto "ZUCCHERO SINTATTICO" dato che ogni tanto a un programmatore in C pu scappare di mettere un solo = nelle uguaglianze cos con la definizione EQ == si potr scrivere cos:
if ( a EQ b ) ...
Evitando errori di sintassi. Un'altra cosa da notare la definizione DEBUG molto utile nelle fasi di test di un programma che si pu usare nel controllo del flusso tramite sempre direttive al preprocessore che vedremo adesso.
Le variabili su cui eseguire controlli devono essere definite tramite #define Anche nel controllo del flusso tramite direttive al preprocessore si possono eseguire controlli con || ( OR ), && ( AND ) e != ( NOT ). La direttiva #endif "dice" al preprocessore che il controllo del flusso finito.
Per eseguire un debug con questo sistema si potrebbe inserire qualcosa tipo:
#if DEBUG 1 printf("x = %d\n", x); printf("y = %s\n", y); ... #endif
Ma ora vedremo con la direttiva #ifdef cosa si pu fare, in pratica "ifdef" sta per "se definito" quindi si pu tramite essa controllare se una variabile stata definita o meno e con l'aggiunta delle direttive #undef e #ifndef vedremo cosa si pu fare con l'esempio seguente:
#include <stdio.h> #define NUMERO 4 int main(void) { #ifndef NUMERO #define NUMBER 4 #ifdef NUMBER #undef NUMBER #define NUMERO 4 #endif return 0; }
Innanzitutto chiariamo cosa vuol dire ifndef e undef, la prima equivale a "se non definito" ( if not define ) mentre la seconda equivale a "togli la definizione" ( undefine ). Nell'esempio sopra definiamo NUMERO dopodich all'interno del corpo main iniziamo col verificare se non definito numero, se ci vero definiamo NUMBER, se invece definito NUMBER togliamo la definizione di NUMBER e definiamo NUMERO. Dopodich si esce dal programma. La direttiva #undef diciamo che inutile nei piccoli programmi, ma risulta utilissima nei programmi di grandi dimensioni composti magari da molti file e da molte persone che ci lavorano e senza andare in giro o sfogliare tra i file se una cosa stata definita o meno questa semplice direttiva ci facilita la vita. L'uso di #ifdef utilissimo nel caso in cui si vogliano usare dei file header. Infatti, un file header potrebbe essere incluso in due diversi file sorgenti che si vanno a compilare insieme, e questo potrebbe generare ambiguit ed errori in fase di compilazione (funzioni o dati che risulterebbero dichiarati due volte). Per evitare questo problema si usano proprio le direttive al preprocessore. Nell'header che andremo a creare avremo una cosa del genere:
Se la variabile _NOMEHEADER_H non definita Definisci la variabile Dichiara tutto il contenuto dell'header Altrimenti, termina la dichiarazione dell'header
In codice:
#ifndef #define _MIOHEADER_H _MIOHEADER_H
In pratica, una volta definita la macro _MIOHEADER_H il file header non verr pi incluso in nessun altro file, risolvendo quindi gli eventuali problemi di header definiti due o pi volte.
PD sta per "Potenza di Due" in pratica se nel corso del programma si esegue qualcosa tipo:
a = PD(3);
Questa definizione una volta richiamata rilascia il valore pi piccolo tra i due numeri dati. Naturalmente l'utilizzo epsanso di parentesi consente di essere sicuri che una volta espansa la stringa tutte le operazioni vengano eseguite nel modo desiderato.
Macro predefinite
Nel C esistono 5 tipi di macro gi definite sempre disponibili che non possono essere ridefinite dal programmatore. Si possono vedere nello schema seguente:
/* MACRO || COSA CONTIENE */ __DATE__ /* Una stringa che contiene la data corrente */ __FILE__ /* Una stringa che contiene il nome del file */ __TIME__ /* Una stringa che contiene l'ora corrente */ __LINE__ /* Un intero che raprresenta il numero di riga corrente */ __STDC__ /* Un intero diverso da 0 se l'implementazione segue lo standard ANSI C */
Operatori # e ##
Questo tipo di operatori sono disponibili solo nel C ANSI. L'operatore unario # trasforma un parametro formale di una definizione di macro in una stringa ad esempio:
Ora invece vediamo l'operatore binario ## che serve a concatenare token. Ad esempio:
#include <stdio.h> #define X(y) x ## y X(3) = X(4) = X(12) = ...
Funzione ricorsive
.
A volte capita di avere a che fare con problemi che sono difficilmente risolvibili ricorrendo a funzioni imperative standard. In alcuni casi, invece di avere una visione di insieme del problema da risolvere pu essere pi comodo avere una visione particolareggiata, progettare un algoritmo che risolva parte del problema e ripetere quest'algoritmo finch il problema non risolto del tutto.
Questo modo per implica una visione di insieme del problema, e per questo non la pi efficiente ( un algoritmo chiamato naive sort). E se invece dividessimo via via l'array in parti pi piccole, fino ad arrivare ad array contenenti ognuno due elementi? Potremmo ordinare ognuno di questi mini-array (si tratterebbe al massimo di fare uno scambio tra due elementi), quindi ricorsivamente in questo modo risalire ad un array ordinato. Questa la soluzione pi ottimizzata in termini di prestazioni, ed implica un nuovo approccio alla risoluzione di un problema: un approccio ricorsivo. Dal particolare (l'ordinamento di array di due elementi) si passa al generale (l'ordinamento di un intero array di dimensioni maggiori), facendo in modo che la funzione di ordinamento richiami sempre se stessa (questo un algoritmo di merge sort, implementato di default in linguaggi come Java e Perl).
intero n n! = n*(n-1)*(n-2)*...*1 Con i cicli classici che abbiamo visto finora potremmo scriverlo cos:
/* Questo il main() */ main() { int n; printf ("Inserire un numero intero: "); scanf ("%d",&n); printf ("Fattoriale di %d: %d\n",n,fat(n)); } /* Questa la funzione che calcola il fattoriale */ int fat(int n) { int i,f=1; for (i=n; i>0; i--) f *= i; return f; }
Vediamo ora come riscrivere la funzione fat() in modo ricorsivo, senza nemmeno usare il ciclo for. Di volta in volta la variabile di appoggio i viene decrementata di un'unit. Proviamo invece a ragionare in modo ricorsivo: Ho una variabile n di cui voglio calcolare il fattoriale: n! = n*(n-1)*(n-2)*...*1 Ma (n-1)! = (n-1)*(n-2)*...*1 -> n! = n*(n-1)! Ma (n-2)! = (n-2)*(n-3)*...*1 -> (n-1)! = (n-1)*(n-2)! E cos via Per calcolare il fattoriale di n posso quindi semplicemente moltiplicare n per il fattoriale di n-1, che a sua volta n-1 moltiplicato per il fattoriale di n-2, e cos via finch non arrivo a 1. Ecco l'implementazione:
int fat(int n) { if (n==1) return 1; else return n*fat(n-1); }
numero di elementi nulli al suo interno. Potremmo anche crearla in modo standard, con un normale ciclo for o con un ciclo while:
/* La funzione countNull accetta come parametri un vettore di interi e la dimensione del vettore stesso, e ritorna il numero di zeri contenuti all'interno del vettore */ int countNull ( int *v, int dim ) int i=0,count=0; {
// Se il vettore non ha elementi, ritorna 0 if (!dim) return 0; else // Finch il vettore ha elementi, controllo se // l'elemento zero. Se s, incremento la variabile // contatore for (i=0; i<dim; i++) if (!v[i]) return count; } count++;
Ecco invece come strutturare la funzione con una ricorsione tail: Se la posizione attuale all'interno del vettore l'ultima, ritorna il numero di zeri contati nel vettore Se alla posizione attuale all'interno del vettore corrisponde uno zero, incrementa la variabile contatore Ritorna la funzione stessa sullo stesso vettore della stessa dimensione ma sull'elemento successivo nel vettore
int countNull(int *v, int dim, int i) if (i==dim) return zero; if (v[i]==0) zero++; return countNull(v,dim,i+1); } {
In questo caso, quando richiamiamo la funzione dobbiamo anche specificare il valore iniziale della variabile i. Poich vogliamo cominciare dall'inizio del vettore, i varr 0.
Liste
.
Una lista un insieme finito e ordinato di elementi di un certo tipo. In informatica una lista si indica come un insieme di termini compresi tra parentesi quadre []. Esempio, ['a','n','c']. Come tutti i tipi di dato astratti, anche le liste sono definite in termini di
Dominio-base dei suoi elementi (interi, caratteri, stringhe...) Operatori di costruzione della lista Operatori di selezione sulla lista
Il grosso vantaggio delle liste sugli array il fatto che una lista si pu definire in modo estremamente dinamico, anche senza conoscere il numero di elementi totale di partenza dei suoi elementi, e di gestire i collegamenti tra un elemento e un altro in modo estremamente versatile. Ma andiamo con ordine.
Dominio base D Insieme di funzioni Insieme di predicati sul dominio D sul dominio D
il costruttore della lista, ovvero la funzione che, dato una lista di partenza e un elemento appartenente al dominio da inserire in cima alla lista, costruisce la lista specificata.
Funzione che ritorna la 'testa' della lista, ovvero il suo primo elemento.
Funzione che ritorna la 'coda' della lista, ovvero una lista uguale a quella di partenza ma privata del primo elemento.
Funzione che ritorna la costante 'lista vuota'. Per convenzione, in C una lista vuota quando il valore della sua testa NULL. L'unico predicato elementare sul tipo astratto di lista cos definito:
Qualche esempio:
cons (5, [3,6,2,3]) crea la lista [5,3,6,2,3] head ([7,3,5,6]) ritorna 7 (testa della lista) tail ([7,3,5,6]) ritorna la lista [3,5,6] (coda della lista) empty ([7,3,5,6]) ritorna falso (la lista non vuota)
Quelle illustrate sono le operazioni di base che si possono effettuare su una lista. Tutte le altre operazioni (inserimento ordinato di elementi, ribaltamento degli elementi, stampa degli elementi presenti...) sono operazioni derivate dalle primitive appena illustrate. Considerando che esiste il concetto di lista vuota (per convenzione la lista avente NULL in testa) e che possibile costruire nuove liste usando il costruttore cons, si possono definire tutte le eventuali funzioni derivate sulla base di quelle gi definite tramite algoritmi ricorsivi.
Rappresentazione statica
La rappresentazione pi ovvia del tipo astratto di lista gestendo gli elementi della lista in un array. La lista cos costruita conterr
Un vettore di lunghezza massima prefissata Una variabile primo, che identifica l'indice del primo elemento della lista Una variabile lunghezza, che indica il numero di elementi contenuti nella lista
L'inconveniente principale il fatto che le dimensioni del vettore sono fisse. Il tipo di dato lista quindi strutturato cos in questo caso:
#define N 100 typedef struct { int primo,lunghezza; int elementi[N]; } list;
// Convenzione: quando la lista vuota l'indice del primo elemento // un numero negativo [Link]=-1; [Link]=0; } // Controlla se la lista vuota bool empty(list l) { return ([Link]==-1); } // Ritorna il primo elemento della lista int head (list l) { if (empty(l)) abort(); return [Link][[Link]]; } // Ritorna la coda della lista list tail(list l) { list t=l; // Se la lista vuota, esce if (empty(l)) abort(); // Altrimenti, la lista t avr come primo elemento // il primo di l incrementato di 1, e la lunghezza // di l decrementata di 1 (ovvero scarto la testa della lista) [Link]++; [Link]--; return t; } // Crea una nuova lista, prendendo come parametri // l'elemento da inserire in testa e una lista di partenza // (eventualmente vuota) list cons (int e, list l) { list t; int i; // Inserisco e in testa alla lista [Link]=0; [Link][[Link]]=e; [Link]=1; // Copio il vettore contenuto in l nella nuova lista for (i=1; i<=[Link]; i++) { [Link][i]=[Link][i-1]; [Link]++; }
Queste sono le funzioni primitive sulla lista. Grazie a queste possibile costruire ricorsivamente eventuali funzioni derivate. Esempio, una funzione che stampi tutti gli elementi della lista:
void showList(list l) {
// Condizione di stop: se la lista vuota, ritorna if (empty(l)) return; // Stampa il primo elemento della lista printf ("%d\n",head(l)); // Richiama la funzione sulla coda di l showList(tail(l)); }
Rappresentazione dinamica
Una rappresentazione di liste estremamente utile quella dinamica. In questo tipo di rappresentazione si perde ogni riferimento statico (vettori, buffer di dimensione fissa). Ogni elemento della lista contiene il suo valore e un riferimento all'elemento successivo nella lista stessa. Si crea quindi cos una lista grafica, con nodi (elementi della lista) e archi (collegamenti tra gli elementi). Un generico elemento della lista sar quindi cos costruito:
// Creo una lista di interi // Nel caso volessi riutilizzare il codice per una lista // di un altro tipo, mi baster modificare il tipo element typedef element int; typedef struct list_element { element value; struct list_element *next; } node;
Il tipo element mi consente di scrivere del codice estremamente modulare, in quanto semplicemente modificando il tipo potr usare la stessa lista per memorizzare interi, float, caratteri e quant'altro. Come possibile notare inoltre nel dichiarare la struttura node ho usato un'etichetta (list_element). Ci indispensabile in quanto all'interno della struttura c' un collegamento a un elemento della struttura stessa (il prossimo elemento della lista). Ma poich node non ancora stato dichiarato a quel punto, indispensabile mettere un'etichetta temporanea. A questo punto, con un nodo della lista cos definito potr includere al suo interno il suo stesso valore e il riferimento al prossimo elemento. Nel caso l'elemento in questione sia l'ultimo della lista, si mette come suo successore, per convenzione, il valore NULL. Per una maggiore genericit del codice possiamo creare funzioni che operano sul tipo element, in modo che se in futuro dovessimo usare lo stesso tipo di lista creato per gestire degli interi per gestire delle stringhe baster cambiare queste funzioni che agiscono su element, e lasciare inalterate le funzioni che operano sulla lista. Si comincia cos a entrare nell'ottica della creazione di codice modulare ovvero codice che possibile scrivere una volta e riusare pi volte. Vediamo le funzioni di base che possono agire sul tipo element (in questo caso tipo int, volendo modificando il tipo baster cambiare le funzioni):
bool isLess (element a, element b) { return (a<b); } bool isEqual (element a, element b) { return (a==b); } element get (element e) { return e; } element readElement() element e; {
appunto come puntatore a un elemento di tipo node. Per il tipo lista le primitive saranno le seguenti:
// Ritorna la costante 'lista vuota' list emptylist() { return NULL; } // Controlla se una lista vuota bool empty(list l) { return (l==NULL); } // Ritorna la testa della lista element head (list l) { if (empty(l)) abort(); return l->value; } // Ritorna la coda della lista list tail (list l) { if (empty(l)) abort(); return l->next; } // Costruttore. Genera una lista dato un elemento // da inserire in testa e una lista list cons (element e, list l) { list t; t = (list) malloc(sizeof(node)); [Link]=get(e); [Link]=l; return t; }
Con queste primitive di base possibile costruire qualsiasi funzione che operi sul tipo di dato 'lista'. Esempio, per la stampa degli elementi contenuti nella lista:
void printList(list l) { // Condizione di stop: lista vuota if (l==NULL) return; printElement(l->head); printf ("\n"); // Scarto l'elemento appena stampato e // richiamo la funzione in modo ricorsivo printList(l->tail);
E allo stesso modo si possono anche definire per la ricerca di un elemento nella lista, per la lettura di un elemento all'indice i della lista e cos via.
accessi al file. In ANSI-C questa variabile di tipo FILE, un'entit definita in stdio.h, e per associarla ad un file ho bisogno di ricorrere alla funzione fopen (sempre definita in stdio.h, come tutte le funzioni che operano su entit di tipo FILE). La funzione fopen cos definita:
FILE* fopen(const char* filename, const char* mode);
dove *filename il nome del nostro file (pu essere sia un percorso relativo che assoluto, ad es. mio_file.txt oppure /home/pippo/mio_file.txt), mentre invece *mode mi indica il modo in cui voglio aprire il mio file. Ecco le modalit possibili:
r Apre un file di testo per la lettura w Crea un file di testo per la scrittura a Aggiunge a un file di testo rb Apre un file binario per la lettura wb Apre un file binario per la scrittura ab Aggiunge a un file binario r+ Apre un file di testo per la lettura\scrittura w+ Crea un file di testo per la lettura\scrittura a+ Aggiunge a un file di testo per la lettura\scrittura r+b Apre un file binario per la lettura\scrittura w+b Crea un file binario per la lettura\scrittura a+b Aggiunge a un file binario per la lettura\scrittura
Quando non possibile aprire un file (es. il file non esiste o non si hanno i permessi necessari per scrivere o leggere al suo interno) la funzione fopen ritorna un puntatore NULL. sempre necessario controllare, quando si usa fopen, che il valore di ritorno non sia NULL, per evitare di compiere poi operazioni di lettura o scrittura su file non valide che rischiano di crashare il programma. Ecco un esempio di utilizzo di fopen per l'apertura di un file in lettura:
#define FILE_NAME ........ FILE *fp; fp = fopen (FILE_NAME,"r"); if (!fp) { printf ("Impossibile aprire il file %s in lettura\n",FILE_NAME); return; } "[Link]"
poi buona norma eliminare il puntatore al file quando non pi necessario. Questo si fa con la funzione fclose, cos definita:
int fclose(FILE *fp);
La funzione fclose ritorna 0 quando la chiusura va a buon fine, -1 negli altri casi (ad esempio, il puntatore che si prova a eliminare non associato ad alcun file).
quelle per scrivere e leggere su file dati binari e quelle per il testo semplice (ASCII). Vediamo prima le funzioni ASCII. Le funzioni ASCII per scrivere e leggere su file non sono altro che specializzazioni delle corrispettive funzioni per leggere e scrivere su stdin/stdout. Abbiamo quindi fprintf, fscanf, fgets e fputs. L'uso di fprintf del tutto analogo a quello di printf, e prende come argomenti un file descriptor (puntatore alla struttura FILE) e una stringa di formato con eventuali argomenti, in modo del tutto analogo a una printf. Esempio:
#define MY_FILE mio_file.txt ..... FILE *fp; fp = fopen (MY_FILE,"w"); if (!fp) } { printf ("Errore: impossibile aprire il file %s in scrittura\n",MY_FILE); return;
// Scrivo su file fprintf (fp,"Questa una prova di scrittura sul file %s\n",MY_FILE);
Analogalmente, si pu usare anche la fputs() per la scrittura di una stringa su file, ricordando che la fputs prende sempre due argomenti (il file descriptor e la stringa da scrivere su file):
#define MY_FILE mio_file.txt ..... FILE *fp; fp = fopen (MY_FILE,"w"); if (!fp) } { printf ("Errore: impossibile aprire il file %s in scrittura\n",MY_FILE); return;
Tramite la fprintf posso scrivere su file anche dati che poi posso andare a rileggere dopo, creando una specie di piccolo 'database di testo'. Esempio:
#include <stdio.h> #include <stdlib.h> #define USER_FILE typedef struct { char user[30]; char pass[30]; "[Link]"
char email[50]; int age; } user; int main(void) FILE *fp; user u; {
if (!(fp=fopen(USER_FILE,"a"))) { printf ("Errore: impossibile aprire il file %s in modalit append\n",USER_FILE); exit(1); } printf ("=> Inseririmento di un nuovo utente <==\n\n"); printf ("Username: "); scanf ("%s",[Link]); printf ("Password: "); scanf ("%s",[Link]); printf ("Email: "); scanf ("%s",[Link]); printf ("Et: "); scanf ("%d",&[Link]); /* Scrivo i dati su file */ fprintf (fp,"%s\t%s\t%s\t%d\n",[Link],[Link],[Link],[Link]); printf ("Dati scritti con successo sul file!\n"); fclose (fp); }
Ovvero una riga per ogni utente, dove ogni campo separato da un carattere di tabulazione.
#define USER_FILE typedef struct { char user[30]; char pass[30]; char email[50]; int age; } user; int main(void) FILE *fp; user u; int i=0; {
"[Link]"
if (!(fp=fopen(USER_FILE,"r"))) { printf ("Errore: impossibile aprire il file %s in modalit read-only\n",USER_FILE); exit(1); } while (fscanf(fp,"%s\t%s\t%s\t%d\n", [Link],[Link],[Link],&[Link])>0) printf ("Username: %s\n",[Link]); printf ("Password: %s\n",[Link]); printf ("Email: %s\n",[Link]); printf ("Et: %d\n\n",[Link]); i++; } printf ("Utenti letti nel file: %d\n",i); fclose (fp); } {
Ci sono modi alternativi per effettuare quest'operazione. Ad esempio, si potrebbero contare gli utenti semplicemente contando il numero di righe nel file, in modo del tutto indipendente dal ciclo di fscanf principale. Si tratta semplicemente di introdurre una funzione del genere:
... int countLines (char *file) FILE *fp; char ch; int count=0; if (!(fp=fopen(file,"r"))) return -1; while (fscanf(fp,"%c",&ch)>0) if (ch=='\n') count++; } return count; {
... i=countLines(USER_FILE);
o ancora usando, invece di ciclare controllando il valore di ritorno di fscanf, si pu ciclare finch non viene raggiunta la fine del file. Per far questo si ricorre in genere alla funzione feof, funzione che controlla se si raggiunta la fine del file puntato dal file descriptor in questione. In caso affermativo, la funzione ritorna un valore diverso da 0, altrimenti ritorna 0
... int countLines (char *file) FILE *fp; char ch; int count=0; if (!(fp=fopen(file,"r"))) return -1; while (!feof(fp)) { if ((ch = getc(fp)) == '\n') count++; } } return count; {
Anche qui, la funzione feof si pone ad un livello di astrazione superiore a quello del sistema operativo. Infatti i sistemi operativi usano strategie differenti per identificare l'EOF (End-of-File). I sistemi Unix e derivati memorizzano a livello di filesystem la dimensione di ogni file, mentre i sistemi DOS e derivati identificano l'EOF con un carattere speciale (spesso identificato dal caratteri ASCII di codice -1). La strategia dei sistemi DOS per si rivela molto pericolosa...infatti, possibile inserire il carattere EOF in qualsiasi punto del file, e non necessariamente alla fine, e il sistema operativo interpreter quella come fine del file, perdendo tutti gli eventuali dati successivi. La funzione feof si erge al di sopra di questi meccanismi di basso livello, rendendo possibile l'identificazione dell'EOF su qualsiasi sistema operativo. Se conosco a priori la dimensione del buffer che devo andare a leggere dal file, preferibile usare la funzione fgets, che ha questa sintassi: char* fgets (char *s, int size, FILE *fp); Ad esempio, ho un file contenente i codici fiscali dei miei utenti. Gi so che ogni codice fiscale lungo 16 caratteri, quindi user la fgets:
#include <stdio.h> #define CF_FILE "[Link]" int main(void ) FILE *fp; char cf[16]; int i=1; {
if (!(fp=fopen(USER_FILE,"r")) ) { printf ("Errore: impossibile aprire il file %s in modalit read-only\n",USER_FILE); exit(1); } while (!feof(fp)) { fgets (cf,sizeof(cf),fp); printf ("Codice fiscale n.%d: %s\n",i++,cf); } }
printf ("Username: "); scanf ("%s",[Link]); printf ("Password: "); scanf ("%s",[Link]); printf ("Email: "); scanf ("%s",[Link]); printf ("Et: "); scanf ("%d",&[Link]); // Scrivo i dati su file if (fwrite (&u, sizeof(u), 1, fp)>0) printf ("Dati scritti con successo sul file!\n"); else printf ("Errore nella scrittura dei dati su file\n"); } fclose (fp);
if (!(fp=fopen(USER_FILE,"r"))) { printf ("Errore: impossibile aprire il file %s in modalit read-only\n",USER_FILE); exit(1); } while (fread(&u,sizeof(u),1,fp)>0) { printf ("Username: %s\n",[Link]); printf ("Password: %s\n",[Link]); printf ("Email: %s\n",[Link]);
SEEK_SET (corrispondente al valore 0), che rappresenta l'inizio del file SEEK_CUR (corrispondente al valore 1), che rappresenta la posizione corrente all'interno del file SEEK_END (corrispondente al valore 2), che rappresenta la fine del file
Ad esempio, se come secondo argomento della funzione passo 3 e come terzo argomento SEEK_CUR, mi sposter avanti di 3 byte a partire dalla posizione attuale all'interno del file. C' poi la funzione ftell: int ftell (FILE *fp); che non fa altro che ritornare la posizione attuale all'interno del file puntato da fp (ovvero il numero di byte a cui si trova il puntatore a partire dall'inizio del file). Esempio pratico: un programmino per la ricerca di una parola all'interno di un file
#include <stdio.h> #include <stdlib.h> #include <string.h> #define MY_FILE "file_to_search.txt" main() { FILE *fp; char s[100]; char *buff; int dim; int i=0; if (!(fp=fopen(MY_FILE,"r"))) { printf ("Errore nella lettura dal file %s\n",MY_FILE); exit(1); }
printf ("Parola da cercare all'interno del file %s:", MY_FILE); scanf ("%s",s); dim=strlen(s); buff = (char*) malloc(dim*sizeof(char)); while (!feof(fp)) { fscanf (fp,"%s",buff); if (!strcmp(s,buff)) { printf ("Parola trovata a %d byte dall'inizio\n", ftell(fp)-dim); i++; } /* Mi posiziono indietro nel file di dim+1 caratteri * a partire dalla posizione corrente */ fseek (fp,-dim+1,SEEK_CUR); } } printf ("%d occorrenze di %s trovate nel file\n",i,s);
Si pu fare una cosa del genere anche nelle nostre applicazioni. Il metodo standard quello di richiamare il main() del nostro programma con due parametri aggiuntivi:
Gli argomenti passati alla nostra applicazione verranno piazzati nell'array di stringhe argv, mentre invece il numero di argomenti passati sar indicato dall'intero argc:
main (int argc, char **argv)
Il primo elemento del vettore di stringhe argv (argv[0]) conterr sempre il nome dell'applicazione in esecuzione, e quindi argc sar sempre almeno uguale a 1 (in quanto argv conterr sempre almeno un valore). Da argv[1] in poi verranno indicati gli argomenti aggiuntivi passati alla nostra applicazione. Esempio pratico:
#include <stdio.h> main (int argc, char **argv) int i; {
printf ("In esecuzione: %s\n",argv[0]); // Stampo tutti gli argomenti passati al programma for (i=1; i<argc; i++) printf ("Argomento n.%d: %s\n",i,argv[i]); }
Libreria math.h
.
Includendo nel proprio codice l'header math.h possibile utilizzare svariate funzioni e costanti matematiche. Ecco le principali:
Funzioni trigonometriche
cos Calcola il coseno di un numero reale (espresso in radianti) sin Calcola il seno di un numero reale (espresso in radianti) tan Calcola la tangente di un numero reale (espresso in radianti) acos Calcola l'arcocoseno di un numero reale asin Calcola l'arcoseno di un numero reale atan Calcola l'arcotangente di un numero reale
Funzioni iperboliche
cosh Calcola il coseno iperbolico di un numero reale sinh Calcola il seno iperbolico di un numero reale tanh Calcola la tangente iperbolica di un numero reale
exp Calcola l'esponenziale di un numero reale log Calcola il logaritmo in base e di un numero reale log10 Calcola il logaritmo in base 10 di un numero reale
Potenze e radici
pow Calcola una potenza. Prende come primo argomento la base e come secondo l'esponente sqrt Calcola la radice quadrata di un numero reale
ceil Approssima per eccesso un numero reale al numero intero pi vicino abs Calcola il valore assoluto di un numero reale floor Approssima per difetto un numero reale al numero intero pi vicino
Costanti
L'header math.h mette anche a disposizione del programmatore alcune costanti matematiche di uso comune con un numero notevole di cifre significative dopo la virgola, senza che ci sia bisogno di
definirle di volta in volta. Tra queste il pi greco (M_PI) e il numero di Nepero e (M_E).
Una volta inizializzato il seme uso la funzione rand() per ottenere un numero pseudocasuale. Tale funzione ritorna per numeri estremamente grandi. Per restringere l'intervallo possibile dei numeri pseudocasuali che voglio generare basta calcolarne uno con rand() e poi calcolarne il modulo della divisione per il numero pi alto dell'intervallo che voglio ottenere. Ad esempio, se voglio ottenere numeri pseudocasuali in un intervallo da 0 a 9 baster
int rnd=rand()%10;
Libreria time.h
.
La libreria time.h dedicata alla gestione della data e dell'ora. Comprende funzioni di tre tipologie: tempi assoluti, rappresentano data e ora nel calendario gregoriano; tempi locali, nel fuso orario specifico; variazioni dei tempi locali, specificano una temporanea modifica dei tempi locali, ad esempio l'introduzione dell'ora legale. Contiene le dichiarazioni della funzione time(), che ritorna l'ora corrente, e la funezione clock() che restituisce la quantit di tempo di CPU impiegata dal proramma.
time_t
Il tipo time_t, definito in time.h, non altro che un long int addibito al compito di memorizzare ora e date misurate in numero di secondi trascorsi dalla mezzanotte del 1 gennaio 1970, ora di Greenwich. Bisogna ammetere che un modo un po' bislacco di misurare il tempo, ma cos perch cos si misurato il tempo sui sistemi Unix. Tuttavia, nonostante possa sembrare un modo strano di rappresentare il tempo, questa rappresentazione estremamente utile per fare confronti tra date che, essendo tutte rappresentate in questo modo, si riducono a semplici confronti tra numeri interi, senza che ci sia bisogno di confrontare giorni, mesi e anni. Ma ci sono anche problemi legati a questa rappresentazione. La rappresentazione del tipo time_t infatti una rappresentazione a 32 bit, che ammette numeri negativi (ovvero numero di secondi prima del 1 gennaio 1970, che consente la rappresentazione di date fino al 1900) e numeri positivi (numero di secondi passati dal 1 gennaio 1970). Il bit pi significativo del numero binario identifica il segno (0 per i numeri positivi, 1 per quelli negativi). In questo modo possibile rappresentare fino a numeri positivi, ovvero numero di secondi dopo la data per eccellenza, e questo un problema perch con una tale rappresentazione la data andr in overflow intorno al 2038 (ovvero, in un certo momento dopo le prime ore del 2038 si arriver ad un punto in cui la cifra pi significativa del numero andr a 1, quindi le date cominceranno a essere contate dal 1900). Il bug del 2038 molto conosciuto in ambiente Unix, e per porre rimedio si sta da tempo pensando di migrare ad una rappresentazione della data a 64 bit.
struct tm
tm una struttura dichiarata sempre in time.h, contiene informazioni circa l'ora e la data, questo il conenuto:
struct tm { int tm_sec int tm_min int tm_hour int tm_mday int tm_mon int tm_year int tm_wday int tm_yday int tm_isdst //secondi prima del completamento del minuto //minuti prima del completamento dell'ora //ore dalla mezzanotte //giorno del mese //mesi passati da gennaio //anni passati dal 1900 //giorni passati da Domenica //giorni passati dal 1 Gennaio //''unknow'' (lol)
};
Esempio
Ecco un piccolo programma che ci mostra a schermo ora e data.
#include <stdio.h> #include <time.h> int main(int argc, char *argv[]) { time_t a; struct tm *b; time(&a); b = localtime(&a); printf("Ora esatta: %s\n", asctime(b)); return 0; }
Un altro modo per visualizzare la data attuale senza ricorrere a un membro della struttura tm il seguente:
#include <stdio.h> #include <time.h> main() { time_t ltime = time(); printf ("%s\n",ctime(<ime)); }
La funzione ctime prende l'indirizzo di una variabile di tipo time_t inizializzata tramite time e stampa il suo valore in formato ASCII. Il formato standard
Giorno della settimana (3 lettere) Mese (3 lettere) Giorno del mese (2 cifre) hh:mm:ss Anno (4 cifre)
Un modo per stampare il tempo in un altro formato diverso da quello previsto da ctime e asctime quello di usare la funzione strftime, che prende come parametri
Una stringa nella quale salvare la data nel formato che si scelto La dimensione della stringa Una stringa di formato (simile a printf) nel quale si specifica il formato in cui stampare la data Un puntatore a struttura tm
Esempio:
#include <stdio.h> #include <time.h> main() { time_t timer=time(); struct tm *now=localtime(&timer); char timebuf[20];
La funzione localtime prende come parametro un puntatore a variabile time_t e ritorna una struttura tm corrispondente a quel tempo.
Il file descriptor, a differenza del file pointer definito in stdio.h che altro non che un puntatore alla struttura FILE, un numero intero che identifica in modo univoco il file aperto all'interno della tabella dei files aperti del sistema operativo. In questa tabella i primi 3 numeri (0,1,2) sono riservati ai cosiddetti descrittori speciali:
Su un sistema Unix posso quindi scrivere su stdout o stderr e leggere dati da stdin come se fossero normali file, quindi usando le stesse primitive (everything is a file!, un motto comune tra i sistemisti Unix). Se apro un altro file sul mio sistema Unix tale file assumer quindi un identificatore pari a 3 nella tabella dei file aperti, se ne apro un altro ancora avr un identificatore 4 e cos via.
open
La funzione di apertura si chiama open, ecco un esempio:
#include <fcntl.h> #include <sys/types.h> #include <sys/stat.h> main() {
Questa funzione associa fd (file descriptor) a nomefile e lo apre in modalit di sola scrittura. La open ritorna un valore intero, che negativo nel caso in cui si verificato un errore, ad esempio il file non esiste o non si hanno i diritti di lettura/scrittura. La sintassi della funzione questa:
int fd, modo, diritti; ... fd = open("nomefile", modo [diritti]);
Modalit di apertura
modo rappresenta la modalit di apertura del file, pu essere una o pi delle seguenti costanti simboliche (definite in fcntl.h):
O_RDONLY apre il file in sola lettura O_WRONLY apre il file in sola scrittura O_RDWR apre il file in lettura e scrittura O_CREAT crea il file O_TRUNC distrugge il contenuto del file O_APPEND tutte le scritture vengono eseguite alla fine del file O_EXCL Se al momento dell'apertura il file gi esiste, la open ritorna errore
(Per queste tre costanti simboliche, se il file non esiste la open ritorna errore)
Per poter specificare pi di una modalit di apertura si pu usare l'operatore di OR bit a bit, esempio:
fd = open("nomefile", O_CREAT | O_WRONLY, 0640);
Permessi
Ora vi starete chiedendo cos' quel 0640, sono i diritti, o permessi, con i quali il file deve essere creato. Sono codificati con una sintassi simile a quella di Unix, che suddivide gli utenti in tre categorie:
possessore del file; appartiene al gruppo collegato al file; non collegato al file in alcun modo.
Per ogni categoria si possono specificare i permessi tramite la forma ottale, costituita da 3 o 4 cifre comprese tra 0 e 7. Esempio:
0640
Tralasciamo il significato della prima cifra a sinistra, che opzionale. Ogni cifra da interpretare come una somma delle prime tre potenze di 2 (2^0=1, 2^1=2, 2^2=4), ognuna delle quali corrisponde ad un permesso - andando da sinistra verso destra, la seconda rappresenta il proprietario, la terza il grupp e l'ultima tutti gli altri utenti; la corrispondenza questa:
Dunque per ottenere un permesso di lettura e scrittura non occorre far altro che sommare il permesso di lettura a quello di scrittura (4+2=6) e cos via. Un altro modo di vedere i permessi Unix di un file tramite la rappresentazione binaria. I permessi Unix visti sopra non sono altro che una rappresentazione in modo ottale di un numero binario che se visto fa capire al volo quali sono i permessi su un particolare file. Ecco come funziona:
U G O rwx rwx rwx 110 100 000
In questo caso l'utente (U) ha permessi di lettura e scrittura sul file. Il gruppo (G) ha solo i permessi di lettura. Gli altri utenti non hanno alcun permesso. Se convertiamo ogni gruppetto di 3 cifre in ottale otteniamo 0640, che effettivamente il permesso che vogliamo.
close
La funzione close serve a chiudere un file descriptor aperto dalla open:
int fd; ... close(fd);
read e write
Le operazioni di lettura e scrittura sul file, utlizzando i file descriptor, si possono effettuare usando le primitive read e write. Esempio di utlizzo di read:
char buf[100]; int dimensione; int fd; int n; ... dimensione = 100; n = read(fd, buf, dimensione);
fd rappresenta il file descriptor da dove si desidera leggere, buf il vettore che conterr i dati letti e dimensione la dimensione in byte del vettore. Il valore di ritorno indica il numero di byte letti da fd; questo valore pu essere inferiore al valore di buf, succede quando il puntatore raggiunge la fine del file; un valore di ritorno uguale a 0 indica la fine del file, mentre invece un valore minore di 0 indica un errore in lettura. Esempio di utilizzo di write:
char buf[100]; int dimensione = 100;
fd il file descriptor da dove si legge, buf il vettore che contiene i dati da scrivere e dimensione la dimensione in byte dei dati da scrivere. Il valore di ritorno della write indica il numero di byte scritti sul file; questo valore pu essere inferiore alla dimensione nel caso in cui il file abbia superato la massima dimensione ammessa, o inferiore di 0 in caso di errore.
Esempio pratico
Ecco un possibile esempio di lettura tramite le primitive appena viste dei contenuti di un file passato via riga di comando alla nostra applicazione:
#include #include #include #include #include <stdio.h> <stdlib.h> <unistd.h> <fcntl.h> <sys/stat.h> {
// Controllo se al programma stato passato almeno un argomento if (argc==1) { printf ("Uso: %s <file>\n",argv[0]); exit(1); } // Provo ad aprire il file passato if ((fd=open(argv[1],O_RDONLY))<0) { printf ("Errore nell'apertura di %s\n",argv[1]); exit(2); } // Finch ci sono caratteri da leggere li leggo tramite read... while (read (fd,buff,sizeof(buff))>0) // ...e li scrivo su stdout tramite write // Notate che per la scrittura posso anche usare la write // usando come file descriptor 1, che identifica lo stdout write (1,buff,sizeof(buff)); // Chiudo il file close (fd); }
lseek
Come per i file pointer, esiste una funzione che consente di muovere il puntatore al file, per i file descriptor si chiama lseek (per i file pointer era fseek). Esempio:
long offset; long n; int start;
dove, come sempre, fd il file descriptor sul quale si muover il puntatore; n il numero di byte che copre lo spostamento (se negativo lo spolstamento avvine all'indietro anzich in avanti); mode invece indica la posizione da quale iniziare a muovere il puntatore: se vale 0 ci sid eve muovere dall'inizio del file, se vale 1 dalla posizione corrente, mentre se vale 2 a partire dalla fine del file. Il valore di ritorno di lseek contiene la posizione corrente del puntatore (dopo lo spostemento ovviamente). Quindi:
lseek(fd, 0L, 1) restituisce la posizione corrente lseek(fd, 0L, 2) restituisce la dimensione del file in byte
Redirezione
La redirezione qualcosa che ad alto livello, dalla nostra shell Unix, si traduce in qualcosa del tipo
./nome_eseguibile > mio_file.txt
Ovvero non stampo l'output dell'eseguibile su stdout o stderr, come sarebbe previsto, ma lo re-direziono su un secondo file, magari un file di log. Come si traduce questa caratteristica a basso livello? Semplice. Abbiamo visto che stdin, stdout e stderr sono visti a basso livello come dei semplici file con dei descrittori speciali (rispettivamente 0, 1 e 2). Posso chiudere, ad esempio, il descrittore 1 (stdout) e fare in modo che venga sostituito da un descrittore arbitrario, che in questo caso sar il descrittore al nostro file di log. Per chiudere il descrittore user la primitiva gi vista close(), mentre invece per fare in modo che il descrittore del mio file di log sovrascriva il descrittore di stdout user la primitiva dup() (duplicate), che prende come unico argomento il descrittore del mio file e lo copia sul primo descrittore disponibile. In questo caso il primo descrittore disponibile quello di stdout, lasciato vuoto dalla chiusura di 1, e quindi qualsiasi testo che indirizzato verso stdout verr re-indirizzato verso il mio file arbitrario. Esempio pratico:
#define MSG "Hello\n" main() { write (1,MSG,sizeof(MSG)); }
Questo codice effettuer come prevedibile la stampa di un semplice messaggio su stdout. Effettuando la redirezione di stdout su un file di log arbitrario diventa:
#define #define MSG ERR "Hello\n" "Impossibile aprire il file di log\n"
main() { char *log="[Link]"; int fd; if ((fd=open(log,O_WRONLY)<0) write (2,ERR,sizeof(ERR)); exit(-1); } // Chiudo stdout close(1); {
// Duplico il mio descrittore del log // che andr a sovrascrivere stdout dup(fd); // A questo punto tutto ci che doveva finire // su stdout verr re-direzionato sul mio log write (1,MSG,sizeof(MSG)); close(fd); }
Una stringa che identifica l'attuale nome del file Una stringa che identifica il nuovo nome da assegnare
Per la cancellazione la primitiva unlink(), che prende come unico argomento una stringa contenente il nome del file da cancelare.
Un buffer nel quale salvare il nome della directory corrente La sua dimensione
Esempio:
#include <stdio.h> #include <unistd.h> #include <dirent.h> main(int argc, char **argv) char dir[MAXNAMLEN]; char *new_dir; {
if (argc==1) { printf ("Uso: %s <dir>\n",argv[0]); exit(-1); } new_dir=argv[1]; // Cambio la directory corrente if (chdir(new_dir)<0) { printf ("Errore - impossibile spostarsi in %s\n",new_dir); exit(-2);
} // Ottengo il nome della directory attuale // e lo salvo in dir getcwd (dir,sizeof(dir)); printf ("Directory attuale: %s\n",dir); }
Questo codice semplicemente prende una directory come argomento da riga di comando e prova a spostarsi in quella directory tramite chdir(), uscendo in caso di errore. In caso di successo invece salva il percorso della directory corrente in un buffer di dimensione MAXNAMLEN (costante definita in dirent.h che identifica la dimensione massima che pu assumere il nome di una directory) e lo stampa su stdout. In sostanza questo listato fa qualcosa di simile al comando cd. All'interno del file dirent.h sono anche definite primitive per la lettura dei file contenuti all'interno di una directory. Ci che ci serve un puntatore a directory di tipo DIR (non molto diverso in sostanza dal puntatore a file di tipo FILE definito in stdio.h che abbiamo visto in precedenza) e un puntatore a una struttura di tipo dirent che conterr le informazioni sulla directory. Il campo che ci interessa maggiormente in questo caso della struttura d_name, che conterr di volta in volta il nome di un file contenuto all'interno della directory. Per l'apertura e la chiusura di un puntatore di tipo DIR useremo le primitive opendir() e closedir(), le cui sintassi non sono molto diverse da quelle di una fopen o di una fclose:
#include <dirent.h> ...... DIR *dir; struct dirent *info; if (!(dir=opendir("nome_dir"))) // Errore ...... closedir(dir);
Cos come fopen, opendir ritorna NULL nel caso in cui non riesca ad aprire la directory passata come argomento. Per scannerizzare uno per uno gli elementi della directory si usa la primitiva readdir(), che legge le informazioni di tutti i file contenuti nella directory, uno dopo l'altro, e le salva in un puntatore a struttura dirent. Quando la lettura terminata readdir ritorna NULL, e si pu prendere questa come condizione di stop. A questo punto, con queste nozioni possiamo scrivere un rudimentale programma che si comporta come il comando ls in C, prendendo come parametro da riga di comando il nome della directory di cui si vuole visualizzare il contenuto:
#include <stdio.h> #include <dirent.h> main (int argc, char **argv) DIR *dir; struct dirent *info; {
if (argc==1) { printf ("Uso: %s <dir>\n",argv[0]); exit(-1); } // Apro il descrittore della directory if (!(dir=opendir(argv[1]))) { printf ("Impossibile aprire la directory %s\n",argv[1]); exit(-2); } // Finch ci sono file all'interno della directory... while (info=readdir(dir)) // ...stampa su stdout il loro nome printf ("%s\n",info->d_name); // Chiudi la directory closedir(dir);
Indirizzi IP e endianness
L'indirizzo IP identifica univocamente una macchina all'interno di una rete, e consiste (almeno nella versione 4 del protocollo, versione universalmente usata da anni in tutte le reti) in 4 gruppi di numeri che possono andare da 0 a 255 (in esadecimale 0,...,FF). Un indirizzo IP occupa quindi complessivamente 32 bit (4 byte) in memoria. Per poter utilizzare indirizzi IP in un'applicazione in C necessario passare la stringa che corrisponde all'IP alla funzione inet_addr, definita in <arpa/inet.h>. Esempio:
#include <sys/types.h> #include <arpa/inet.h>
..... in_addr_t addr; struct in_addr a; // Nella mia applicazione, la variabile addr sar associata all'IP di localhost addr = inet_addr([Link]) // Associo al membro s_addr della struttura in_addr la variabile appena associata all'indirizzo a.s_addr=addr; printf (Indirizzo IP associato a 0x%x: %s\n, addr, inet_ntoa(a));
In <netinet/in.h> definita la costante INADDR_ANY, che identifica un qualsiasi indirizzo IP (usato nel codice dei server per specificare che l'applicazione pu accettare connessioni da qualsiasi indirizzo). Attenzione, avrete notato l'uso di un membro della struttura in_addr. Tale struttura (relativamente scomoda e in s per s poco utile, ma preservata nella gestione degli indirizzi in C per una compatibilit con il passato) deputata a contenere indirizzi di rete, ed cos definita:
struct in_addr u_long } { s_addr;
s_addr conterr l'indirizzo ottenuto con inet_addr. Noterete poi l'uso della funzione inet_ntoa (Network to ASCII), che vuole come parametro un dato di tipo in_addr. Tale funzione necessaria per ottenere una stringa ASCII standard a partire da un indirizzo per un motivo particolare, legato alle convenzioni del protocollo TCP/IP. In tale protocollo, infatti, si usa una convenzione di tipo big endian (ovvero le variabili pi grandi di un byte si rappresentano a partire dal byte pi significativo). Tale convenzione era in uso anche su altre macchine, come i processori Motorola e i VAX, ma la maggioranza delle macchine odierne usa lo standard little endian (prima i byte meno significativi e poi a salire quelli pi significativi) per rappresentare le informazioni in memoria o nella CPU. Per leggere sulla mia macchina un informazione passata in formato di rete e viceversa devo quindi fare ricorso a funzioni in grado di passare da una convenzione all'altra. La funzione duale di inet_ntoa sar ovviamente inet_aton, che converte una stringa in formato host che rappresenta un IP (quindi con numeri e punti) in formato binario di rete, per poi salvarla all'interno di una struttura in_addr passata come parametro alla funzione:
int inet_aton(const char *cp, struct in_addr *inp);
Esistono anche funzioni per operare conversioni su tipi di dato diversi dalle stringhe, quali htonl (da codifica Host Byte Order a codifica Network Byte Order, Long), htons (da codifica Host a codifica Network, Short), ntohl (da codifica Network a codifica Host, Long) e ntohs (da codifica Network a codifica Host, Long).
Porte
Per poter effettuare una connessione non basta un indirizzo IP e il protocollo da usare, necessario anche specificare la porta dell'host alla quale si desidera collegare il socket, ovvero il servizio da
richiedere. In definitiva, quindi, per costruire un socket per la comunicazione tra un client e un server ho bisogno di
Protocollo per la comunicazione (TCP, UDP) Indirizzo IP di destinazione Porta su cui effettuare il collegamento
Per poter utilizzare un socket in un programma ho bisogno di far ricorso alle strutture sockaddr, definite in <sys/socket.h>. La struttura sockaddr di riferimento strutturata in questo modo:
struct sockaddr { // Famiglia del socket short sa_family; // Informazioni sul socket char sa_data[];
Nel nostro caso, in cui useremo dei socket per la comunicazione di applicazioni via internet, useremo la struttura sockaddr_in, convertendola in sockaddr, quando richiesto, tramite operatori di cast:
struct sockaddr_in { // Flag che identifica la famiglia del socket, // in questo caso AF_INET short sa_family; // Porta short sin_port; // Indirizzo IP, memorizzato in una struttura // di tipo in_addr struct in_addr sin_addr; // Riempimento di zeri char sin_zero[8];
Ci sono caratteristiche in questa struttura quantomeno curiose e apparentemente obsolete e ridondanti, ma conservate per tradizione e per compatibilit con il passato. In primis il riferimento alla struttura in_addr (vista prima) per memorizzare l'indirizzo IP, quando si poteva tranquillamente ricorrere ad una variabile long. Il riferimento a questa struttura all'interno di sockaddr uno dei pi profondi misteri della tradizione Unix. In secundis, il riempimento della struttura con una stringa (sin_zero) che non fa altro che contenere degli zeri, o comunque caratteri spazzatura. Ci necessario per rendere la dimensione della struttura pari esattamente a 16 byte, in modo da poter effettuare senza problemi il cast da sockaddr a sockaddr_in e viceversa (in quanto sono della stessa dimensione).
Inizializzazione dell'indirizzo
Per inizializzare l'indirizzo all'interno della nostra applicazione dovremo quindi far ricorso ad un membro della struttura sockaddr_in, specificando al suo interno famiglia protocollare (AF_INET), porta e indirizzo IP. Per fare ci conviene creare una procedura esterna al main che faccia il tutto:
void addr_init (struct sockaddr_in *addr, int port, long int ip) // Inizializzazione del tipo di indirizzo (internet) addr->sin_family=AF_INET; {
// Inizializzazione della porta (da host byte order // a network byte order addr->sin_port = htons ((u_short) port); // Inizializzazione dell'indirizzo (passando per la // struttura in_addr addr->sin_addr.s_addr=ip;
Per la chiusura del socket ricorreremo invece alla primiva close, passandogli come parametro il descrittore del nostro socket. A questo punto possibile connettersi all'host sfruttando il socket appena creato, usando la primitiva connect. Tale primitiva richiede come parametri
Il descrittore del socket da utilizzare Un puntatore a sockaddr, contenente le informazioni circa dominio del socket, indirizzo IP di destinazione e porta (l'abbiamo creato in precedenza) La dimensione del puntatore a sockaddr
La funzione, in modo analogo a socket, ritorna -1 nel caso la connessione non sia andata a buon fine. Esempio pratico per il nostro caso:
if (connect(sd, (struct sockaddr*) &server, sizeof(struct sockaddr))<0) { printf ("Impossibile collegarsi al server %s sulla porta %d\n", inet_ntoa(server.sin_addr.s_addr),PORT); exit(4); } else { printf (Connessione effettuata con successo al server %s sulla porta %d\n, inet_ntoa(server.sin_addr.s_addr),PORT); }
In questo caso richiesto l'operatore di cast esplicito, in quanto in precedenza abbiamo creato una variabile di tipo sockaddr_in ma la funzione richiede una variabile di tipo sockaddr.
Il socket da sfruttare (da cui leggere o su cui scrivere) Un puntatore ai dati interessati (una variabile su cui salvare i dati letti o la variabile da scrivere su socket) La dimensione dei dati (da leggere o da scrivere) Un eventuale flag (si lascia a 0 nella maggior parte dei casi)
Entrambe le funzioni ritornano il numero di byte letti o scritti, quindi si possono fare dei cicli con queste funzioni del tipo finch ci sono dati da leggere o scrivere su socket, fai una certa cosa sfruttando il fatto che quando non ci sono pi dati le funzioni ritornano zero.
Lato server
Per inizializzare una comunicazione di rete su un client basta questa procedura:
addr_init (inizializzazione della variabile di tipo sockaddr_in che identifica l'indirizzo e la porta) socket (creazione del socket per la comunicazione con il server) connect (connessione al server sfruttando il socket appena creato)
Su un server sono necessari un paio di passaggi in pi. La procedura in genere questa (non solo in C ma per qualsiasi linguaggio di programmazione):
addr_init (inizializzazione della variabile di tipo sockaddr_in) socket (creazione del socket) bind (creazione del legame tra il socket appena creato e la variabile sockaddr_in che identifica l'indirizzo del server) listen (mette il server in ascolto per eventuali richieste da parte dei client) accept (accettazione della connessione da parte di un client)
dove sockfd l'identificatore del socket, *my_addr il puntatore alla variabile di tipo sockaddr che identifica l'indirizzo e addrlen la lunghezza di tale variabile. La funzione ritorna 0 in caso di successo, -1 in caso di errore. La sintassi di listen invece la seguente:
int listen(int sockfd, int backlog);
dove sockfd il descrittore del socket e backlog il numero massimo di connessioni che il server pu accettare contemporaneamente. Anche questa funzione ritorna 0 in caso di successo e -1 in caso di errore. La sintassi di accept infine la seguente:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
dove sockfd il descrittore del socket, *addr il puntatore alla variabile di tipo sockaddr e *addrlen il puntatore alla sua lunghezza. In caso di successo accept ritorna un valore >0 che il descrittore del socket accettato, mentre ritorna -1 in caso di errore. Attenzione: la accept ritorna un nuovo identificatore di socket, che il socket da utilizzare da quel momento in poi per le comunicazioni con il client. Inoltre, alla accept va passato il puntatore alla variabile sockaddr che identifica il client, non quello del server.
Esempio pratico
Bando alle ciance, vediamo ora un semplice codice in C per l'invio di messaggi sulla rete sfruttando i socket TCP che abbiamo appena esaminato. Il server rimane in attesa di messaggi sulla porta 3666 e quando arrivano li scrive su stdout, mentre il client si collega al server (il cui indirizzo passato come parametro da riga di comando) e gli invia un messaggio, passato anch'esso come una lista di parametri da riga di comando. Codice del client:
#include #include #include #include #include #include #include #include #include <stdio.h> <stdlib.h> <string.h> <unistd.h> <netinet/in.h> <sys/types.h> <sys/wait.h> <sys/socket.h> <errno.h>
// Porta per la comunicazione #define PORT 3666 // Inizializzazione della variabile sockaddr_in void addr_init (struct sockaddr_in *addr, int port, long int ip) addr->sin_family=AF_INET; addr->sin_port = htons ((u_short) port); addr->sin_addr.s_addr=ip; {
} main(int argc, char **argv) { int i,sd; int var1,var2,var3,var4; int sock_size=sizeof(struct sockaddr_in); int N,status; pid_t pid; struct sockaddr_in server,client; // Controllo che vengano passati almeno due argomenti if (argc<3) { printf ("%s <server> <msg>\n",argv[0]); exit(1); } // Controllo che l'IP del server passato sia un indirizzo IPv4 valido if (sscanf(argv[1],"%d.%d.%d.%d",&var1,&var2,&var3,&var4) != 4) printf ("%s non un indirizzo IPv4 valido\n",argv[1]); exit(2); } // Inizializzazione dell'indirizzo addr_init (&server,PORT,inet_addr(argv[1])); // Creazione del socket if ((sd=socket(AF_INET,SOCK_STREAM,0))<0) { printf ("Impossibile creare un socket TCP/IP\n"); exit(3); } // Creazione della connessione if (connect(sd, (struct sockaddr*) &server, sock_size)<0) { printf ("Impossibile collegarsi al server %s sulla porta %d: errore %d\n", inet_ntoa(server.sin_addr.s_addr),PORT,errno ); exit(4); } printf ("Connessione stabilita con successo con il server %s sulla porta %d\n", inet_ntoa(server.sin_addr.s_addr), ntohs(server.sin_port)); // Il numero di parole contenute nel messaggio pari ad argc-2, // ovvero argc-(nome del programma)-(IP del server) N=argc-2; // Dico al server che sto per inviargli N stringhe send (sd, (int*) &N, sizeof(int), 0); // Per i che va da i ad argc... for (i=2; i<argc; i++) { // ...N la lunghezza dell'i-esima stringa N=strlen(argv[i]); {
// Dico al server che sto per inviargli una stringa lunga N caratteri send (sd,(int*)&N,sizeof(int),0);
// Invio al server la stringa send (sd,argv[i],N,0); printf ("Stringa %s lunga %d caratteri inviata con successo al server %s\n", argv[i],N,inet_ntoa(server.sin_addr.s_addr)) ; } // Chiusura della connessione close(sd); exit(0); }
// Porta su cui mettersi in ascolto #define PORT 3666 // Numero massimo di connessioni accettabile #define MAXCONN 5 void addr_init (struct sockaddr_in *addr, int port, long int ip) addr->sin_family=AF_INET; addr->sin_port = htons ((u_short) port); addr->sin_addr.s_addr=ip; } main() { int sd,new_sd; struct sockaddr_in server,client; int sock_size=sizeof(struct sockaddr_in); int pid,status; int i,args,N; char *buff; {
// Inizializzazione dell'indirizzo // Con INADDR_ANY specifico che posso accettare connessioni da qualsiasi indirizzo addr_init (&server,PORT,INADDR_ANY); // Creazione del socket if ((sd=socket(AF_INET,SOCK_STREAM,0)) < 0) { printf ("Impossibile inizializzare il socket TCP/IP %d\n", getsockname (sd, (struct sockaddr*) &server,
&sock_size)); } exit(1);
// Lego il socket appena creato all'indirizzo del server if (bind(sd, (struct sockaddr*) &server, sizeof(server))<0) { printf ("Impossibile aprire una connessione sulla porta %d\n" un'altra applicazione\n",PORT); exit(2); } printf ("Server in ascolto sulla porta %d\n",PORT); // Metto il server in ascolto if (listen(sd,MAXCONN)<0) { printf ("Impossibile accettare nuove connessioni sul socket creato\n"); exit(3); } printf ("Server in ascolto - accetta fino a un massimo di %d connessioni\n",MAXCONN); // Accetto connessioni finch ce ne sono while (1) { // Accetto le connessioni da parte del client creando un nuovo { if ((new_sd=accept(sd, (struct sockaddr*) &client, &sock_size)) printf ("Impossibile accettare una connessione dal client inet_ntoa(client.sin_addr.s_addr)); exit(4); } printf ("Connessione stabilita con successo con il client %s sulla porta %d\n", inet_ntoa(client.sin_addr.s_addr), ntohs (client.sin_port) ); // Ricevo il numero di messaggi che il client ha da inviare recv (new_sd, (int*) &args, sizeof(int), 0); printf ("Stringa ricevuta da %s: ", inet_ntoa(client.sin_addr.s_addr)); // Finch il client ha stringhe da inviare... for (i=0; i<args; i++) { // ...leggo la dimensione dell'i-esima stringa recv (new_sd, (int*) &N, sizeof(int), 0); // Alloco memoria per ricevere la stringa buff = (char*) malloc(N*sizeof(char)); // Ricevo la stringa recv (new_sd,buff,N,0); "La porta potrebbe essere gi in uso da
// Scrivo su stdout la stringa appena ricevuta printf ("%s ",buff); } } } printf ("\n");
Algoritmi di scheduling
Nel corso degli anni gli algoritmi di scheduling si sono sempre pi evoluti, cercando di evitare da una parte problemi quali l'occupazione prolungata della CPU da parte di un solo processo e, dal lato opposto, problemi di starvation (ovvero il congelamento di un processo che, a causa di politiche errate nell'algoritmo di scheduling, quali un'errata gestione della priorit dei processi, non acquister mai una priorit sufficiente per essere eseguito, rimanendo per sempre in attesa). L'algoritmo di scheduling pi elementare quello round-robin, un algoritmo che suddivide il tempo della CPU in quanti uguali. Esempio: ho 3 processi in esecuzione, con i seguenti tempi:
task1 = 250ms task2 = 150ms task3 = 200ms
con una politica round-robin che, mettiamo, suddivide il tempo di utilizzo della CPU in 50ms per ogni task, avr: 1.task1 occupa la CPU per 50 ms (ovvero, passa dallo status READY in cui si trova prima di andare in esecuzione allo status RUNNING per 50ms, per poi essere fermato dal sistema operativo, passando in status SLEEPING e, dopo un certo intervallo, nuovamente in status READY) 2.task2 occupa la CPU
per 50 ms 3.task3 occupa la CPU per 50 ms 4.task1 occupa la CPU per 50 ms 5.task2 occupa la CPU per 50 ms 6.task3 occupa la CPU per 50 ms 7.task1 occupa la CPU per 50 ms 8.task2 occupa la CPU per 50 ms (a questo punto il codice da eseguire all'interno di task2 terminato) 9.task3 occupa la CPU per 50 ms 10.task1 occupa la CPU per 50 ms 11.task3 occupa la CPU per 50 ms (a questo punto il codice da eseguire all'interno di task3 terminato) 12.task1 occupa la CPU per 50 ms (a questo punto anche il codice da eseguire all'interno di task3 terminato) Quest'algoritmo semplice da implementare a livello di kernel ed evita problemi di starvation, in quanto non ha una politica predefinita per le priorit di un task. Tuttavia, attraverso quest'algoritmo pi il task grande (ovvero maggiore il suo tempo di esecuzione), pi viene premiato, in quanto pu occupare la CPU per un periodo cumulativo di tempo maggiore dei task pi piccoli. Gli algoritmi round-robin, nelle varianti weighted round-robin e deficit round-robin, vengono anche utilizzati per lo scheduling dei pacchetti provenienti da connessioni multiple. Ad esempio, se il sistema riceve dei pacchetti da n fonti f1,f2,...,fn, possibile attraverso algoritmi di questo tipo stabilire per quanto tempo ogni fonte autorizzata a inviare pacchetti al sistema. L'altra grande classe di algoritmi di scheduling, ideata per evitare i problemi degli algoritmi RR, sono gli algoritmi a priorit, ideati per evitare che i task che richiedono un tempo di esecuzione maggiore vengano maggiormente premiati, come negli algoritmi RR. Gli algoritmi di questo tipo si dividono a loro volta in Algoritmi a priorit statica. In questi algoritmi la priorit di un task viene stabilita all'atto della sua creazione, in base alle sue caratteristiche Algoritmi a priorit dinamica. In questi algoritmi la priorit di un task pu variare durante l'esecuzione. Questo utile per i seguenti motivi: Per penalizzare i task che impegnano troppo la CPU Per evitare problemi di starvation (ovvero per evitare che nella coda di esecuzione dei processi non riescano mai ad andare in esecuzione) Per aumentare la priorit di un processo in base al suo tempo di attesa nella coda
Programmazione multiprocesso
Un sistema Unix fortemente improntato sulla programmazione multiprocesso. In particolare, quando un sistema Unix viene avviato viene anche generato un processo, chiamato init, con la priorit massima. Questo processo alla base di tutti i processi che vengono successivamente generati all'interno del sistema. Le shell altro non sono che processi figli del processo init, la procedura di autenticazione attraverso username e password a sua volta gestita da altri due processi, generalmente generati dalla shell stessa, i processi login e getty. E ancora, ogni eseguibile avviato nel sistema non fa altro che generare un nuovo processo all'interno della shell che lo ha richiamato, e a sua volta l'eseguibile stesso pu generare altri processi (vedremo presto come farlo). Un processo eredita dal processo che lo ha richiamato l'area dati (ovvero le variabili presenti all'interno dello stack dell'eseguibile prima che venisse generato il processo figlio). Ma, una volta che viene mandato in esecuzione, ha una propria area dati (questo vuol dire che le modifiche attuate all'interno della propria area dati non modificano i dati all'interno del processo padre) e, ovviamente, una propria area di codice, che contiene il codice che il processo deve eseguire. Sui sistemi Unix per generare un nuovo processo si utilizza la primitiva fork(). Questa primitiva fa le operazioni appena descritte sopra, ovvero genera un nuovo processo, con un proprio PID (Process ID, ovvero un numero che identifica il processo) e copia all'interno della sua area dati l'area dati del processo padre. La primitiva fork() ritorna
0 nel caso del processo figlio (quindi, se il risultato della fork() 0 so che l ci devo andare a scrivere il codice che verr eseguito dal processo figlio)
un valore > 0 nel caso del processo padre -1 se c' stato un errore (ad esempio, se la tabella dei processi piena, se non ho abbastanza spazio in memoria per allocare un nuovo processo, se non ho i diritti per creare nuovi processi)
<stdio.h> <stdlib.h> <unistd.h> <sys/wait.h>
int main(int argc, char *argv[]) { int pid; int status; printf ("Sono il processo padre, il mio PID %d\n", getpid()); /* Genero un nuovo processo */ pid = fork(); if ( pid == -1 ) { /* ERRORE! Non stato possibile creare il nuovo processo */ printf ("Impossibile creare un nuovo processo\n"); exit(1); } if ( pid == 0 ) { /* In questo caso la fork() ha ritornato 0, quindi qui ci scrivo il codice del figlio */ printf ("Sono il processo figlio di %d, il mio PID %d\n", getppid(), getpid()); exit(0); } if ( pid > 0 ) { /* In questo caso la fork() ha ritornato un valore maggiore di 0, quindi qui scrivo il codice del processo padre */ printf ("Sono il processo padre e ho generato un processo figlio\n"); /* Attendo che il processo figlio venga terminato, e salvo il suo valore di ritorno nella variabile status */ while ( (pid = wait(&status)) > 0 ); /* Dal valore di status ricavo il valore di ritorno del processo figlio */ status = (status & 0xFF) >> 8; } } printf ("Il processo %d terminato con status %d\n", pid, status);
return 0;
Un paio di commenti. Innanzitutto, un processo pu conoscere in ogni momento il suo PID e il PID del processo che lo ha generato rispettivamente attraverso le primitive getpid() e getppid() (GET Parent PID). Lo studio dei valori di ritorno della primitiva fork() gi stato fatto precedentemente e commentato nel codice, quindi non sto qui a discuterlo nuovamente. invece interessante l'uso della
primitiva wait(), utilizzata all'interno del codice. Questa primitiva mette il processo padre in attesa finch tutti i processi figli non vengono terminati, ritorna -1 se non ci sono processi figli da attendere o un valore > 0 che rappresenta il PID del processo figlio appena terminato. Come parametro prende invece un puntatore a una variabile int. Su questa variabile viene scritto lo status con cui terminato il processo figlio (attraverso un return o la primitiva exit()) nel seguente formato (in esadecimale):
0xSS00
dove SS rappresenta lo status (in esadecimale) con cui il programma terminato (nel nostro caso 0), e le ultime due cifre sono 2 zeri. Questo nel caso in cui il processo terminato in modo naturale. Se invece dovesse essere terminato in modo innaturale, ovvero tramite un segnale da parte del padre, gli zeri e il valore dello status verrebbero invertiti. Per ottenere il valore di ritorno devo quindi ricorrere a uno stratagemma a basso livello. Faccio un AND tra la mia variabile e il numero esadecimale 0xFF00 (in binario 1111 1111 0000 0000), in modo da azzerare eventuali valori diversi da zero nelle due cifre esadecimali meno significative, quindi faccio uno shift a destra di 1 byte del valore attuale, in modo da ritrovarmi con un valore del tipo 0x00SS, che rappresenta lo status autentico ritornato dal processo figlio. La riga
while ((pid=wait(&status)>0);
dice quindi al processo padre finch la primitiva wait() ritorna un valore maggiore di zero, ovvero finch ci sono processi da attendere, salva questo valore nella variabile pid, quindi salva il valore dello status nella variabile status. Nel caso il valore di ritorno dei miei processi non mi interessi pi di tanto, posso scrivere
while ((pid=wait((int*) 0)>0);
Vediamo ora un piccolo esempio di programma al quale vengono passati due argomenti, rappresentanti due nomi di file, e che genera due processi figli, ognuno dei quali legge un carattere dal file ad esso associato e lo riporta su standard output. Ogni processo figlio ritorna al padre il numero di caratteri letti all'interno del file. (N.B.: in questo esempio ho utilizzato le primitive a basso livello del kernel Unix, ovvero open, read, write e close, per l'apertura/lettura/scrittura/chiusura di un file, e non le funzioni ad alto livello specificate in stdio.h, proprio perch voglio creare un programma ottimizzato al 100% per sistemi Unix, e suppongo che il lettore sia familiare con queste primitive. In caso contrario, si possono visionare le pagine di manuale Unix associate, o la documentazione presente su internet)
#include #include #include #include #include <stdio.h> <stdlib.h> <unistd.h> <fcntl.h> <sys/wait.h>
int main(int argc, char **argv) { /* Descrittore dei file */ int fd; int i, pid, status; /* Numero di caratteri letti */ int N = 0; /* Buffer in cui verr salvato il carattere letto */ char buff[1]; if ( argc < 3 ) { printf ("Errore nel numero di argomenti passati\n");
/* Errore */ if ( pid == -1 ) { printf ("Errore nella creazione del processo figlio\n"); exit(2); } /* Codice del figlio */ if ( pid == 0 ) { /* Provo ad aprire il file associato al processo */ if ( (fd = open(argv[i+1], O_RDONLY)) < 0 ) { printf ("Errore: impossibile leggere il file %s\n", argv[i+1]); exit(3); } printf ("Contenuto del file %s:\n", argv[i+1]); /* Finch ci sono caratteri da leggere all'interno del file, li riporto su stdout */ while ( read(fd,buff,1) > 0 ) { printf ("%c", buff[1]); N++; } close(fd); /* Ritorno il numero di caratteri letti */ exit(N);
/* Codice del processo padre */ if ( pid > 0 ) { while( (pid = wait(&status)) > 0 ) { status = (status & 0xFF) >> 8; printf ("Il processo %d terminato e ha letto %d caratteri dal file\n", pid, status); } } } return 0; }
Out); questo vuol dire che, se un processo scrive sulla pipe e un altro legge i dati scritti, quest'ultimo legge i dati nell'ordine preciso in cui sono stati scritti. Dal punto di vista di sistema, una pipe viene descritta da un array di due interi: il primo valore dell'array identifica il canale di lettura della pipe, il secondo valore identifica quello di scrittura. Su un sistema Unix per inizializzare una pipe uso la primitiva pipe(), primitiva che ritorna un valore >=0 nel caso in cui la pipe creata con successo, -1 in caso contrario, e prende come parametro l'identificatore della pipe. Piccolo esempio di utilizzo:
/* Identifico il tipo pipe_t (pipe type) come un array di due interi */ typedef int pipe_t[2]; ... pipe_t pp; if (pipe(pp)<0) { Errore! }
/* D'ora in avanti user il canale pp[0] per leggere dalla pipe, pp[1] per scrivere sulla pipe */
Ovviamente, se provo a scrivere sul canale di lettura della pipe o viceversa ottengo un errore di broken pipe, in quanto sto tentando di eseguire un'operazione non consentita. Vediamo ora un esempio pi corposo, in cui un processo padre inizializza una pipe e crea un processo figlio. Il processo figlio prende da stdin una stringa, di lunghezza massima N, inserita dall'utente e la scrive sulla pipe. Il processo padre attende che il figlio termini e scrive la stringa su stdout, leggendola dal canale di lettura della pipe. (N.B.: per leggere, scrivere o chiudere una pipe utilizzo sempre le primitive read, write e close, esattamente le stesse primitive che userei per un file o per un socket).
#include #include #include #include #include <stdio.h> <stdlib.h> <string.h> <unistd.h> <sys/wait.h>
/* Massima lunghezza dell'input inserito */ #define N 100 typedef int pipe_t[2]; int main(int argc, char *argv[]) { pipe_t pp; char buff[N]; /* Creazione della pipe */ if (pipe(pp) < 0) { printf ("Errore nella creazione della pipe\n"); exit(1); } /* Creazione di un processo figlio */ switch (fork()) { case -1: printf ("Impossibile creare un processo figlio\n"); exit(2); break; /* Codice del figlio */ case 0: /* Chiudo il canale di lettura della pipe, in quanto il processo
figlio deve interessa */ solo scrivere sulla pipe e il canale di lettura non mi close(pp[0]); /* Chiedo all'utente di inserire una stringa */ printf ("Stringa da inviare sulla pipe: "); fgets(buff, N, stdin); buff[strlen(buff)]='\0'; /* Scrivo la stringa appena letta sulla pipe */ if (write(pp[1], buff, N) < 0) { printf ("Errore nella scrittura su pipe\n"); exit(3); } close(pp[1]); exit(0); break; /* Codice del padre */ default: /* Chiudo il canale di scrittura dal lato del padre, dato che devo solo leggere dalla pipe */ close(pp[1]); printf ("Aspetto che il figlio venga terminato...\n"); wait( (int*) 0 ); /* Una volta che il figlio terminato, leggo la stringa inserita dalla pipe */ if (read(pp[0], buff, N) < 0) { printf ("Errore nella lettura da pipe\n"); exit(1); } printf ("Il processo figlio ha scritto %s sulla pipe\n", buff); exit(0); break; } } return 0;
possibile anche effettuare la ridirezione di un certo canale su una pipe. Ricordiamo che in un sistema Unix stdin, stdout e stderr non sono altro che descrittore di file speciali, identificati rispettivamente dai valori 0, 1 e 2. Quindi posso chiudere uno di questi canali e ridirigere il traffico diretto da o verso uno di questi canali su una pipe, cos come potrei fare la ridirezione su file. Esempio:
... typedef int pipe_t[2]; pipe_t pp; ... close(1); /* Chiudo stdout */ dup(pp[1]); /* Duplico il canale di scrittura della pipe, che acquista il primo canale disponibile. */
/* Poich ho appena chiuso il canale stdout, tutto il traffico diretto su stdout verr * ridiretto sulla pipe. */
Questo meccanismo, che ora abbiamo visto implementato a basso livello, viene implementato ad alto livello dai comandi di pipe della shell. Ad esempio se do un comando del tipo ps ax | grep init, non fa altro che eseguire il comando ps. Il canale di output di questo comando viene chiuso, e viene ridiretto su una pipe costruita per la comunicazione tra ps e grep.
SIGHUP process SIGINT SIGQUIT SIGILL SIGABRT SIGFPE SIGKILL SIGSEGV SIGPIPE SIGALRM SIGTERM SIGUSR1 SIGUSR2 SIGCHLD SIGCONT SIGSTOP SIGTSTP SIGTTIN process SIGTTOU
Term Term Core Core Core Core Term Core Term Term Term Term Term Ign Cont Stop Stop Stop Stop
Hangup detected on controlling terminal or death of controlling Interrupt from keyboard Quit from keyboard Illegal Instruction Abort signal from abort(3) Floating point exception Kill signal Invalid memory reference Broken pipe: write to pipe with no readers Timer signal from alarm(2) Termination signal User-defined signal 1 User-defined signal 2 Child stopped or terminated Continue if stopped Stop process Stop typed at tty tty input for background tty output for background process
I segnali si installano con la primitiva signal() (standard SystemV, quello pi utilizzato) o sigset() (standard BSD, meno utilizzato). Entrambe le primitive prendono come primo argomento il segnale associato (uno di quelli presenti nella lista), come secondo argomento una funzione di tipo void che prende un parametro di tipo int (che rappresenta il numero del segnale ricevuto); questa la funzione che verr richiamata quando viene lanciato un dato segnale. Il segnale viene invece lanciato con la primitiva kill(), una primitiva che prende come primo parametro il PID del processo che deve ricevere il segnale, come secondo il segnale da inviare. Esempio:
#include <stdio.h> #include <signal.h> void foo(int sig) {
void do_nothing(int sig) { signal(SIGUSR1,do_nothing); } main() { int pid; // Installazione dei segnali signal(SIGTERM,foo); signal(SIGUSR1,do_nothing); pid=fork(); switch(pid) { case -1: printf (Errore nella creazione del processo\n); exit(1); break; // Figlio case 0: printf (Sono il processo %d, generato da %d, e attendo un segnale da parte di mio padre\n, getpid(), getppid()); // Invio al processo padre il segnale SIGUSR1, un segnale personalizzato kill(getppid(), SIGUSR1); // Pongo il processo in attesa di un segnale attraverso la primitiva pause() pause(); exit(0); break; // Padre default: // Mi metto in attesa di un segnale pause(); // Una volta ricevuto il segnale SIGUSR1 da parte del figlio, mando al figlio il segnale SIGTERM kill(pid, SIGTERM); exit(0); break; } }
Programmazione multithread
Il concetto di thread, pur essendo operativamente molto simile a quello di processo, in sostanza un concetto diverso. Fondamentalmente, l'inizializzazione di un nuovo processo sempre qualcosa di oneroso per il sistema, in quanto un processo ha una sua area di memoria (ovvero una sua area di codice, una sua area di dati e un suo stack) e un suo PID che lo identifica all'interno della tabella dei
processi, e alla sua inizializzazione il sistema operativo dovr provvedere al nuovo processo le risorse di memoria richieste. Il thread invece lo possiamo vedere come un mini-processo (per usare una terminologia un po' grossolana ma che rende bene l'idea) che pu essere richiamato all'interno di un processo stesso. Il thread condivide l'area di memoria con lo stesso processo chiamante (ovvero condivide con il processo chiamante gli stessi dati e la stessa area di stack, il che vuol dire che una modifica sulle variabili operata da un thread visibile da tutti gli altri thread del processo stesso). Il vantaggio principale della programmazione multithread la maggiore velocit di inizializzazione e di esecuzione di un thread rispetto a quella di un processo, a costo di una minore indipendenza in fatto di memoria condivisa tra i thread di un processo stesso. Per ricorrere alla programmazione multithread in C in ambiente Unix useremo la libreria pthread (inclusa di default in molte installazioni Unix), e compileremo i sorgenti con l'opzione -lpthread. Per creare un nuovo thread useremo la funzione pthread_create(), che prende come parametri un puntatore all'identificatore del thread (una variabile di tipo pthread_t, tipo definito nell'header sys/types.h), gli attributi del thread creato (generalmente NULL), il puntatore alla funzione contenente il codice che verr eseguito dal thread (generalmente una funzione di tipo void* che prende un argomento di tipo void*) e un array contenente gli argomenti da passare alla funzione. User invece la funzione pthread_exit() per terminare l'esecuzione di un thread (questa funzione prende come parametro il valore da ritornare al processo chiamante) e pthread_join() per porre il processo chiamante in attesa finch il thread creato non viene terminato (questa funzione prende come argomenti l'identificatore del thread e un puntatore alla variabile in cui verr salvato il valore di ritorno del thread). Esempio:
#include <stdio.h> #include <pthread.h> #include <sys/types.h> // Funzione che verr eseguita dal thread void* start(void* arg) { printf (Sono un thread richiamato dal processo padre\n); pthread_exit(0); } main() { // Identificatore del thread pthread_t t; int status; if (pthread_create(&t, NULL, start, NULL) != 0) { printf (Errore nella creazione del nuovo thread\n); exit(1); } // Attendo che il thread venga terminato pthread_join(t, &status); } printf (Il thread a 0x%x terminato con status %d\n, &t, status);
Vediamo ora come passare degli argomenti alla funzione del thread:
#include #include #include #include <stdio.h> <stdlib.h> <pthread.h> <sys/types.h>
// Funzione che verr eseguita dal thread void* start(void* arg) { // L'argomento passato sottoforma di dato void, ovvero un dato grezzo. // Lo converto in int attraverso un operatore di cast int *my_arg = (int) arg; printf (Sono un thread generato dal processo padre. Mi stato passato come argomento %d\n, (*x)); pthread_exit(0); } main(int argc, char **argv) pthread_t t; int status; int arg[1]; {
if (argc<2) { printf (Passami almeno un parametro\n); exit(1); } arg[0]=atoi(argv[1]); if (pthread_create(&t, NULL, start, arg) != 0) { printf (Errore nella creazione del nuovo thread\n); exit(1); } // Attendo che il thread venga terminato pthread_join(t, &status); printf (Il thread a 0x%x terminato con status %d\n, &t, status); }
Retro di un moderno PC. In viola, la porta parallela La porta parallela una delle principali interfacce I/O su un calcolatore. Inizialmente usata per connettere la stampante al computer (oggi su molte macchine moderne questa porta non neanche pi presente in quanto la maggior parte delle stampanti al giorno d'oggi usano un'interfaccia USB), la porta parallela in seguito diventata un interfaccia estremamente utilizzata da elettronici e informatici per pilotare tramite il calcolatore dispositivi self-made, in virt dell'estrema facilit di programmazione di quest'interfaccia.
Disclaimer
La programmazione della porta parallela un campo estremamente affascinante, ma a cui avvicinarsi con cautela. Il chip che gestisce la porta parallela sulla scheda madre, e in molti casi gestisce anche altri componenti, quali dischi, interfacce di I/O ecc. Se non si ha abbastanza esperienza con il saldatore e si vuole collegare un dispositivo fatto in casa alla parallela, meglio NON collegarlo direttamente alla porta parallela sulla scheda madre. Ci vuole poco a creare un corto circuito che pu danneggiare fisicamente e in modo irreparabile il chip sulla scheda madre, che in genere non sostituibile e costringe alla sostituzione fisica dell'intera scheda madre. L'avvertenza ancora pi forte se si collega il proprio marchingegno elettronico ad un portatile nuovo di zecca. Ci vuole poco per trasformare il portatile nuovo di zecca in un oggetto da discarica se tra l'interfaccia di I/O e il proprio circuito collegato si viene a creare un corto circuito. Il mio consiglio di interporre tra il proprio dispositivo e la porta parallela sulla scheda madre un buffer tri-state che possa proteggere la scheda madre stessa, magari un tri-state integrato come il pic 74LS44, che ha il seguente schema elettrico:
O, meglio ancora, si pu inserire nel proprio slot ISA o PCI della scheda madre una scheda del genere, che costa una decina di euro:
Se c' qualcosa di sballato nel circuito salta la scheda PCI e con una decina di euro si pu comprare una nuova, e almeno non salta la scheda madre. Ancora, se si acquista un computer moderno probabile che non sia presente l'interfaccia parallela. In questo caso si pu rimediare con un adattatore USB-parallela come il seguente (una decina di euro):
Pin di una porta parallela Su una porta parallela possibile leggere o scrivere 1 byte (8 bit) per volta, informazione che viene salvata su un registro interno a 8 bit della porta. I pin che ci interessano in questa trattazione sono quindi quelli numerati da 2 a 9 (a ogni pin corrisponde un bit). I pin da 10 a 17 e 1 vengono usati come pin di controllo, per leggere lo status della porta o inviare segnali, mentre quelli da 18 a 25 sono tutti collegati a massa (tensione di riferimento nulla).
Mentre invece su un sistema Windows si pu controllare dalla gestione avanzata delle periferiche.
ioperm
Primitiva fondamentale per poter aprire un canale di comunicazione con la periferica di I/O ioperm(), il cui compito di settare o rimuovere i permessi di accesso ad una qualsiasi periferica di I/O. Come
parametri prende
L'indirizzo iniziale che identifica la periferica di I/O (0x378 nel caso della porta parallela) Il numero di byte assegnati alla periferica a partire dal byte iniziale (in genere per la parallela se ne considerano 4) Un intero che identifica se attivare o disattivare l'accesso alla periferica (1 per poter accedere alla periferica, 0 quando l'accesso non serve pi)
C' da ricordare che per utilizzare questa primitiva necessario avere i privilegi di amministratore. Quindi l'applicazione che intende accedere alla periferica necessario che sia di propriet di root e abbia il bit UID settato, in modo da poter accedere alla periferica con ioperm(). Esempio di uso:
// Indirizzo di partenza della periferica #define PORT 0x378 ... int uid=geteuid(); // Se non sono root, setto i privilegi di root if (uid) setreuid(0,0); // Accedo alla periferica if (ioperm(PORT,3,1)==-1) { perror ("Errore nell'accesso alla periferica\n"); exit(1); } // Torno a settare i permessi di utente normale if (uid) setreuid(uid,uid);
inb o outb
Per leggere un byte per volta su una porta di I/O e scriverli il kernel mette a disposizione le primitive inb e outb, utilizzabili nel proprio codice C a patto di includere l'header <asm/io.h> (in quanto sono parallele alle istruzioni ASM IN e OUT). La loro sintassi la seguente:
short int inb (int port); void outb (short int val, int port);
inb legge un byte dalla porta all'indirizzo port (precedentemente aperta) e ritorna il valore letto. outb invece scrive il byte val sulla porta all'indirizzo port. Per applicazioni diverse dalla porta parallela (che avendo 8 data pin pu interagire con 1 byte per volta) possibile leggere o scrivere sulla periferica una word per volta o una double word rispettivamente con le primitive inw-outw o inl-outl, che hanno la stessa sintassi di quelle gi viste.
Esempio pratico
Prendiamo un esempio facile facile in esame. Immaginiamo di aver collegato alla porta parallela un led, collegato in modo che sia polarizzato in diretta (terminale positivo sul data pin n.1 della porta
parallela e terminale negativo collegato ad un qualsiasi ground pin della porta parallela). Vogliamo che il nostro led si accenda a intermittenza, diciamo pure con un intervallo di 1 secondo tra un cambiamento e l'altro (in pratica vogliamo sfruttare la porta parallela come un generatore di onde quadre). La cosa possibilissima con le conoscenze che abbiamo finora. Ecco il codice in C:
#include <stdio.h> #include <stdlib.h> #include <asm/io.h> // Indirizzo della parallela #define PORT 0x378 main() { // Controllo che utente sono int uid=geteuid(); // Se non sono root, acquisisco i privilegi con un setreuid() if (uid) setreuid(0,0); // Attivo la porta if (ioperm(PORT,3,1)<0) exit(1); // Torno a essere utente normale setreuid(uid,uid); // Ciclo infinito while (1) { // Scrivo 0000 0001 sulla porta // in modo da alimentare solo il data pin n.1 // dove collegato il nostro diodo outb(0xFF,PORT); // Aspetto un secondo sleep(1); // Disattivo i data pin scrivendo 0000 0000 sulla porta outb(0,PORT); // Aspetto un secondo sleep(1); } }
Applicazione pratica
Per vedere come possibile interfacciarsi con un database MySQL tramite il C, prendiamo subito in esame un esempio pratico. Abbiamo un database MySQL chiamato "esami", che gestisce gli esami tenuti in una certa facolt. Il database contiene queste tabelle:
---CORSO--+----------+----------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +----------+----------+------+-----+---------+----------------+ | codcorso | int(11) | NO | PRI | NULL | auto_increment | | nomeC | char(40) | YES | | NULL | | | coddoc | int(11) | NO | MUL | | | +----------+----------+------+-----+---------+----------------+ ---DOCENTE--+----------+----------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +----------+----------+------+-----+---------+----------------+ | coddoc | int(11) | NO | PRI | NULL | auto_increment | | nomeD | char(20) | YES | | NULL | | | cognomeD | char(20) | YES | | NULL | | +----------+----------+------+-----+---------+----------------+ ---ESAME--+----------+---------+------+-----+---------+-------+ | Field | Type | Null | Key | Default | Extra | +----------+---------+------+-----+---------+-------+ | coddoc | int(11) | NO | PRI | 0 | | | codcorso | int(11) | NO | PRI | 0 | | | matr | int(11) | NO | PRI | 0 | | | voto | int(11) | YES | | NULL | | +----------+---------+------+-----+---------+-------+ ---STUDENTE--+------------+----------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +------------+----------+------+-----+---------+----------------+ | matr | int(11) | NO | PRI | NULL | auto_increment | | nomeS | char(20) | YES | | NULL | | | cognomeS | char(20) | YES | | NULL | | | anno_corso | int(1) | YES | | NULL | | +------------+----------+------+-----+---------+----------------+
La nostra applicazione dovr interfacciarsi con questo database in modo da poter prelevare informazioni al suo interno. Innanzitutto, prima di effettuare qualsiasi operazione sul database, bisogna inizializzare il descrittore del database, usato all'interno dell'applicazione, tramite la funzione mysql_init() (definita, come tutte le funzioni che operano su database MySQL, in mysql/mysql.h), che ha questa sintassi: MYSQL *mysql_init(MYSQL *mysql) dove MYSQL un tipo di dato primitivo usato dalla libreria MySQL e, in questo caso, rappresenta il descrittore del nostro database. La funzione ritorna NULL quando non possibile creare il descrittore. Esempio di utilizzo:
#include <mysql/mysql.h> ....... MYSQL db; if (!mysql_init(&db)) { printf ("Errore nell'inizializzazione del database\n"); exit(1); }
A questo punto bisogna collegarsi fisicamente al database sull'host in questione, usando la funzione mysql_real_connect, che ha la seguente sintassi: MYSQL *mysql_real_connect(MYSQL *mysql, const char *host, const char *user, const char *passwd, const char *db, unsigned int port, const char *unix_socket, unsigned long client_flag) dove *mysql rappresenta l'indirizzo del descrittore del database che abbiamo inizializzato prima, host l'IP o il nome dell'host sul quale ospitato il database, user l'username con cui accedere al database e passwd la sua password corrispondente, db l'eventuale database a cui collegarsi (se sulla macchina esistono pi istanze di MySQL, altrimenti si pu tranquillamente lasciare a NULL), port l'eventuale porta a cui collegarsi (se il database in ascolto su una porta diversa da quella di default, altrimenti si pu tranquillamente lasciare a 0), unix_socket l'indirizzo dell'eventuale socket da utilizzare per la connessione (in genere si pu lasciare a NULL), client_flag un intero che rappresenta eventuali informazioni aggiuntive da passare al db (in genere si lascia a 0, per esigenze particolari sul sito developer di MySQL c' una voce dedicata ai possibili valori che pu assumere questo flag, nel caso di esigenze particolari). Nel nostro caso di esempio, ci collegheremo al server MySQL presente sul nostro host locale (localhost), sfruttando il descrittore db creato prima, lo username 'root' e la password 'prova':
char *db_host="localhost"; char *db_user="root"; char *db_pass="prova"; if (!mysql_real_connect(&db, db_host, db_user, db_pass, NULL, 0, NULL, 0)) printf ("Errore di connessione al database su %s\n",db_host); else printf ("Connessione avvenuta con successo al database su %s\n",db_host);
A questo punto selezioniamo il database da utilizzare sull'host a cui ci siamo collegati. Il nostro database era quello dedicato agli esami della facolt, chiamato "esami". Per selezionare un database da un descrittore gi aperto usiamo la funzione mysql_select_db:
if (mysql_select_db(&db,db_name))
printf ("Errore di connessione al database %s\n",db_name); else printf ("Connessione avvenuta con successo al database %s\n",db_name);
Ora la connessione al database avvenuta con successo, e il database pronto ad accettare le nostre richieste. Per fare una query SQL al database si usa la funzione mysql_real_query, a cui bisogna passare i seguenti parametri:
Indirizzo del descrittore del db Query SQL, sotto forma di stringa Lunghezza della query
Esempio: vogliamo interrogare il database in modo da ottenere il numero di matricola, il nome e il cognome di tutti gli studenti che hanno sostenuto almeno un esame, con il nome dell'esame superato e il voto corrispondente. Si tratta di fare un join tra 3 tabelle del db: studente (dal quale prelevo il nome, il cognome e la matricola degli studenti), corso (dal quale prelevo il nome del corso) e esame (dal quale prelevo il voto). Ovviamente la condizione di join che il codice del corso di esame sia uguale a quello di corso, e la matricola dello studente sia uguale a quella di esame. Ordinando i risultati in modo crescente secondo il codice del corso, la query diventa cos:
char *query = "select [Link],[Link],[Link],[Link],[Link] " "from studente s,corso c,esame e " "where [Link]=[Link] " "and [Link]=[Link] " "order by [Link]";
Ora possibile salvare il risultato della query in una variabile apposita (di tipo predefinito MYSQL_RES), tramite la funzione mysql_store_result, quindi contare il numero di campi letti attraverso mysql_num_fields e salvare il nome di ogni campo (es. nomeS, cognomeS, voto...) in una variabile apposita (di tipo MYSQL_FIELD) attraverso la funzione mysql_fetch_fields. Ecco quindi come ottenere i nomi di tutti i campi letti dalla query all'interno del database e stamparli su schermo uno per uno (la formattazione del testo non sar ottimale, ma solo per capire come funziona il procedimento):
MYSQL_RES *res; MYSQL_FIELD *f; int i; ......... res = mysql_store_result(&db); f = mysql_fetch_fields(res); for (i=0; i<mysql_num_fields(res); i++) printf ("%s\t",f[i].name);
Ora stampiamo i contenuti effettivi di ogni riga della query. Per fare ci, usiamo un altro tipo di dato
primitivo di MySQL (MYSQL_ROW) e usiamo la funzione mysql_fetch_row. Per leggere tutte le righe date in output dalla query il codice diventa quindi qualcosa del genere:
MYSQL_ROW row; ......... // Finch ci sono righe da leggere... while ((row=mysql_fetch_row(res))) { // ...per ogni riga letta... for (i=0; i<n; i++) //...stampane il contenuto i-esimo printf ("[%s]\t", row[i]); printf ("\n"); }
A questo punto il nostro interfacciamento con il database completo, e ripuliamo sia il risultato della query sia l'identificatore della connessione con il database, attraverso le funzioni mysql_free_result e mysql_close:
mysql_free_result (res); mysql_close(&db);
int i; unsigned int n; MYSQL db; MYSQL_RES *res; MYSQL_ROW row; MYSQL_FIELD *f; if (mysql_init(&db)==NULL) { printf ("Errore nell'inizializzazione del database\n"); exit(1); } if (!mysql_real_connect(&db, db_host, db_user, db_pass, NULL, 0, NULL, 0)) printf ("Errore di connessione al database su %s\n",db_host); else printf ("Connessione avvenuta con successo al database su %s\n",db_host);
if (mysql_select_db(&db,db_name)) printf ("Errore di connessione al database %s\n",db_name); else printf ("Connessione avvenuta con successo al database %s\n",db_name); if ( mysql_real_query (&db, query, (unsigned int) strlen(query)) ) printf ("Errore nell'esecuzione della query %s\n",query); exit(2); } res = mysql_store_result(&db); n = mysql_num_fields(res); f = mysql_fetch_fields(res); printf ("\n"); for (i=0; i<n; i++) printf ("%s\t",f[i].name); printf ("\n"); while ((row=mysql_fetch_row(res))) { for (i=0; i<n; i++) printf ("[%s]\t", row[i]); printf ("\n"); } mysql_free_result (res); mysql_close(&db); } {
Ovviamente, le funzioni contenute in questo codice di esempio si possono riutilizzare per effettuare delle query su qualsiasi db, e anche per effettuare operazioni di creazione, inserimento e aggiornamento di record all'interno di un database. La documentazione completa per le funzioni di interfacciamento tra MySQL e C la potete trovare qui.
CGI in C
.
Utilizzando il meccanismo dei CGI (Common Gateway Interface) possibile innescare vere e proprie applicazioni che hanno la libert di svolgere qualsiasi funzione eseguibile sul web server da un programma, per poi restituire un risultato in forma di pagina HTML. Il C consente di fare operazioni del genere, in modo forse meno avanzato rispetto a linguaggi dedicati come PHP o Perl ma estremamente flessibile, date le sue caratteristiche.
La directory /cgi-bin una sottodirectory della directory del web server che contiene le applicazioni CGI. 2. Attivazione del CGI - Il server HTTP (es. Apache, Netscape Server o Microsoft IIS) riceve la URL, la interpreta e lancia il processo (o thread) che esegue il CGI. 3. Risposta del CGI - Il risultato della computazione deve dar luogo a una pagina HTML di risposta, che il CGI invia verso il suo Standard Out (per i CGI lo STDOUT viene intercettato dal server HTTP) tenendo conto di quale deve essere il formato di una response HTTP. 4. Risposta del server HTTP - Sar poi il server HTTP ad inviare la response verso il client che aveva effettuato la request. Passiamo adesso a vedere come si scrive un CGI in C, prendendo come spunto un'applicazione che stampa all'interno di una pagina web 'hello world':
//Il CGI hello.c #include <stdio.h> int main(int argc, char *argv[]) {
printf("Content-type: text/html\n\n"); /*informazione necessaria per la response*/ /*Inviamo su STDOUT i tag HTML*/ printf("<html>\n" "<head>\n" "<title>Hello World!</title>\n" "</head>\n" "<body>\n" "<h1><p align=\"center\">Hello World</p></h1>\n" "</body>\n" "</html>\n"); } return 0;
Compilando questo programma all'interno della directory /cgi-bin del nostro server web
gcc -o [Link] hello.c
otteniamo un eseguibile CGI che possiamo richiamare all'interno del nostro browser nel modo visto sopra
[Link]
L'esame del codice non nulla di assurdo, tenendo sempre presente che lo STDOUT di una CGI viene rediretto direttamente al client HTTP. Degna di nota questa riga:
printf("Content-type: text/html\n\n");
Il suo scopo quello di specificare al client HTTP il tipo di contenuto che si sta per inviare (in questo caso del testo HTML). Senza questa specifica il client non sa come comportarsi e stamper una pagina bianca. Si notino le due linee vuote; sono assolutamente necessarie, per le convenzioni del protocollo HTTP, ma sono spesso dimenticate. Il segnale di STDOUT viene quindi intercettato dal browser, e viene generata una pagina web con i contenuti specificati. Prendiamo ora in esame un rudimentale orologio che invia al client HTTP una pagina web contenente l'ora del server sfruttando la libreria time.h:
/* Ora quasi esatta */ #include <stdio.h> #include <time.h> int main(int argc, char *argv[]) time_t bintime; struct tm *curtime; {
time(&bintime); curtime = localtime(&bintime); printf("Data e ora: %s\n", asctime(curtime)); printf("</h1>\n"); printf("</body>\n"); printf ("</html>\n"); } return 0;
GET
Nel metodo GET i dati inseriti dall'utente o previsti dal programmatore vengono caricati nell'URL, e il loro contenuto, a livello del sistema server, finisce in una variabile d'ambiente chiamata QUERY_STRING. Immaginiamo ad esempio di avere il seguente codice HTML (ricordate che la scelta del metodo, GET o POST, va fatta a livello del codice del form HTML):
<form method=GET action=/cgi-bin/cgi1> Come ti chiami? <input type="text" name="nome"><br> <input type="submit" value="Clicca"> </form>
un semplice form HTML che chiede all'utente di turno come si chiama e invia la stringa inserita dall'utente all'eseguibile 'cgi1' tramite metodo GET (per convenzione gli eseguibili CGI si mettono nella directory /cgi-bin del server). Se salviamo questa pagina come '[Link]' dopo aver cliccato sul tasto 'Clicca' la richiesta verr inoltrata tramite metodo GET a cgi1, che quindi verr richiamato nel seguente modo:
[Link]
Nel caso ci fossero stati pi campi oltre al nome (ad esempio un campo 'password') avremmo avuto una cosa del genere:
[Link] nome=Nome_inserito_dall_utente&password=Password_inserita
In pratica quando inviamo una richiesta tramite GET l'eseguibile CGI che viene richiamato (o lo script PHP/ASP) viene richiamato passando nell'URL una struttura del genere:
[Link]
Ora immaginiamo che il nostro eseguibile cgi1 debba leggere il nome inserire dall'utente e generare per lui una pagina HTML di benvenuto (es. 'Benvenuto pippo!'). Ecco un potenziale codice C come potrebbe essere:
#include <stdio.h> #include <stdlib.h>
// Funzione che converte eventuali caratteri speciali // all'interno della stringa inserita dall'utente in // caratteri ASCII leggibili // Prende come parametri la stringa sorgente, la stringa // di destinazione e la lunghezza della stringa da 'uncodare' void unencode (char *src, char *dest, int len); // Funzione per il prelevamento di // un campo da una query // Prende come parametri la query in cui cercare // e il nome del campo da cercare (in questo caso 'nome') char* get_field(char *query, char *field); main() { char *query,*nome; int len; // Genero la pagina HTML printf ("Content-type: text/html\n\n"); printf ("<html>\n" "<head>\n" "<title>Pagina di benvenuto</title>\n" "</head>\n" "<body>\n"); // Se la richiesta GET non contiene niente, la pagina stata richiamata // in modo errato, quindi esco if ((query=getenv("QUERY_STRING"))==NULL) { printf ("<h3>Pagina richiamata in modo errato</h3>\n" "</body></html>\n"); exit(1); } // Controllo la lunghezza della query e // genero una stringa lunga quanto la query // che conterr il nome inserito dall'utente // Ricordiamo che query ora sar una stringa // del tipo 'nome=pippo' len=strlen(query); nome = (char*) malloc(len*sizeof(char)); // Ora nome conterr il campo 'nome' della query nome=get_field (query,"nome"); printf ("<h3>Benvenuto %s!</h3>\n" "</body></html>\n",nome); exit(0); } char* get_field(char *query, char *field) int i,j,len,pos; char *tmp,*input; {
// tmp sar il pattern di ricerca all'interno della query // Nel nostro caso andr a contenere la stringa 'nome=' tmp = (char*) malloc( (strlen(field)+1)*sizeof(char) ); // input lunga quanto la query, e andr a contenere // il campo da noi ricercato input = (char*) malloc(len*sizeof(char)); // tmp <- nome=pippo sprintf (tmp, "%s=", field); // Se all'interno della query non c' il campo richiesto, esco if (strstr(query,tmp)==NULL) return NULL; // Cerco la posizione all'interno della query // in cui stato trovato il campo nome pos = ( (int) strstr(query,tmp) - (int) query) + (strlen(field)+1); // Controllo quanto lungo il pattern nome=blablabla // Questo ciclo termina quando viene incontrato un '&' all'interno // della query (ovvero quando comincia un nuovo campo) o quando la stringa terminata // Alla fine i conterr il numero di caratteri totali nel pattern di ricerca for (i=pos; ; i++) { if (query[i]=='\0' || query[i]=='&') break; } // Salvo il contenuto della query che mi interessa in input for (j=pos; j<i; j++) input[j-pos]=query[j]; // 'unencodo' input, rendendo eventuali caratteri speciali umanamente leggibili unencode(input,input,len); // Ritorno input return input; {
'
// Ciclo finch non ho letto tutti i caratteri specificati for (i=0; i<len; i++, src++, dest++) { // Se il carattere corrente di src un '+', lo converto in uno spazio ' if (*src=='+') *dest=' ';
// Se il carattere corrente un '%' else if (*src=='%') { // Se il carattere successivo non un carattere valido, // il carattere di destinazione sar un '?', // altrimenti sar il carattere ASCII corrispondente if (sscanf(src+1, "%2x", &code) != 1) code='?'; *dest = (char) code; // Leggo il prossimo carattere
src += 2; } // Se un carattere alfanumerico standard e non un carattere speciale, // allora il carattere di destinazione uguale a quello sorgente else *dest=*src;
La funzione unencode indispensabile. Infatti, se l'utente dovesse inserire degli spazi o dei caratteri speciali qualsiasi all'interno del form (ovvero caratteri non alfanumerici) questi all'interno della QUERY_STRING verranno tradotti con i codici ASCII corrispondenti preceduti da un '%'. Ad esempio, se l'utente dovesse inserire 'pippo pappo', la query diventera 'nome=pippo+pappo'. Per convertire il carattere a quello inizialmente inserito dall'utente quindi necessario passare per unencode. Per comodit conviene tenersi le funzioni get_field e unencode da qualche parte pronte per l'uso, vista la loro utilit all'interno delle CGI in C. Personalmente ho sviluppato una piccola libreria (cgic) che contiene tutte queste funzioni utili al programmatore di CGI in C, senza che ci sia bisogno di reinventare la ruota ogni volta. Il link al pacchetto lo potete trovare alla fine di questo articolo.
POST
La scelta tra metodo GET e metodo POST legata ad un preciso criterio di programmazione, che prevede che un GET venga scelto se e soltanto se i campi all'interno del form sono idempotenti tra di loro. Questa la regola formale. La regola empirica insegna che le richieste GET vanno usate solo per campi di piccole dimensioni (ad esempio, form con checkbox, con variabili contenenti i nomi di pagine esterne da richiamare all'interno del codice, con campi contenenti piccole stringhe e cos via). Non una buona idea utilizzare richieste GET, ad esempio, per inviare un messaggio postato da un utente in un forum, in quanto verr fuori un URL lunghissimo senza senso. anche rischioso usare GET per form di login, in quanto i dati di autenticazione passerebbero in chiaro nell'URL. In tutti questi casi (e altri) consigliabile l'uso del metodo POST. Il metodo POST genera una query string che uguale in tutto e per tutto a quella generata dal metodo GET (nel nostro caso, sempre nome=nome_inserito). La differenza il metodo GET prevede che la query venga inviata al server tramite la variabile d'ambiente QUERY_STRING, mentre a livello client viene integrata nell'URL stesso. Il metodo POST invece prevede che la query venga inviata dal client al server all'interno del pacchetto HTTP stesso, e viene letta dal server come se fosse un normale input (quindi con scanf, gets o fgets). Prima di inviare la query vera e propria il client invia al server una stringa che identifica la lunghezza della query che sta per essere inviata. Questa stringa viene salvata dal server nella variabile d'ambiente CONTENT_LENGTH. In questo modo il server riceve la lunghezza della query che sta per essere inviata, prepara un buffer di dimensioni adatte e quindi legge la query con le funzioni per la lettura dell'input gi viste. Dopo la procedura rimane uguale (ovvero lettura del contenuto di una variabile con un metodo come get_field e decodifica dei caratteri con un metodo come unencode). Ecco un esempio di codice HTML che invia i dati di un form tramite metodo POST (esempio tipico, l'invio di un messaggio in un form che viene poi inviato ad un eseguibile CGI e stampato su schermo):
<form method="POST" action="/cgi-bin/cgi2"> Inserisci qui il tuo messaggio:<br>
Link esterni
possibile usare librerie gi pronte per l'uso di eseguibili CGI in C (come get_field, unencode e altre), senza dover reinventare la ruota e riscrivere funzioni da zero di volta in volta. Qui trovate la mia libreria, testata con successo su sistemi Unix, che gi contiene molte funzioni comode per la scrittura di eseguibili CGI.
Inoltre i programmi che fanno uso delle librerie PCAP, accedendo alle interfacce di rete con i privilegi di superutente, hanno bisogno di essere avviati con i privilegi di root su sistemi Unix, di amministratore su sistemi Windows.
La funzione pcap_lookupdev in pratica cerca il miglior dispositivo di rete disponibile sul sistema e ritorna una stringa ad esso associata, altrimenti NULL se non c' nessun dispositivo di rete. La stringa errbuf, di lunghezza PCAP_ERRBUF_SIZE definita in pcap.h, serve a contenere eventuali messaggi di errore. Per trovare invece tutte le interfacce di rete sul sistema possiamo usare la funzione pcap_findalldevs, che prende come parametri un puntatore a puntatore a un tipo di dato pcap_if_t e il solito buffer di errore. pcap_if_t non altro che un tipo di dato che identifica un'istanza della struttura pcap_if cos definita:
struct pcap_if { struct pcap_if *next; char *name; /* name to hand to "pcap_open_live()" */ char *description; /* textual description of interface, or NULL */ struct pcap_addr *addresses; bpf_u_int32 flags; /* PCAP_IF_ interface flags */ };
Ecco quindi un codice per visualizzare tutte le interfacce di rete disponibili su un sistema:
pcap_if_t *ifc; char errbuf[PCAP_ERRBUF_SIZE]; struct sockaddr_in *addr; pcap_findalldevs (&ifc,errbuf); printf ("Interfacce di rete trovate sul sistema:\n\n"); // Finch ci sono interfacce da visualizzare... while (ifc->next) { // ...stampo nome e descrizione printf ("%s: %s\n",ifc->name,ifc->description); // Finch ci sono indirizzi associati all'interfaccia di rete... while (ifc->addresses) { // ...stampo gli indirizzi addr = (struct sockaddr_in*) ifc->addresses->addr; printf ("Indirizzo: %s\n",inet_ntoa(addr->sin_addr.s_addr)); // Passo all'indirizzo successivo ifc->addresses=ifc->addresses->next; } // Passo all'interfaccia di rete successiva ifc=ifc->next; }
Per verificare l'indirizzo e la netmask associate ad un'interfaccia di rete conviene usare la funzione pcap_lookupnet(), che prende come argomenti
Il nome dell'interfaccia di rete Un puntatore ad una variabile a 32 bit che identifica la rete Un puntatore ad una variabile a 32 bit che identifica la netmask errbuf
La funzione ritorna -1 nel caso non ci sia nessun indirizzo associato ad un'interfaccia di rete. Esempio, per trovare l'indirizzo associato all'interfaccia di rete eth0:
bpf_u_int32 net,mask; char errbuf[PCAP_ERRBUF_SIZE]; ...... if (pcap_lookupnet("eth0",&net,&mask,errbuf)==-1) { printf ("Nessun indirizzo associato a eth0: %s\n",errbuf); exit(1); }
Sniffing
La funzione messa a disposizione dalle PCAP per l'apertura di un dispositivo di rete per lo sniffing pcap_open_live(). Tale funzione prende come argomenti:
Il nome del dispositivo di rete su cui effettuare lo sniffing Il numero massimo di byte da catturare per ogni sessione Un valore booleano (promisc) che se settato a 1 pone il dispositivo di rete in modalit promiscua. Se lasciato a 0 di default PCAP sniffer solo il traffico di rete diretto verso la propria interfaccia di rete to_ms, che identifica il numero di secondi passati i quali la sessione di sniffing va in timeout. Se settato a 0 non ci sar nessun timeout per la sessione di sniffing errbuf
La funzione ritorna un puntatore ad una variabile di tipo pcap_t, che per il resto del listato sar il descriptor della nostra sessione di sniffing, oppure NULL in caso di errore. Esempio pratico:
pcap_t *sniff; char errbuf[PCAP_ERRBUF_SIZE]; ...... if (!(sniff=pcap_open_live("eth0",1024,1,0,errbuf))) { printf ("Errore nella creazione di una sessione di sniffing su eth0: %s\n",errbuf); exit(1); } printf ("Sessione di sniffing creata con successo\n");
Questo codice apre una sessione di sniffing sul dispositivo eth0 in modalit promiscua, leggendo 1024 byte per volta, senza un timeout impostato per la sessione e con un eventuale buffer di errore salvato in errbuf. Se l'esecuzione del codice va a buon fine in sniff troveremo un descrittore per la nostra sessione di sniffing da usare in seguito nel codice. A questo punto necessario compilare la sessione di sniffing specificando un eventuale filtro. Il filtro servir nel caso in cui non vogliamo sniffare tutto il traffico di rete ma solo quello diretto o proveniente da una determinata porta, solo il traffico TCP o solo quello UDP e cos via. Per compilare la sessione useremo la funzione pcap_compile() che prende i seguenti argomenti:
Il descrittore della sessione di sniffing inizializzato precedentemente con pcap_open_live() Un puntatore ad una variabile di tipo bpf_program, dove verr memorizzata la versione compilata della nostra sessione Una stringa di filtro
La variabile booleana optimize che stabilisce se il filtro andr ottimizzato o meno La netmask sulla quale verr applicato il filtro (precedentemente inizializzata tramite pcap_lookupnet())
La stringa di filtro sar una stringa che identificher il tipo di traffico da filtrare. La sintassi dettagliata illustrata qui. In generale, una stringa di filtro strutturata nel seguente modo per il filtraggio su una determinata porta o protocollo:
[proto] [src|dst] [port numero_porta]
Ad esempio
tcp dst port 80
catturer tutti e soli i pacchetti destinati alla porta 80 e scarter gli altri. Per il filtraggio sugli host la stringa di filtro sar cos costruita:
[host] [src|dst indirizzo_host]
Ad esempio
host dst [Link]
catturer tutti e soli i pacchetti destinati all'host [Link]. Nel caso non si voglia utilizzare un filtro e si vogliano sniffare tutti i pacchetti baster settare la filter_string a NULL. Esempio pratico:
pcap_t *sniff; bpf_u_int32 net,mask; struct bpf_program filter; // Questo per sniffare senza filtri char *filter_string=NULL; // Questo per filtrare, per esempio, solo il traffico destinato alla porta 80 char filter_string[] = "tcp dst port 80"; ...... pcap_compile (sniff,&filter,filter_string,0,net);
Quest'uso di pcap_compile() compiler la nostra sessione di sniffing puntata da sniff, salver la versione compilata su filter usando la stringa di filtro filter_string, senza opzioni di ottimizzazione e usando la netmask salvata in net. Una volta creato e compilato il filtro il caso di associarlo alla nostra sessione di sniffing. Questo si fa con la funzione pcap_setfilter(), che prende come argomenti
Il descrittore di tipo pcap_t della sessione Il puntatore all'istanza di bpf_program nella quale salvato il filtro appena compilato
pcap_setfilter (sniff,&filter);
Questa riga assocer il descrittore della sessione creato in precedenza al filtro appena creato. A questo punto tutto pronto per cominciare il ciclo di sniffing vero e proprio attraverso la funzione pcap_loop(), che prende come argomenti
Il descrittore di tipo pcap_t della sessione Il numero di pacchetti da sniffare prima di uscire (0 per non imporre nessun limite) Il nome della funzione da richiamare quando giunge un pacchetto (la funzione che compier le operazioni richieste su quel pacchetto) Eventuali argomenti aggiuntivi (generalmente settati a NULL)
dir al compilatore di creare un ciclo di sniffing associato al descrittore sniff, senza imporre un limite massimo di pacchetti sniffati. Ogni volta che un pacchetto transita sull'interfaccia di rete viene richiamata la funzione pack_handle() per gestirla, funzione cos definita:
void pack_handle (u_char *args, const struct pcap_pkthdr *p_info, const u_char *packet) {
Questa la sintassi standard di una funzione passata come argomento a pcap_loop(). Il primo argomento punta all'ultimo argomento specificato in pcap_loop(), ovvero agli eventuali argomenti aggiuntivi aggiunti (generalmente NULL). Il secondo argomento un puntatore alla struttura pcap_pkthdr, che contiene informazioni circa il pacchetto appena sniffato. Questa struttura cos definita:
struct pcap_pkthdr { struct timeval ts; /* time stamp */ bpf_u_int32 caplen; /* length of portion present */ bpf_u_int32 len; /* length this packet (off wire) */ };
Ora di cattura del pacchetto Lunghezza della porzione catturata Lunghezza totale del pacchetto
L'ultimo argomento un buffer contenente il contenuto vero e proprio del pacchetto. In questo caso possiamo semplicemente scrivere all'interno della nostra funzione un
printf ("%s\n",packet);
Un programma costruito in questo modo, con una tale funzione, far un dump di tutti i pacchetti transitanti su un'interfaccia di rete su stdout. Possiamo fare qualcosa di pi elaborato conoscendo gli standard dei pacchetti TCP/IP. Ad esempio nel caso di un'interfaccia ethernet risalire al MAC mittente e al MAC destinatario del pacchetto, tenendo presente che queste informazioni occupano i primi 12 byte del pacchetto, un gioco da ragazzi:
printf ("MAC sorgente: %.2x:%2x:%.2x:%.2x:%.2x:%.2x\n", packet[0],packet[1],packet[2],packet[3],packet[4],packet[5]); printf ("MAC destinatario: %.2x:%2x:%.2x:%.2x:%.2x:%.2x\n", packet[6],packet[7],packet[8],packet[9],packet[10],packet[11]);
Per maggiori informazioni sulla struttura dei pacchetti TCP/IP rimando alle sezioni apposite nell'area reti.
Packet injection
Tramite le PCAP anche possibile fare packet injection, ovvero inserire su un'interfaccia di rete pacchetti costruiti arbitrariamente. La funzione da usare in questo caso pcap_inject(), che prende i seguenti argomenti:
Il descrittore della sessione di sniffing Il buffer contenente il pacchetto costruito arbitrariamente La lunghezza del pacchetto
Si pu quindi sniffare un pacchetto su un'interfaccia di rete, modificare il MAC o l'IP mittente e inviare una risposta al destinatario, che creder che quel pacchetto venga dal mittente specificato. Tecnica ancora pi efficace se abbinata a tecniche di ARP poisoning.
Prerequisiti matematici
Dati due vettori di grandezze e dipendente funzionalmente dal corrispondente elemento di X per V: , tale che ogni elemento , si dice media pesata il prodotto scalare
Sistemi fuzzy
I sistemi fuzzy sono sistemi che si ispirano alla logica fuzzy, una logica polivalente che si pu considerare un ampliamento della logica booleana classica, che prende in esame non solo un numero discreto possibile di valori, come lo 0 e 1 nell'algebra di Boole, ma anche possibili valori intermedi non numerabili. La logica fuzzy si pone quindi come valida alternativa alla logica tradizionale nell'esame dei problemi reali, in cui i valori che possono assumere le variabili in gioco non sono numerabili, o almeno non facilmente numerabili.
non sapr risolvere un integrale definito con il metodo dei rettangoli con la stessa rapidit con cui lo risolverebbe un calcolatore elettronico, ma pu riconoscere con una facilit disarmante un cane da un albero, o la voce di un amico da lontano, anche se disturbata da altri rumori. Delle applicazioni simili in campo tecnologico le hanno anche le reti neurali, utili, ad esempio, per il riconoscimento visivo elettronico, per il riconoscimento vocale, e cos via.
Dove sono gli input presentati al neurone o alla rete neurale, sono i pesi sinattici delle singole connessioni (ovvero quanto quella connessione influenza il risultato finale). La media pesata degli input per i pesi sinattici delle singole connessioni fornisce il potenziale postsinattico del neurone:
L'output y del neurone dato da f (P-), dove una soglia caratteristica del neurone, mentre f una funzione di trasferimento. Le principali funzioni di trasferimento utilizzate nelle reti neurali sono la funzione a gradino, la funzione sigmoidale e la tangente iperbolica, tutte funzioni aventi codominio nell'intervallo [0,1] (o [-1,1] nel caso della tangente iperbolica). La funzione a gradino, il tipo di funzione di trasferimento pi semplice usata nelle reti neurali, una funzione cos definita:
Usando questa funzione, il neurone emette un segnale y=1 quando x = (P-) 0, quindi P, mentre emette un segnale y=0 (quindi rimane inattivo) quando P<. Un'altra caratteristica funzione di trasferimento la sigmoidale, o curva logistica, di equazione
Al variare del parametro A la curva pu diventare pi o meno ripida. In particolare, la curva tende alla funzione a gradino g(x) che abbiamo visto prima per A-, mentre invece tende g(-x) quando A+.
Grafico della curva sigmoidale Se x=0, ovvero se P=, allora il valore di uscita del neurone artificiale sar 0.5, mentre invece sar approssimativamente 0 (ovvero il neurone spento) per P e 1 per P. Una propriet molto interessante di questa funzione, una propriet molto utilizzata nella fase di apprendimento delle reti neurali, riguarda sua sua derivata prima. In particolare
Questa propriet implica che la derivata della funzione sigmoidale si pu scrivere come un semplice prodotto, sorvolando le regole di derivazione, e questo molto utile a fine computazionale (un calcolatore potr trovare facilmente la derivata di una funzione cos costruita). Una funzione alternativa alla sigmoidale, relativamente meno usata nel campo delle reti neurali, la tangente iperbolica, di equazione
Ogni neurone pu dare in un dato momento, come abbiamo visto, un solo valore in output in funzione dei suoi input, mentre una rete neurale pu complessivamente dare un numero variabile di valori in output. Se quindi una rete ha input n valori in un dato momento, la rete dar in output m valori in quel momento, in funzione delle . Ovvero
Quindi i valori in input in un certo momento sono un vettore X di n componenti, generalmente compresi tra 0 e 1. Anche gli m valori del vettore di output Y sono compresi tra 0 e 1, in quanto vengono confinati in questo intervallo dalla funzione di trasferimento usata (funzione a gradino o sigmoidale), quindi il vettore X va a identificare un punto A all'interno di un ipercubo booleano di n dimensioni, e Y un punto B all'interno di un'altro ipercubo booleano a m dimensioni. Nel caso di n=m=2 gli ipercubi degenerano in 2 quadrati di vertici, mentre invece nel caso n=m=3 gli ipercubi degenerano in 2 cubi di vertici. La rete neurale pu quindi essere vista come un'applicazione binaria che associa a ogni punto A contenuto nel primo ipercubo un punto B contenuto nel secondo. La grande idea alla base delle reti neurali per non solo l'applicazione binaria tra i punti di un insieme e i punti di un altro insieme. L'applicazione associa il punto A e anche un suo intorno ad un intorno del punto B del secondo insieme. Questo molto utile nel caso in cui i segnali di input sono sporcati, ad esempio nel caso di una rete neurale per il riconoscimento vocale, in grado di fare il suo dovere anche quando il suono sporcato da rumori esterni, oppure una rete neurale per il riconoscimento calligrafico, in grado di fare il suo dovere anche quando il simbolo grafico non perfettamente identico a quello appreso in fase di training. Questa propriet deriva proprio dalle propriet tipicamente fuzzy delle reti neurali.
Tecniche di apprendimento
Le reti neurali possono apprendere in due modi diversi: in modo supervisionato e in modo non supervisionato. Nel primo caso, in ogni istante t vengono presentati alla rete dei campioni corrispondenti valori di output desiderati. Le variazioni dei pesi sinattici in input, e i
sono una funzione dell'errore, e quindi dello scarto , dove l'output ottenuto, e l'output desiderato. Gli algoritmi di apprendimento in genere hanno come obiettivo quello di minimizzare l'errore quadratico medio. Quindi un'apprendimento supervisionato richiede la conoscenza sia dei valori di input , sia dei valori desiderati viene definito il training set della rete neurale. . Questi due tipi di dato forninscono quello che
Nel caso dell'apprendimento non supervisionato, vengono forniti alla rete molti campioni di input , da associare a un numero m di classi . Il programmatore non fornisce alla rete la classe di appartenenza di ogni vettore di input; la rete stessa ad auto-organizzarsi, modificando i suoi pesi sinattici in modo da poter eseguire classificazioni corrette. Gli algoritmi di apprendimento hebbiani sono classificabili all'interno di questa categoria. Questi algoritmi, basati sulla legge di Hebb, rafforzano il peso sinattico tra due generici neuroni i, j quando la loro attivit concorde (ovvero quando i risultati delle loro funzioni di trasferimento sono di segno concorde), mentre lo indebolisce nel caso opposto, esattamente come accade tra i neuroni del sistema nervoso umano: Dove un coefficiente compreso tra 0 e 1 da cui dipende la variazione del peso sinattico.
Ovvero i tipi di dati strutturati per i neurodi, le sinapsi, i layer e la rete neurale. Definiamole in questo modo:
#define _PRECISION struct TypeNeuron { float
_PRECISION trans_value; _PRECISION prop_value; sinapsi* in_links[16]; int num_in_links; sinapsi* out_links[16]; int num_out_links; _PRECISION (*trans_func)(_PRECISION prop_value); };
Come valore di precisione della rete ho deciso di usare float (precisione semplice a virgola mobile), ma nulla ci impedisce di usare int, long int o double. Le variabili prop_value e trans_value non identificano altro che, rispettivamente, il valore di propagazione del neurone e il suo valore di trasferimento, ovvero il valore prodotto in output dalla funzione di trasferimento. Per il resto, dichiaro 16 collegamenti in entrata e 16 in uscita (ovvero 16 sinapsi che collegano il neurone ad altri 16 neuroni in ingresso e altre 16 che collegano il neurone a 16 neuroni in uscita), e tengo il conto delle sinapsi rispettivamente nelle variabili num_in_links e num_out_links. Infine dichiaro un puntatore a funzione (*trans_func), in modo da poter scegliere successivamente che funzione di trasferimento usare per quello specifico neurone.
struct TypeSynapsis { _PRECISION delta; _PRECISION weight; neuron *in,*out; };
Qui dichiaro il tipo sinapsi, caratterizzato da un puntatore al neurone di ingresso e uno a quello di uscita (in e out), un suo peso sinattico weight (corrispondente al wi che abbiamo considerato finora nelle formule) e una sua delta, corrispondente alla variazione dei pesi sinattici in fase di apprendimento della legge di Hebb.
struct TypeLayer { neuron** elements; int num_elements; void (*update_weights)(layer* lPtr); };
Un layer consiste in un insieme di neuroni, e conterr quindi un puntatore alla lista di neuroni che ne fanno parte (elements), il loro numero (num_elements) e un puntatore a una funzione per aggiornare i pesi sinattici nella fase di apprendimento (update_weights). Una rete neurale semplice generalmente composta di 3 layer:
un layer di input che prende in ingresso i dati un layer di output che fornisce i dati elaborati uno o pi layer nascosti (nel nostro caso ne basta uno) che connettono i due layer di input e output. Sono deputati alla fase di elaborazione dei dati
struct TypeNN { int max_epochs; _PRECISION l_rate; layer* layer* layer* input_layer; hidden_layer; output_layer;
};
Questa struttura identifica la rete nel suo complesso, con i puntatori ai 3 layer che la compongono e due parametri che useremo in fase di apprendimento. In particolare, max_epochs il numero massimo di epoche, ovvero di cicli di aggiornamento dei pesi sinattici, che la rete pu effettuare, mentre l_rate il learning rate della rete (corrispondente all' che abbiamo visto negli algoritmi di apprendimento). Veniamo ora alle funzioni per inizializzare gli elementi della nostra rete:
void init_net(neuralnet *net) { net = (neuralnet*) malloc(sizeof(neuralnet)); max_epochs=1024; // Valore arbitrario l_rate=0.5; // Valore arbitrario } void new_layer(layer *l) { l = (layer*) malloc(sizeof(layer)); num_elements=0; } void new_neuron(neuron *n) { n = (neuron*) malloc(sizeof(neuron)); num_in_links=0; num_out_links=0; }
} }
for(j=0;j < layer_out->num_elements; j++) { curr_out = layer_out->elements[j]; aux_syn = (sinapsi*)malloc(sizeof(sinapsi)); aux_syn->in = curr_in; aux_syn->out = curr_out; aux_syn->weight = norm(get_rand()); curr_in->out_links[curr_in->num_out_links++] = aux_syn curr_out->in_links[curr_out->num_in_links++] = aux_syn; }
Questa funzione collega tra di loro due layer (layer_in e layer_out). Per fare ci alloca memoria per ogni sinapsi tra ogni neurone di layer_in e ogni neurone di layer_out, attraverso due cicli for (il primo cicla su tutto il layer di input e il secondo su tutto il layer di output). Per ogni collegamento neuroneneurone viene creata una sinapsi, una sinapsi che, ovviamente, dovr sapere che neuroni collegare, quindi:
aux_syn->in = curr_in; aux_syn->out = curr_out;
Per inizializzare il peso della sinapsi ho utilizzato un valore casuale, cos calcolato dalla funzione get_rand():
float get_rand() float x,y; {
Quello che faccio inizializzare il seme dei numeri casuali e assegnare alla variabile x un numero casuale. Per portare questo valore all'interno dell'intervallo [-0.5, 0.5] sfrutto uno stratagemma matematico. Il codominio della funzione seno in [-1,1], quindi il codominio della funzione sinx sar ovviamente in [0,1]. Se sottraggo 0.5 al valore di questa funzione ottengo un valore compreso tra [-0.5, 0.5]. Passiamo ora agli input da fornire alla rete. In questo esempio, forniremo alla rete degli input tramite un file in cui sono salvati dei numeri separati da ';'. La nostra rete dovr imparare ad effettuare la somma algebrica, quindi nel file i primi due numeri rappresentano le quantit da sommare, e il terzo numero il risultato desiderato. Esempio:
1;2;3;5;7;12;3;5;8;.....
Dapprima dichiariamo una funzione che apra il file in questione in modalit lettura:
#define TRAINING_FILE [Link] { int open_training_file() int fd;
Vediamo ora la funzione int get_data(float *data, int fd). Questa funzione prende come parametri un puntatore a float (in cui verr salvato il numero letto) e un file descriptor (ottenuto dalla funzione open_training_file()), e ritorna -1 in caso di errore, altrimenti salva in *data il numero letto dal file fino al successivo ';'.
int get_data(float *data, int fd) int curr_char; int status; int is_dec; char buf[1],ch; {
// Attenzione: cos come dichiarata questa stringa pu essere soggetta // a buffer overflow. Imponete voi dei controlli ulteriori per evitarlo, // controllando prima quanti caratteri ci sono nel file fino al prossimo
// ';' e dichiarando la stringa dinamicamente char aux_str[256]; curr_char=0; // Ciclo finch ci sono caratteri da leggere nel file while( ( status = read(fd,buf,sizeof(buf)) ) != 0) { // Se status < 0, c' qualche errore if(status<0)if(_DEBUG)perror(strerror(errno)); ch=buf[0]; // Se il carattere letto proprio un ';', esco dal ciclo if(ch == ';') break; // Altrimenti continuo. Gli a capo sono ininfluenti if(ch == '\n')continue; // Gli unici caratteri validi al fine della lettura sono . - e tutti // i valori numerici. Se il carattere letto non uno di quelli, // ritorno errore if(ch != '.' && ch != '-' && ( ch < 48 || ch > 57 )) { if(_DEBUG)fprintf(stderr,"invalid ch %d\n",ch); data = NULL; return -1; } // Controllo quanti punti ci sono nel numero if( ch == '.' ){ // Se gi stato trovato un . allora c' un errore if(is_dec){ aux_str[curr_char++]=ch; aux_str[curr_char]='\0'; fprintf(stderr,"invalid format: two '.' found in %s\n",aux_str); return -1; } // Altrimenti, il numero decimale else is_dec=1; } // Salvo l'ulteriore carattere letto nella stringa aux_str aux_str[curr_char++] = ch; } // Termino la stringa aux_str[curr_char]='\0'; // Converto la stringa in float e salvo il valore in *data *data = atof(aux_str); return 0; }
Il codice gi abbastanza commentato, quindi non mi dilungher ulteriormente. A questo punto studiamo il modo in cui la rete processa l'output. Quando la rete legge i valori di input, i neuroni del layer di input cambiano di valore, e i nuovi valori che assumono sono quelli della coppia di numeri. A questo punto, l'informazione passer ai neuroni del layer nascosto, che calcoleranno prima il potenziale
post-sinattico quindi il valore di uscita della funzione di trasferimento che, nel nostro caso, una semplice funzione del potenziale post-sinattico. In particolare, avendo scelto come funzione di trasferimento la funzione identit, avremo Per cominciare, facciamo leggere gli input al layer di ingresso:
int fd; int status; float temp; // Apro il file con gli input fd=open_training_file(); // Ciclo su tutti gli elementi del layer di input for(i=0; i < net->input_layer->num_elements; i++) { // Se la funzione get_data ritorna un valore negativo, allora c' qualcosa // che non va negli input if((status = get_data(&temp,fd)) < 0){ fprintf(stderr,"Invalid input data\n"); free(net); return -1; } // Il valore del potenziale post-sinattico del neurone quello appena // letto da input, e il valore di trasferimento sar uguale in virt della // scelta di funzione di trasferimento che abbiamo fatto net->input_layer->elements[i]->prop_value=(_PRECISION)temp; net->input_layer->elements[i]->trans_value=(_PRECISION)temp; }
Per i neuroni appartenenti al layer di output, il discorso esattamente lo stesso fatto con il layer nascosto, e il codice rimarr perfettamente identico. A questo punto, abbiamo gi visto che possibile rendere pi preciso il valore di uscita della rete neurale agendo sui singoli pesi sinattici. Per la struttura che abbiamo dato al file di input, la rete legge dal file sia i valori da sommare sia il risultato esatto, quindi provvederemo a far leggere il risultato giusto alla rete con la funzione get_data(). Una volta che abbiamo sia il risultato desiderato, sia il risultato effettivo della rete, opereremo sui pesi sinattici della rete in questo modo: Dove una costante della rete compresa tra 0 e 1 chiamata learning rate, ed definita a nostro piacimento (pi il valore di alto, pi la rete modificher sensibilmente i suoi pesi sinattici in seguito a un errore). Un learning rate alto render pi veloce l'apprendimento della rete a discapito della precisione, mentre invece un learning rate basso render l'apprendimento pi lento, ma la rete guadagner in fatto di precisione (diciamo pure che un valore intorno a 0.5 rappresenta un buon compromesso). il valore in input al neurone e (delta di output) cos definita:
dove
funzione di trasferimento calcolata nel potenziale post-sinattico . Questa la base dell'algoritmo di apprendimento Widrow-Hoff, un algoritmo di apprendimento supervisionato che calcola i pesi necessari partendo da pesi casuali, e apportando a questi delle modifiche progressive in modo da convergere alla soluzione finale. Nel nostro caso, poich
dove des_out il valore desiderato in output e linear_derivate() sar, nel nostro caso, una funzione che ritorner sempre 1 (ovviamente cambiando la funzione di trasferimento cambier anche questa funzione). Per aggiornare i pesi useremo l'equazione appena esaminata:
void update_output_weights(layer* lPtr,_PRECISION delta,_PRECISION l_rate){ int i,j; sinapsi* sPtr; neuron* nPtr; for(i=0; i<lPtr->num_elements; i++) { nPtr = lPtr->elements[i]; for(j=0;j < nPtr->num_in_links;j++){ sPtr = nPtr->in_links[j]; // wij = Dj xi sPtr->delta = -(sPtr->in->trans_value*delta*l_rate); } } }
Lo stesso algoritmo sar valido anche per il layer nascosto. Ora, trovato l'incremento (o decremento) da applicare ai pesi sinattici, baster ciclare su tutta la rete e applicare a tutte le sinapsi i nuovi pesi:
commit_weight_changes(net->output_layer); commit_weight_changes(net->hidden_layer);
con
void commit_weight_changes(layer* lPtr){ int i,j; neuron* nPtr; sinapsi* sPtr; // Ciclo su tutti gli elementi del layer for(i=0; i < lPtr->num_elements; i++) { nPtr = lPtr->elements[i]; // Ciclo su tutte le sinapsi collegate ad un certo neurone for(j=0; j < nPtr->num_in_links; j++) { // La sinapsi sar associata al j-esimo collegamento del neurone sPtr = nPtr->in_links[j]; // Il peso della sinapsi viene aggiornato con il delta // appena calcolato sPtr->weight += sPtr->delta; // Resetto il valore di delta, in modo da potergli applicare // nuove modifiche sPtr->delta = 0; } } }
Come intuibile, maggiore sar il numero di epoche (ovvero di iterazioni di questo tipo, in cui modifico il valore dei pesi per convergere sempre pi al valore desiderato), maggiore sar la precisione dei valori di output della rete. Il valore massimo di iterazioni ammissibile l'avevamo precedentemente stabilito
all'interno della variabile max_epochs. A questo punto, nel nostro main() inseriamo un ciclo che effettua automaticamente questo procedimento per max_epochs volte:
// Ciclo per max_epochs volte for(j=0; j<net->max_epochs; j++) { // Leggo i valori in input dal file, con il procedimento gi visto // in precedenza for(i=0; i<net->input_layer->num_elements; i++) { if((status = get_data(&temp,fd)) < 0){ fprintf(stderr,"errore irreversibile, closing...\n"); free(net); return -1; } net->input_layer->elements[i]->prop_value=(_PRECISION)temp; net->input_layer->elements[i]->trans_value=(_PRECISION)temp; } // Passo i valori prima al layer nascosto, quindi al layer di output propagate_into_layer(net->hidden_layer); propagate_into_layer(net->output_layer); // Calcolo la delta di output if((status = get_data(&des_out,fd)) < 0){ fprintf(stderr,"errore irreversibile, closing...\n"); return -1; } else { out_delta = compute_output_delta(net->output_layer->elements[0]>prop_value,des_out); update_output_weights(net->output_layer,out_delta,net->l_rate); } // Calcolo la variazione dei pesi sinattici per il layer nascosto // e aggiorno tutti i pesi sinattici update_hidden_weights(net->hidden_layer,out_delta,net->l_rate); commit_weight_changes(net->output_layer); commit_weight_changes(net->hidden_layer); net_output = net->output_layer->elements[0]->prop_value; printf("DES=%f\tERROR=%f\tOUT=%f\tDELTA=%f\n",des_out, (des_out-net_output),net_output,out_delta);
Riferimenti bibliografici
Silvio Cammarata - Sistemi fuzzy Silvio Cammarata - Reti neurali [Link], [Link] - Imparare il C: una guida per Linux (che ringrazio per gli ottimi riferimenti di codice in C, una guida alla programmazione semplicemente eccellente)
Raw socket
.
I socket standard usati in C sono relativamente comodi da usare in quanto automatizzano tutti i meccanismi implementati dal protocollo TCP/IP, lasciando allo sviluppatore solo la responsabilit del livello applicativo. Tuttavia in alcuni contesti si vuole avere il controllo completo di ci che viene inviato sulla rete. Applicazioni tipiche sono l'IP spoofing (invio di un pacchetto ad un host con un altro IP) e i conseguenti attacchi Smurf. In questi casi pu risultare comodo costruirsi il pacchetto inviato sull'interfaccia di rete pezzo per pezzo. Per fare ci il C mette a disposizione i raw socket, dei socket su cui possibile inviare pacchetti grezzi creati dallo sviluppatore (ovviamente delle profonde conoscenze dei protocolli di rete e di trasporto sono richieste). Vediamo subito un esempio pratico con un'applicazione che crea un pacchetto da zero che pinga localhost e lo invia su raw socket:
#include #include #include #include #include <stdio.h> <unistd.h> <sys/socket.h> <netinet/in.h> <linux/ip.h> 8 sizeof(struct iphdr) sizeof(struct icmphdr)
typedef unsigned char u8; typedef unsigned short u16; typedef unsigned long u32; struct icmphdr { u8 u8 u16 u16 u16 }; type; code; checksum; id; sequence; {
unsigned short csum (u16 *buf, int nwords) unsigned long sum;
} main()
for (sum = 0; nwords > 0; nwords--) sum += *buf++; sum = (sum >> 16) + (sum & 0xffff); sum += (sum >> 16); return ~sum; { int i,sd,one,len; unsigned char buff[BUFSIZ],in[BUFSIZ]; char data[56]; char *tmp;
struct sockaddr_in sin; struct iphdr *ip = (struct iphdr*) malloc(IPLEN); struct icmphdr *icmp = (struct icmphdr*) malloc(ICMPLEN); srand ((unsigned) time(NULL)); sd=socket (PF_INET, SOCK_RAW, IPPROTO_ICMP); sin.sin_family=AF_INET; sin.sin_port=0; sin.sin_addr.s_addr=inet_addr("[Link]"); memset (buff,0,sizeof(buff)); for (i=0; i<56; i++) data[i]=i; ip->version=4; ip->ihl=5; ip->tos=0; ip->tot_len=IPLEN+ICMPLEN+sizeof(data); ip->id=0; ip->frag_off=0; ip->ttl=64; ip->protocol=IPPROTO_ICMP; ip->check=0; ip->saddr=inet_addr("[Link]"); ip->daddr=inet_addr("[Link]"); ip->check = csum ((u16*) buff, ip->tot_len >> 1); icmp->type=ICMP_ECHO; icmp->code=0; icmp->checksum=0; icmp->id=1; icmp->sequence=1; tmp = (char*) malloc(ICMPLEN+sizeof(data)); memcpy (tmp, icmp, ICMPLEN); memcpy (tmp+ICMPLEN, data, sizeof(data)); icmp->checksum=csum((u16*) tmp, ICMPLEN+sizeof(data) >> 1); memcpy (buff, ip, IPLEN); memcpy (buff+IPLEN, icmp, ICMPLEN); memcpy (buff+IPLEN+ICMPLEN, data, sizeof(data)); one=1; if (setsockopt (sd, IPPROTO_IP, IP_HDRINCL, &one, sizeof (one)) < 0) printf ("Warning: Cannot set HDRINCL!\n"); if (sendto (sd, buff, ip->tot_len, 0, (struct sockaddr *) &sin, sizeof (sin)) < 0) { printf ("Error in send\n"); exit(1);
Questa dichiarazione necessaria per poter usare la struttura iphdr, contenente tutti i campi di un header IP, che semplifica notevolmente il lavoro. Successivamente dichiaro la struttura di un header ICMP (icmphdr).
unsigned short csum (u16 *buf, int nwords) unsigned long sum; {
for (sum = 0; nwords > 0; nwords--) sum += *buf++; sum = (sum >> 16) + (sum & 0xffff); sum += (sum >> 16); return ~sum;
Questa la funzione per il calcolo del checksum di un header (complemento a 1 della somma dei complementi a 1 dell'header diviso in word da 16 bit). In seguito inizializzo il socket come socket raw
sd=socket (PF_INET, SOCK_RAW, IPPROTO_ICMP);
riempio gli ultimi 56 byte del pacchetto con byte casuali (struttura classica di un pacchetto ping)
for (i=0; i<56; i++) data[i]=i;
in quanto dovr calcolare il checksum sull'header ICMP e sulla parte di dati. Ora copio le strutture cos riempite in un buffer
memcpy (buff, ip, IPLEN); memcpy (buff+IPLEN, icmp, ICMPLEN); memcpy (buff+IPLEN+ICMPLEN, data, sizeof(data));
setto l'opzione IP_HDRINCL sul socket (necessaria per iniettare pacchetti raw, richiede i privilegi di root)
one=1; if (setsockopt (sd, IPPROTO_IP, IP_HDRINCL, &one, sizeof (one)) < 0) printf ("Warning: Cannot set HDRINCL!\n");
} else